diff --git a/dashboard/CHAT_CONFIG_BINDING_FIX.md b/dashboard/CHAT_CONFIG_BINDING_FIX.md new file mode 100644 index 000000000..4b2687757 --- /dev/null +++ b/dashboard/CHAT_CONFIG_BINDING_FIX.md @@ -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` 路由表),不改变后端接口语义。 +- 绑定失败不会阻止新会话创建:这样即使后端配置路由接口异常,用户仍可继续使用默认配置进行聊天,避免前端功能不可用。 diff --git a/dashboard/PR_DESCRIPTION.md b/dashboard/PR_DESCRIPTION.md new file mode 100644 index 000000000..190d33b76 --- /dev/null +++ b/dashboard/PR_DESCRIPTION.md @@ -0,0 +1,60 @@ + + + +创建新会话(`GET /api/chat/new_session`)后没有把“当前选择的配置文件(abconf)”绑定到该会话,导致新会话始终使用默认配置。 +本 PR 在新会话创建成功后,自动调用 `POST /api/config/umo_abconf_route/update` 将所选配置绑定到该会话对应的 UMO 路由;同时对 StandaloneChat(测试配置用)补齐相同的绑定逻辑,并统一 UMO/Storage Key 的生成规则与安全访问方式,避免多处实现不一致或在受限存储环境下抛异常导致绑定无效。 + +### Modifications / 改动点 + + + + +- `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. / 这不是一个破坏性变更。 + + +### Screenshots or Test Results / 运行截图或测试结果 + + + + +**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 / 检查清单 + + + + +- [ ] 😊 如果 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. diff --git a/dashboard/src/components/chat/ConfigSelector.vue b/dashboard/src/components/chat/ConfigSelector.vue index 3566b2264..d53e3ed77 100644 --- a/dashboard/src/components/chat/ConfigSelector.vue +++ b/dashboard/src/components/chat/ConfigSelector.vue @@ -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(); diff --git a/dashboard/src/utils/chatConfigBinding.ts b/dashboard/src/utils/chatConfigBinding.ts index 8a5b009bd..192f25d9c 100644 --- a/dashboard/src/utils/chatConfigBinding.ts +++ b/dashboard/src/utils/chatConfigBinding.ts @@ -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}` }; } -