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:
Lishiling
2026-02-21 21:40:16 +08:00
parent 32854397fb
commit bcd7e67394
4 changed files with 260 additions and 11 deletions
+161
View File
@@ -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` 路由表),不改变后端接口语义。
- 绑定失败不会阻止新会话创建:这样即使后端配置路由接口异常,用户仍可继续使用默认配置进行聊天,避免前端功能不可用。
+60
View File
@@ -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();
+29 -3
View File
@@ -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}`
};
}