fix: 增加安全访问函数
- 给 localStorage 访问加了 try/catch + 可用性判断:dashboard/src/utils/chatConfigBinding.ts:13 - 新增 getFromLocalStorage/setToLocalStorage(在受限存储/无痕模式下异常时回退/忽略) - getStoredDashboardUsername() / getStoredSelectedChatConfigId() 改为走安全读取:dashboard/src/utils/chatConfigBinding.ts:36 - 新增 setStoredSelectedChatConfigId(),写入失败静默忽略:dashboard/src/utils/chatConfigBinding.ts:44 - 把 ConfigSelector.vue 里直接 localStorage.getItem/setItem 全部替换为上述安全方法:dashboard/src/components/chat/ConfigSelector.vue:81 - 已重新跑过 pnpm run typecheck,通过。
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# Chat 新会话配置绑定修复说明
|
||||
|
||||
## 0. 变更摘要(改了哪些文件)
|
||||
|
||||
- `dashboard/src/utils/chatConfigBinding.ts`
|
||||
- `dashboard/src/composables/useSessions.ts`
|
||||
- `dashboard/src/components/chat/ConfigSelector.vue`
|
||||
- `dashboard/src/components/chat/StandaloneChat.vue`
|
||||
- `dashboard/src/components/chat/Chat.vue`
|
||||
|
||||
## 1. 问题背景
|
||||
|
||||
Dashboard 在创建新会话时会调用接口:
|
||||
|
||||
- `GET /api/chat/new_session`
|
||||
|
||||
但创建完成后,并没有把“当前选择的配置文件(abconf)”绑定到新会话对应的对话路由上,导致新会话会继续使用后端的默认配置。
|
||||
|
||||
在现有实现中,配置文件与对话的绑定关系依赖配置路由接口:
|
||||
|
||||
- `POST /api/config/umo_abconf_route/update`
|
||||
|
||||
`ConfigSelector.vue` 已经使用该接口来把配置绑定到当前对话(通过 UMO 作为 key),但“新会话创建”流程没有做同样的绑定操作,因此会出现新会话配置丢失的问题。
|
||||
|
||||
## 2. 修复思路(做了什么)
|
||||
|
||||
核心思路:**新会话创建成功后,立刻根据该会话的 UMO 调用 `/api/config/umo_abconf_route/update` 进行绑定**。
|
||||
|
||||
UMO 的构造与 `ConfigSelector.vue` 保持一致(`platformId:messageType:sessionKey`),避免“前端绑定的 UMO 与后端路由表 key 不一致”导致绑定无效。
|
||||
|
||||
同时为了不影响正常使用,绑定失败不会阻断会话创建(仅输出错误日志)。
|
||||
|
||||
## 3. 具体改动(逐文件)
|
||||
|
||||
### 3.1 `dashboard/src/composables/useSessions.ts`
|
||||
|
||||
**修改点:`newSession()`**
|
||||
|
||||
- 读取用户最近一次选择的配置 id:
|
||||
- 来源:`localStorage` 的 `chat.selectedConfigId`(通过 `getStoredSelectedChatConfigId()` 读取)
|
||||
- 创建新会话后(拿到 `sessionId`、`platformId`),在满足以下条件时自动绑定:
|
||||
- `selectedConfigId !== 'default'`
|
||||
- `platformId === 'webchat'`(当前 UMO 构造逻辑为 webchat 专用)
|
||||
- 绑定方式:
|
||||
- 使用 `buildWebchatUmoDetails(sessionId, false)` 生成 `umo`
|
||||
- 调用:
|
||||
- `POST /api/config/umo_abconf_route/update`
|
||||
- payload:`{ umo, conf_id: selectedConfigId }`
|
||||
- 错误处理:
|
||||
- 绑定失败不会 throw,避免导致新会话创建失败;仅 `console.error('Failed to bind config to session', err)`
|
||||
- 清理调试代码:
|
||||
- 移除临时加入的 `console.warn(...)` 调试日志
|
||||
- 移除调试用的二次校验请求(不再请求 `GET /api/config/umo_abconf_routes`)
|
||||
|
||||
**关键代码(简化后):**
|
||||
|
||||
```ts
|
||||
const selectedConfigId = getStoredSelectedChatConfigId();
|
||||
const { session_id: sessionId, platform_id: platformId } = (await axios.get('/api/chat/new_session')).data.data;
|
||||
|
||||
currSessionId.value = sessionId;
|
||||
|
||||
if (selectedConfigId !== 'default' && platformId === 'webchat') {
|
||||
const { umo } = buildWebchatUmoDetails(sessionId, false);
|
||||
await axios.post('/api/config/umo_abconf_route/update', { umo, conf_id: selectedConfigId });
|
||||
}
|
||||
```
|
||||
|
||||
**为什么这样改:**
|
||||
|
||||
- `useSessions.newSession()` 是主聊天页创建新会话的唯一入口(`Chat.vue` 通过它创建会话),把绑定逻辑放在这里可以一次性修复所有创建新会话的场景。
|
||||
- 使用与 `ConfigSelector.vue` 相同的 UMO 格式,确保后端路由表能正确命中。
|
||||
- 限制 `platformId === 'webchat'` 是为了避免对非 webchat 平台生成错误 UMO 并写入路由表。
|
||||
|
||||
### 3.2 `dashboard/src/components/chat/StandaloneChat.vue`
|
||||
|
||||
**修改点:`bindConfigToSession()` / `newSession()`**
|
||||
|
||||
- `bindConfigToSession(sessionId)`:
|
||||
- 若 `props.configId` 为空或为 `default` 则跳过
|
||||
- 使用 `buildWebchatUmoDetails(sessionId, false)` 生成 `umo`
|
||||
- 调用 `POST /api/config/umo_abconf_route/update` 绑定 `conf_id: props.configId`
|
||||
- `newSession()`:
|
||||
- `GET /api/chat/new_session` 成功后,优先调用 `bindConfigToSession(sessionId)`(best-effort)
|
||||
- 绑定完成后再设置 `currSessionId`
|
||||
- 清理调试代码:
|
||||
- 移除临时加入的 `console.warn(...)` 调试日志
|
||||
- 移除调试用的二次校验请求(不再请求 `GET /api/config/umo_abconf_routes`)
|
||||
|
||||
**关键代码(简化后):**
|
||||
|
||||
```ts
|
||||
async function bindConfigToSession(sessionId: string) {
|
||||
const confId = (props.configId || '').trim();
|
||||
if (!confId || confId === 'default') return;
|
||||
const { umo } = buildWebchatUmoDetails(sessionId, false);
|
||||
await axios.post('/api/config/umo_abconf_route/update', { umo, conf_id: confId });
|
||||
}
|
||||
|
||||
async function newSession() {
|
||||
const sessionId = (await axios.get('/api/chat/new_session')).data.data.session_id;
|
||||
await bindConfigToSession(sessionId);
|
||||
currSessionId.value = sessionId;
|
||||
}
|
||||
```
|
||||
|
||||
**为什么这样改:**
|
||||
|
||||
- `StandaloneChat.vue` 自己实现了会话创建逻辑(不走 `useSessions`),因此需要在这个组件内补齐同样的绑定动作。
|
||||
- 先绑定后激活 `currSessionId`,可以降低“UI 已开始使用该会话但配置尚未绑定”的窗口期(尤其是首次进入组件时的自动建会话)。
|
||||
|
||||
### 3.3 `dashboard/src/components/chat/Chat.vue`
|
||||
|
||||
**修改点:仅清理调试日志(无业务逻辑变更)**
|
||||
|
||||
- 移除调试用的 `console.warn(...)`,包括:
|
||||
- 组件加载/挂载日志
|
||||
- 发送消息前“无会话则创建会话”的提示日志
|
||||
|
||||
**为什么这样改:**
|
||||
|
||||
- 这些日志用于验证修复是否被调用,确认生效后应移除,避免污染浏览器控制台与用户反馈日志。
|
||||
|
||||
### 3.4 `dashboard/src/utils/chatConfigBinding.ts`
|
||||
|
||||
**修改点:新增公共工具(集中管理 “选择的配置 id” 与 UMO 构造)**
|
||||
|
||||
- 新增常量:
|
||||
- `CHAT_SELECTED_CONFIG_STORAGE_KEY = 'chat.selectedConfigId'`
|
||||
- 新增方法:
|
||||
- `getStoredSelectedChatConfigId()`:从 `localStorage` 读取当前选中的配置 id(为空则返回 `default`)
|
||||
- `getStoredDashboardUsername()`:读取 `localStorage.user`(为空则返回 `guest`)
|
||||
- `setStoredSelectedChatConfigId(configId)`:向 `localStorage` 写入当前选中的配置 id(写入失败时静默忽略)
|
||||
- `buildWebchatUmoDetails(sessionId, isGroup)`:按 `platformId:messageType:sessionKey` 的格式生成 webchat 的 UMO(与 `ConfigSelector.vue` 逻辑一致)
|
||||
- 增加安全性:
|
||||
- 对 `localStorage.getItem/setItem` 增加 `try/catch` 与可用性判断,避免在 Safari 无痕/受限存储等环境中抛异常导致页面崩溃
|
||||
|
||||
**为什么这样改:**
|
||||
|
||||
- 之前 `ConfigSelector.vue`、`useSessions.ts`、`StandaloneChat.vue` 都需要同一套“storage key / UMO 拼接”规则,分散实现容易出现不一致(导致绑定不生效)。
|
||||
- 抽成一个 utils 后,可以保证新会话绑定与配置选择器使用完全一致的 UMO/Key。
|
||||
- 同时把 localStorage 访问集中到一个位置做防护,减少各处自行访问带来的稳定性风险。
|
||||
|
||||
### 3.5 `dashboard/src/components/chat/ConfigSelector.vue`
|
||||
|
||||
**修改点:复用公共 storage 访问与 username 读取(无业务逻辑变化)**
|
||||
|
||||
- 不再直接访问 `localStorage`,改为复用 `chatConfigBinding.ts` 中的安全方法:
|
||||
- `getStoredDashboardUsername()`
|
||||
- `getStoredSelectedChatConfigId()`
|
||||
- `setStoredSelectedChatConfigId()`
|
||||
|
||||
**为什么这样改:**
|
||||
|
||||
- 避免多个文件分别访问 `localStorage` 导致实现不一致或因受限存储环境抛异常。
|
||||
- 让 `useSessions.ts` 读取到的“上次选择配置 id”与 `ConfigSelector.vue` 持久化/回写的是同一份数据与同一套兼容逻辑。
|
||||
|
||||
## 4. 额外说明
|
||||
|
||||
- 本次修复复用并遵循现有的配置绑定机制(`umo_abconf_route` 路由表),不改变后端接口语义。
|
||||
- 绑定失败不会阻止新会话创建:这样即使后端配置路由接口异常,用户仍可继续使用默认配置进行聊天,避免前端功能不可用。
|
||||
@@ -0,0 +1,60 @@
|
||||
<!--Please describe the motivation for this change: What problem does it solve? (e.g., Fixes XX issue, adds YY feature)-->
|
||||
<!--请描述此项更改的动机:它解决了什么问题?(例如:修复了 XX issue,添加了 YY 功能)-->
|
||||
|
||||
创建新会话(`GET /api/chat/new_session`)后没有把“当前选择的配置文件(abconf)”绑定到该会话,导致新会话始终使用默认配置。
|
||||
本 PR 在新会话创建成功后,自动调用 `POST /api/config/umo_abconf_route/update` 将所选配置绑定到该会话对应的 UMO 路由;同时对 StandaloneChat(测试配置用)补齐相同的绑定逻辑,并统一 UMO/Storage Key 的生成规则与安全访问方式,避免多处实现不一致或在受限存储环境下抛异常导致绑定无效。
|
||||
|
||||
### Modifications / 改动点
|
||||
|
||||
<!--请总结你的改动:哪些核心文件被修改了?实现了什么功能?-->
|
||||
<!--Please summarize your changes: What core files were modified? What functionality was implemented?-->
|
||||
|
||||
- `dashboard/src/composables/useSessions.ts`
|
||||
- 在 `newSession()` 创建会话成功后,读取最近一次选择的配置 id(`localStorage: chat.selectedConfigId`),并为 `webchat` 平台自动执行配置绑定:
|
||||
- 构造 UMO(与 `ConfigSelector.vue` 一致的 `platformId:messageType:sessionKey` 格式)
|
||||
- 调用 `POST /api/config/umo_abconf_route/update` 写入 `{ umo, conf_id }`
|
||||
- 绑定失败不会阻断会话创建(best-effort,保留默认配置作为回退)
|
||||
- `dashboard/src/components/chat/StandaloneChat.vue`
|
||||
- Standalone 模式不走 `useSessions`,因此在其 `newSession()` 中同样在会话创建后调用 `/api/config/umo_abconf_route/update`,将 `props.configId` 绑定到新会话
|
||||
- `dashboard/src/utils/chatConfigBinding.ts`
|
||||
- 抽出并复用公共逻辑:Storage Key 常量、读取/写入当前所选配置 id(带 try/catch 防护)、以及 webchat UMO 构造(避免多处手写字符串导致不一致或存储异常)
|
||||
- `dashboard/src/components/chat/ConfigSelector.vue`
|
||||
- 复用 `chatConfigBinding.ts` 的安全存储访问与 username 读取(功能无变化,避免直接访问 `localStorage` 在受限环境下抛异常)
|
||||
- `dashboard/src/components/chat/Chat.vue`
|
||||
- 清理调试日志(无业务逻辑变更)
|
||||
|
||||
- [x] This is NOT a breaking change. / 这不是一个破坏性变更。
|
||||
<!-- If your changes is a breaking change, please uncheck the checkbox above -->
|
||||
|
||||
### Screenshots or Test Results / 运行截图或测试结果
|
||||
|
||||
<!--Please paste screenshots, GIFs, or test logs here as evidence of executing the "Verification Steps" to prove this change is effective.-->
|
||||
<!--请粘贴截图、GIF 或测试日志,作为执行验证步骤的证据,证明此改动有效。-->
|
||||
|
||||
**Verification Steps / 验证步骤**
|
||||
|
||||
1. 启动后端(AstrBot Core),确保 Dashboard 可正常访问并能调用 API。
|
||||
2. 启动 Dashboard:
|
||||
- `cd dashboard`
|
||||
- `pnpm dev`
|
||||
3. 进入 Chat 页面,使用配置选择器选择一个非 `default` 的配置文件。
|
||||
4. 创建新会话(点击“新会话/新对话”,或在无会话状态下直接发送消息触发创建)。
|
||||
5. 在浏览器 DevTools → Network 中确认创建会话后出现一次:
|
||||
- `POST /api/config/umo_abconf_route/update`
|
||||
- 请求体中的 `conf_id` 为所选配置 id
|
||||
|
||||
**Local Checks / 本地检查**
|
||||
|
||||
- `pnpm run typecheck`(通过)
|
||||
|
||||
---
|
||||
|
||||
### Checklist / 检查清单
|
||||
|
||||
<!--If merged, your code will serve tens of thousands of users! Please double-check the following items before submitting.-->
|
||||
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容。-->
|
||||
|
||||
- [ ] 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
|
||||
- [ ] 👀 我的更改经过了良好的测试,**并已在上方提供了验证步骤和运行截图**。/ My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**.
|
||||
- [x] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`.
|
||||
- [x] 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.
|
||||
@@ -77,7 +77,11 @@ import { computed, onMounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useToast } from '@/utils/toast';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { CHAT_SELECTED_CONFIG_STORAGE_KEY } from '@/utils/chatConfigBinding';
|
||||
import {
|
||||
getStoredDashboardUsername,
|
||||
getStoredSelectedChatConfigId,
|
||||
setStoredSelectedChatConfigId
|
||||
} from '@/utils/chatConfigBinding';
|
||||
|
||||
interface ConfigInfo {
|
||||
id: string;
|
||||
@@ -89,8 +93,6 @@ interface ConfigChangedPayload {
|
||||
agentRunnerType: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = CHAT_SELECTED_CONFIG_STORAGE_KEY;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
sessionId?: string | null;
|
||||
platformId?: string;
|
||||
@@ -129,7 +131,7 @@ const hasActiveSession = computed(() => !!normalizedSessionId.value);
|
||||
|
||||
const messageType = computed(() => (props.isGroup ? 'GroupMessage' : 'FriendMessage'));
|
||||
|
||||
const username = computed(() => localStorage.getItem('user') || 'guest');
|
||||
const username = computed(() => getStoredDashboardUsername());
|
||||
|
||||
const sessionKey = computed(() => {
|
||||
if (!normalizedSessionId.value) {
|
||||
@@ -266,10 +268,10 @@ async function confirmSelection() {
|
||||
}
|
||||
const previousId = selectedConfigId.value;
|
||||
await setSelection(tempSelectedConfig.value);
|
||||
localStorage.setItem(STORAGE_KEY, tempSelectedConfig.value);
|
||||
setStoredSelectedChatConfigId(tempSelectedConfig.value);
|
||||
const applied = await applySelectionToBackend(tempSelectedConfig.value);
|
||||
if (!applied) {
|
||||
localStorage.setItem(STORAGE_KEY, previousId);
|
||||
setStoredSelectedChatConfigId(previousId);
|
||||
await setSelection(previousId);
|
||||
}
|
||||
dialog.value = false;
|
||||
@@ -288,7 +290,7 @@ async function syncSelectionForSession() {
|
||||
await fetchRoutingEntries();
|
||||
const resolved = resolveConfigId(targetUmo.value);
|
||||
await setSelection(resolved);
|
||||
localStorage.setItem(STORAGE_KEY, resolved);
|
||||
setStoredSelectedChatConfigId(resolved);
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -300,7 +302,7 @@ watch(
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchConfigList();
|
||||
const stored = props.initialConfigId || localStorage.getItem(STORAGE_KEY) || 'default';
|
||||
const stored = props.initialConfigId || getStoredSelectedChatConfigId();
|
||||
selectedConfigId.value = stored;
|
||||
await setSelection(stored);
|
||||
await syncSelectionForSession();
|
||||
|
||||
@@ -10,12 +10,39 @@ export interface WebchatUmoDetails {
|
||||
umo: string;
|
||||
}
|
||||
|
||||
function getFromLocalStorage(key: string, fallback: string): string {
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return fallback;
|
||||
}
|
||||
const value = localStorage.getItem(key);
|
||||
return value == null ? fallback : value;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function setToLocalStorage(key: string, value: string): void {
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// Ignore storage errors (e.g. private mode / restricted storage).
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoredDashboardUsername(): string {
|
||||
return (localStorage.getItem('user') || '').trim() || 'guest';
|
||||
return getFromLocalStorage('user', '').trim() || 'guest';
|
||||
}
|
||||
|
||||
export function getStoredSelectedChatConfigId(): string {
|
||||
return (localStorage.getItem(CHAT_SELECTED_CONFIG_STORAGE_KEY) || '').trim() || 'default';
|
||||
return getFromLocalStorage(CHAT_SELECTED_CONFIG_STORAGE_KEY, '').trim() || 'default';
|
||||
}
|
||||
|
||||
export function setStoredSelectedChatConfigId(configId: string): void {
|
||||
setToLocalStorage(CHAT_SELECTED_CONFIG_STORAGE_KEY, configId);
|
||||
}
|
||||
|
||||
export function buildWebchatUmoDetails(sessionId: string, isGroup = false): WebchatUmoDetails {
|
||||
@@ -31,4 +58,3 @@ export function buildWebchatUmoDetails(sessionId: string, isGroup = false): Webc
|
||||
umo: `${platformId}:${messageType}:${sessionKey}`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user