Appearance
ADR-203: AI 助手会话持久化与独立面板
状态: 🟢 已完成(Phase 1 多会话 IndexedDB 持久化 + Phase 2 主窗口内独立面板) 日期: 2026-07-29 关联: ADR-196(内置 AI 诊断助手,本 ADR 覆盖其 P4「状态不持久」限制与 P5「关面板清空会话」决策)、ADR-176(BackendService 双适配器,IndexedDB 统一存储范式)、ADR-093(声明式菜单 Schema,面板挂载)、ADR-075(registerPopupMenu overlay 工厂)
背景与问题
ADR-196 落地的 AI 诊断助手将对话历史(_messages)作为模块级内存变量,并在面板关闭时显式清空(P5 决策「保证下次打开为干净初始态」)。实际使用暴露两个痛点:
- 退出即丢:切走设置菜单再回来,整段对话蒸发;用户无法延续之前的诊断/对话思路。
- 界面受限:诊断助手是设置菜单下的一个子层卡片,对话框
max-height: 300px,长对话/Markdown 渲染施展不开,且与其它设置耦合在同一 overlay。
用户诉求:①对话上下文可持久(关面板不丢、重开恢复);②AI 助手作为独立界面承载,而非藏在设置菜单深处。
决策
1. 多会话持久化,统一走 IndexedDB
- 复用
frontend/src/core/backend/idb.ts,STORES新增'chats'store,并将DB_VERSION从 1 升到 2(必须升版本号:IndexedDB 的onupgradeneeded仅在版本号提升时触发,对已存在 v1 数据库必须升版本才能补建缺失 store,否则老用户chatsstore 永不创建、所有 chat-store 操作抛 NotFoundError 而被静默降级,功能完全失效)。 - 桌面(WebView2)与网页统一走 IndexedDB(项目已共用 idb.ts),不新增 Go binding、不落 config.json——避免与 LLM 配置混存、避免 Go 侧序列化对话历史、避免双端存储分叉。
- 会话拆两键存储:
meta:<id>(元信息{id,title,mode,createdAt,updatedAt},供列表快速枚举)+msgs:<id>(消息数组,懒加载)。活动会话 id 存metastore 的chat:activeId;chat-store.ts暴露clearActiveId供清空会话 / 删除当前会话且无剩余时清除指针,避免陈旧 activeId 残留。 - 封装在新增
frontend/src/core/ai/chat-store.ts,所有读操作对损坏/缺失数据降级(返回 undefined/空数组),不向上抛污染 UI。
2. 多会话模型(新建/切换/删除/重命名)
- 标题由首条 user 消息前 20 字自动派生(
deriveTitle),空则回退ai.chat.untitled;支持手动重命名。 - 空会话(无任何消息)不落盘,避免历史列表堆积空条目;
_clearChat清空当前会话时删除其磁盘记录。 - 持久化触发点:用户消息发出、每轮
_finalizeStream成功后,经DebouncedTimer500ms 防抖合并写;关面板 / 切换会话前_flushSession()同步落盘。 - reasoning 思考过程不持久化(仅存正式 content,与 ADR-196 后续「reasoning 不入
_messages」一致)。
3. 主窗口内独立面板(不开新 WebView 窗口)
- 新增
frontend/src/menus/assistant-panel.ts,用registerPopupMenu注册独立 overlay(overlayClass: 'sceneOverlay-assistant',宽min(560px, 92vw)、max-height: 85vh),复用settings-diagnostic.ts导出的renderDiagnosticPanel({ withSessions: true })。 - 不开新 WebView 窗口:虽项目已有 Wails 多窗口先例(plaza 预热窗口),但独立窗口需新 Go binding + 跨窗口配置同步 + bundle 加载模式,复杂度高且违反「单一主应用」直觉;主窗口内独立 overlay 以最小代价满足「独立大界面」诉求。
- AI 助手在底部导航栏占据独立按钮(
#btnAssistant,键 8,Ctrl+8),与模型/动作/场景/环境/设置并列,覆盖 ADR-196 的 P4「操作路径深度」——核心功能单次点击可达。 - 设置菜单不再含「AI 诊断助手」条目(
SETTINGS.DIAGNOSTIC枚举保留已备引用,folder/route 均移除)。 - 独立面板顶部含会话历史卡(
buildDiagnosticSchema({ withSessions: true }))。
覆盖 ADR-196 的既有决策
| ADR-196 条目 | 原决策 | 本 ADR 变更 |
|---|---|---|
| P4 已知限制「诊断/闲聊模式状态不持久」 | _mode/_messages 面板销毁重置 | 会话(含 mode)持久化到 IndexedDB,重开恢复 |
| P5「关面板清空会话」 | dispose 里 _messages.length = 0 | 移除清空,改为 _flushSession() 落盘;内存保留供重开复用 |
| 开放问题 #3「错误缓冲是否持久化」 | Phase 0 不做 | 本 ADR 只持久化对话会话,错误缓冲仍为内存环(不变) |
实施记录
idb.ts:STORES追加'chats'。新增
core/ai/chat-store.ts:listSessions/loadSession/saveSession/deleteSession/getActiveId/setActiveId/newSessionId/deriveTitle+ 类型ChatSession/ChatSessionFull/ChatMode。settings-diagnostic.ts:新增会话状态(_activeSessionId等)+_persistSession(防抖)/_flushSession/_loadActiveSession/_createSession/_switchSession/_deleteSessionAndAdjust/_renderSessionList;接入发送/收尾/清空/关面板;buildDiagnosticSchema加withSessions选项 +export;抽renderDiagnosticPanel/_disposeDiagnosticPanel供两入口复用。新增
menus/assistant-panel.ts:独立面板入口showAssistant。settings.ts:移除 DIAGNOSTIC folder 项 + routes 表项 + 拦截逻辑。app.css:.sceneOverlay-assistant布局 + 对话框放宽 + 会话列表样式。i18n 5 语言新增
ai.chat.untitled/newSession/history/rename/delete/deleteConfirm。测试:
chat-store.test.ts8 项(CRUD/排序/标题派生/降级)。验证:
tsc --noEmit0 错;前端 238 tests 通过;npm run build:dev通过;go build ./...通过(Go 未改,仅回归)。settings.ts:移除 DIAGNOSTIC folder 项 + routes 表项 + 拦截逻辑。3×HTML +
dom.ts+init.ts:新增独立 nav 按钮#btnAssistant并注册 click handler。events.ts:navActions[8]映射到showAssistant,buildNavMaps自动拾取 data-shortcut="8"。shortcut-app.ts:注册toggle:assistant(Ctrl+8)。i18n 5 语言新增
'shortcuts.label.assistant'。验证:
tsc --noEmit0 错;全量 tests 通过;check:docs无 ERROR。
审核修复(2026-07-29)
- P1 升
DB_VERSION至 2:原决策「onupgradeneeded自动补建,无需升DB_VERSION」错误。IndexedDB 规范规定onupgradeneeded仅在版本号提升时触发;老用户已有 v1 数据库(无chatsstore),新代码不升版本号则 store 永不补建,chat-store 所有操作抛 NotFoundError 被静默降级,功能完全失效。idb.ts升DB_VERSION=2+ onupgradeneeded 注释说明 v1→v2 路径。 - P1 修复
_deleteSessionAndAdjust复活已删除会话:原实现先deleteSession(id)再_switchSession(remaining[0].id),但_switchSession内的_flushSession会把当前_messages(仍属于已删除会话)回写_activeSessionId(已删除会话 id),复活磁盘记录。修复:删除前先清空_messages+_activeSessionId,使_flushSession走空会话早退分支。 - P2
_clearChat/_deleteSessionAndAdjust清除chat:activeId:原实现仅清内存_activeSessionId,未清磁盘meta:chat:activeId,留下陈旧指针。新增chat-store.clearActiveId(),两处调用同步清除。 - P3 文档锚点
[doc:adr-202]→[doc:adr-203]:13 处代码注释锚点编号错误(ADR 实际编号 203),批量修正。 - P3
events.tsnavActions[8] 风格统一:原.then()回调不捕获错误且与同文件 [3-5] 的async/await风格不一致,统一为async/await。 - P4 测试补充:
chat-store.test.ts增 3 项——listSessions跳过缺 id 的腐败 meta、loadSession对 messages 非数组降级为空数组、clearActiveId往返。共 11 项。 - 验证:
tsc我相关 0 错(工作区原有nav-touch.test.ts:132/164TS1361 与本审核无关);chat-store11/11 通过;全量 2499/2501 通过(2 失败均nav-touch.test.ts预存在);check:docs无 ERROR。
假设与边界
- 假设 WebView2 IndexedDB 配额足够存对话历史(纯文本,量小);不做 LRU 淘汰(后续再议)。
- 不做跨设备同步、不做云端存储、不新开 WebView 窗口。
- 底部导航栏
#btnAssistant为发现性入口(图标 lucide:bot + 标签 "AI" + 快捷键 Ctrl+8),覆盖「HUD 快捷入口」需求。