Cordy for Chrome

Changelog·Beta

v2.3.2Latest2026-07-12

💬 聊天内核重写(三 hook 合一 useCordyChat,UI 直吃 AI SDK v7 原生 parts)· ⬆️ AI SDK v6→v7 架构级升级 · 🧾 服务层债务全量清零(SLD 87/87)· 🛡️ 安全信任边界加固 + CWS 上架合规批(知情同意 / optional host)· 📦 产物 97MB→25.5MB(−74%)· 📚 资料库/书签/稍后读入 dashboard · 🔊 本地 AI/TTS 可靠性大修 · 📖 文档体系重建 + CI 四关卡闸

自 v2.3.1 起 166 commits · 548 files · +54,753 / −27,264。主线为四轮系统性整改(SLD 服务层深审 → 全项目五维剖析 → 收尾清障 → 合规批),测试 1,634 条 / 245 文件全绿。

⚠️ 升级注意:①站点访问权限改为可选——升级后页面内功能(划词助手等)需在 设置 → 通用 点一次「启用」重新授权(sidepanel 全部功能不受影响);②Hindi 本地 TTS 已移除(Devanagari 文本自动回退系统语音,与 ko/de/ru/ar 一致);③首次向云端 AI 发送内容前会出现一次性知情同意弹窗。

💬 聊天内核重写(ENG-10~14)

  • 三 hook 合一 useCordyChat:use-synced-chatuse-ai-sdk-adapteruse-ai-elements-chat 意外分层(3 套消息格式、4 个流监听器)重写为单 hook,UI 全链直吃 AI SDK v7 原生 UIMessage.parts,删 10 文件/两层中间格式;监听器收敛单一 chunk 泵,passive(旁观窗口)四段重写为响应式发现 + 结构化种子。
  • 分支正确性:parentId 分支盲修死——写入挂接与渲染共享同一图遍历内核 + 回合锚,switchRoot/switchBranch 包单事务消除并发插队腐蚀(a041796 系);双窗交错写为分叉语义,新增分叉事后提示(toast 一键切换查看,f36ba0b)。
  • 停止不再丢数据:用户停止的部分回复如实落库(含 stoppedByUser 标记),纯工具轮次落库,停止标记以 parts 终态精准判定;工具 denied 状态一等公民(拒绝不再显示成功/转圈)。
  • 长会话打开提速:历史加载窗口下沉 loader(初载 50 + 真实「加载更早」),500 条会话挂载冻结消除(c8beba4)。

⬆️ AI SDK v6→v7(2144efb 合入)

  • 流协议原生化 + ToolLoopAgent 统一编排(净删 ~2,800 行);工具审批切 v7 一级 toolApproval,退役自研审批层;书签分类切结构化输出。

🛡️ 安全与信任边界

  • 提示注入防线补全:浏览器派生数据(页面正文/选区/工具返回)统一标记不可信(2c9326e);审批闸伪造封死 + offscreen 通道 sender 校验(e20efb6);命令模式收窄工具集(9c731b9);logger 脱敏浏览器端真强制(0c3ce83);WAR 加 use_dynamic_url 防扩展指纹(a883d27);三条静默吞写改可见失败(1ba0ec7)。

🛡️ CWS 上架合规批(C1~C3)

  • 知情同意(C1):首次向云端 AI 发送前弹一次性数据使用确认;所有云端出口(聊天/翻译/压缩/摘要/分类/云 TTS)fail-closed 单闸收敛,可随时在 设置 → 数据与隐私 撤回(9e431c8/8063358/99b8883);本地 AI 路径不涉。
  • 站点权限可选化(C3):manifest 移除 install-time host_permissions,content script 改运行时注册,首用手势授权 + 撤销自愈 + 重启持久(6700b01);安装时零"读取所有网站"警告。
  • 划词弹窗可关闭:设置新开关,关闭瞬间即时生效,右键/命令面板/TTS 入口不受影响(41a1b87)。
  • 合规文书三件套(隐私政策/商店叙事/审核说明书)入库 docs/compliance/

🔊 本地 AI / TTS 可靠性

  • Kokoro:空音素句张量层守卫修死 OrtRun 零帧崩溃(emoji/外语文本,13a5cd4);引擎偏好单源化(删孤儿双存储,f3fb098);TTS watchdog 三段栈(结构化恢复 + 探活分级 + SW 心跳)。
  • Gemma:模型下载可取消(URL-scoped abort;真机两轮打回修复 transformers.js 吞 AbortError 致 UI 永卡、取消残留 stub 假"已下载",8e8afcc/c6c299a/70eb623);内联翻译/解释结果可保存到聊天(0dfdd6f)。
  • Hindi 本地 TTS 移除(4a04de5):17.86MB all 语言包不再内置,Devanagari 回退系统语音。

📦 产物瘦身 97MB → 25.5MB

  • ORT wasm 双份打包去重 −22.5MB(bec79b2);死语言包 zlx/gmw tree-shake −9.2MB(497bd2c);Hindi all 包移除 −17.9MB;chunk 体积预算收紧为 4MB 真闸。

📚 资料库与 Dashboard

  • 浏览历史/稍后读/收藏/对话历史迁入 dashboard;书签真目录树(重命名/新建/删除/移动/拖拽写回 chrome)+ AI 分类(无 key 回退 Gemini Nano);稍后读三段分流(inbox/later/archived);跨面全文检索(34fc0dd)。

🧾 工程与质量

  • SLD 服务层债务审计 87/87 清零(数据完整性/死代码/契约收敛/runId 化,34acd52);数据存续批(近全量备份 v5、CordyDB v7~v11 迁移测试、ghost API-key 对账、自定义 provider 密钥 UI)。
  • god-module 拆分:offscreen main / local-ai-tab(2164→109)/ chat-message(1157→338)/ inline relay / transport 装配(ENG-20~24);enhanced-chat-layout 908→596。
  • CI 四关卡闸(push/PR 触发 compile/lint/test/build,40e872d);compile:test 类型债 137→0 恢复阻断;两道棘轮闸(禁静默失败 / 禁裸 CJK 串)。
  • 文档体系重建:ARCHITECTURE / MV3_PLATFORM_CONTRACT / AI_ENGINEERING / ENGINEERING_GATES 四份 canonical + STACK 生成器 + 死链 CI,归档 ~60 份过期文档,指令文件 5→1。
v2.3.12026-06-24

🛡️ 代理安全三道防线 + 代码评审整改批次(findings F01–F30)· 🧹 God-module 拆分(background index.ts 2437→113 行 / enhanced-chat-layout.tsx 2277→885 行)· 🗂️ OneTab 一键收纳 · 🔤 翻译结果多开对比 · 🛠️ 会话检索 + 标签页 AI 工具 · 📊 单会话 token 用量看板 · ⚡ 流式可靠性与内容脚本性能大修 · ⬆️ react-router 8 + 依赖升级

自 v2.3.0 起 65 commits · 140 files · +12,312 / −5,187。本条目以 gpt5.5pro 代码评审(findings F01–F30)整改为主线,叠加 6/16 的功能批次与流式 / 启动可靠性修复。无破坏性数据格式变更。

🛡️ 代理安全(Agent Safety · F02)

  • 三道防线:浏览器上下文(页面正文 / 选区)标记为不可信数据以防提示注入(27fdac9,防线 A);新增「禁用浏览器工具」总开关,云端 + 本地 Gemma 双侧 gate(8b6de68,F02-B);写 / 敏感工具人工审批闸默认开启(7cd46ac,防线 C),审批文案 i18n 同步全部 locale(06afb3b,F02-C)。
  • closeTab 按显式 tabId 关闭消除竞态(1edb5f5,F02-D/F27);收紧 Agent 工具入参校验(83487ab,F27)。

🔒 安全加固(评审 findings)

  • offscreen ↔ background 旁路消息加 sender 鉴权438c504,F04)。
  • URL 脱敏后再交给模型 / 持久化(a98def8,F07)。
  • history / bookmarks 改为 optional 权限 + 授权 UI;tabs / scripting / host 仍安装期(e024d57,F19)。
  • content 引导 → UI 改用隔离世界私有通道,替换可被宿主页面伪造的 window CustomEvent(24440df,F24)。
  • secure-storage 密钥清理仅删可证实损坏项 + DB 重试设上限(b68f5ed,F23)。
  • 备份导入加 schema + 版本上限校验836746b,F06)。
  • 删除死代码 GET_API_KEYS / WARMUP / ApiKeyCache84a41a8,F03/F30)。

🧹 God-module 拆分(F20)

  • background index.ts 2437 → 113 行(−95%):按「同步监听先注册 → 异步服务后初始化」的 MV3 启动约束,拆为薄壳 + 10 个 service 模块 + 组合根(steps 3/4/5/7/8/9/10/12a–c/12),含启动关键的 Offscreen / TTS / SidePanel 生命周期控制器。
  • enhanced-chat-layout.tsx 2277 → 885 行(−61%):抽出 ChatHeader 组件 + useChatModelCapability / useChatContextResolver / useComposer / useConversationActions 等 hooks(steps 1/2/6/11 + SP 收尾)。

🗂️ 标签页与工具

  • OneTab 一键收纳 + 恢复即移除22c04a8);扩展图标右键菜单 + Ctrl+Shift+K 一键收纳本窗口标签(3fd0d7c)。
  • 会话检索 + 标签页 AI 工具d6e5413)。
  • 宿主页翻译结果卡片多开对比55eb002)。

📊 聊天

  • 单会话 token usage 看板5e91a6e)。
  • error 态可重发,修复重试按钮 / 命令菜单死锁(8730313);允许纯附件消息提交(e4b0a40,F26)。
  • 移除聊天流强制 maxOutputTokens7a0c837)。

⚡ 性能与可靠性

  • 每会话独立 useChat,后台 tab 流式不再中断(8140658);流式期间保活 SW,修复长会话 Failed to fetchfbeec58)。
  • content-ui idle-unmount:完全闲置 60s 后经 WXT ui.remove() 释放 React root,下次划词在同一 handle 上 ui.mount() 重挂(3f4c582);移除 content-ui 缓存破坏参数(088d67a,F14)。
  • 流式批缓冲 50 → 16ms,消除「一小段一小段」停顿(4792e09)。
  • 多标签提取限并发 + 每标签超时(6cd1701,F25);页面提取限界 + 收紧隐藏 / 类名过滤(5eeca3a,F08)。
  • sidepanel 渲染:单一根 TooltipProvider1963d0d,F15a)、TTS 状态变更不再 fan-out 到全部消息(7967b38,F15c)、仅用户消息查询 sibling 分支(b20e000,F15)、ChatMessage memo 正确性(80b60dd,F16/F17)。

🐛 Bug 修复

  • 触摸板单点切换会话 tab(4a01586)。
  • @ 引用已开标签页静默注入取内容a5eccbf)。
  • 内容脚本 context 失效时静默降级,消除 uncaught 报错(858c623)。
  • 划词菜单关闭 / 重试 + 内联卡片启动错误态(58509ff,F09/F11/F12);TTS progress / complete / error 按 requestId 过滤(e7c821b,F13)。
  • streamdown 短回复尾部标记强制扫描(7b678bf,F29);mention 编辑器超 max-h 时内部滚动,避免大段文本溢出叠加页脚(b1affa0)。
  • ORT .wasm 打包进扩展,去除远程托管 WASM(162ed12,F01,启动阻断级)。

⬆️ 依赖与构建

  • react-router 7.18.0 → 8.0.1(major)d4a94bf)+ 10 个低风险运行时 / 工具依赖升级(b527b01)。
  • minimum_chrome_version=142 + bundle 预算单一来源(c108c07,F28/F21)。
  • i18n:t() key 静态校验脚本 + 批量新增 key(bcb4d18);lint expect-expect 识别自定义断言助手(778df77)。

📚 文档

  • 新增 cordy 代码评审整改实现报告(51922cb);归档 gpt5.5pro 代码评审报告与整改方案(285234b)。
v2.3.02026-06-15

🪪 品牌重命名 Web Nexus → Cordy · 🌐 浏览器上下文 @ 提及 · 🔤 翻译工作台 · 🧠 预算驱动上下文装配 + 🗜️ 会话自动压缩 · 🖥️ 本地原生 runtime(Ollama / LM Studio / llama.cpp / vLLM / LocalAI)· ☁️ 新增 Vercel AI Gateway + 火山方舟 Ark provider · 🔊 火山云端 TTS(语音合成 2.0)· ⌨️ 命令面板重构 · ⚡ 性能与可靠性大修 + 架构审计批次

自 v2.2.5 起 72 commits · 495 files · +39,860 / −21,340。本条目把开发期的 v2.3.1(翻译工作台)/ v2.3.2(上下文基座)/ v2.3.3(本地原生 runtime)/ v2.4.0(浏览器上下文提及)四个里程碑合并为单次 v2.3.0 发布。

🪪 品牌重命名:Web Nexus → Cordy(2dc0257

  • 全项目从 Web Nexus / Nexus 更名为 Cordy,覆盖文档、代码、i18n 文案、storage key、数据库与测试夹具;lib/database/nexus-db.tscordy-db.ts
  • 无行为变更,但 storage key 改名(web-nexus:*cordy:*web-nexus-local-native-runtimescordy-local-native-runtimes 等)。按项目「No backward compatibility」原则不加迁移 shim,新装即用新 key。

🌐 浏览器上下文 @ 提及(Browser Context Mentions)

  • 浏览器提及上下文模型87dab66):新增 mention context 数据模型,把标签页 / 书签 / 历史等浏览器实体抽象为可在聊天中引用的上下文源。
  • 内联 @ 提及编辑器8b2c9de):聊天输入框引入基于 Lexical 的 MentionEditor,输入 @ 即可挑选浏览器实体并内联成 chip。
  • 显式标签页正文抽取f2762c0):background 支持按需提取指定 tab 的正文内容,供被提及的页面作为上下文注入。
  • 接入聊天工作流41098ba):被提及的浏览器引用随消息进入装配链路,作为 page-context 计入预算。
  • 提及编辑器粘贴文件40f17d0):PasteFilesPluginCOMMAND_PRIORITY_NORMAL 抢在 PlainText 默认粘贴前拦截带文件的剪贴板(截图 / 复制的图片),走与上传按钮相同的 attachments.add() 校验;纯文本粘贴不受影响(修复启用提及后粘贴图片被静默丢弃)。

🔤 翻译工作台(Translation Workbench)

  • Dashboard 翻译工作台:新增 #/translate 懒加载页面,支持源/目标语言、default/local/cloud runtime、共享 Settings 翻译模型选择、长文本输入输出、停止、重试、复制和清空。
  • 长文本顺序分块翻译:新增 Translation Workbench 后台服务和纯 chunker,按段落优先拆分,顺序广播 chunk 进度;default 只走 Chrome Translator API,local 走 Settings 中选定的本地翻译模型,cloud 走 Settings 中选定的云端 provider/model,三者不做隐藏 fallback。
  • Sidepanel 翻译入口:聊天头部新增翻译工作台快捷按钮,直接打开 Dashboard 翻译页。
  • 流式输出be98a85):工作台译文改为逐 chunk 流式回填右栏。
  • 保留源排版5dc26d5):chunker 改为「每非空行一个 chunk」并携带原始换行分隔符,重组用 译文 + separator 拼接,单元测试锁定「源文逐字可重建」不变量;修复单 chunk 输入丢失段落 / 列表结构的问题。

🧠 上下文基座(Context Substrate)

  • 预算驱动的聊天上下文装配1e60401):聊天发送链路从固定历史条数升级为按 token 预算选择历史。新增 lib/ai/context-budget.ts 纯模块,按 effectiveWindow − systemTokens − currentInputTokens − fileTokens − pageContextTokens − toolOverheadTokens − outputReserveTokens − safetyMarginTokens 计算 history budget;从最新往最老遍历候选,整条消息塞得下才入选(不做消息内截断),塞不下的老消息跳过而非截断。historyCount 退化为候选上限(默认 20),最终包含数由预算决定。
  • 保守可控的预算缺省:未知模型 context window 退回 10,000 token 兜底;safety margin = max(1024, floor(effectiveWindow · 0.15));output reserve = min(maxOutputTokens ?? 2_000, floor(effectiveWindow · 0.25));tool overhead 占位 1,000 token;非文本附件占位 100 token / 件。三字符约一 token 的字符近似估算继续使用,不引入 provider tokenizer 包。
  • 每发预算重读ChromeExtensionTransport 的 settings getter 在每次 sendMessage 从当前 chatSlotModelId 解析 contextWindow / maxOutputTokens,模型 mid-conversation 切换不会复用过期窗口而过截或溢出。
  • 生产可观测性ChromeExtensionTransport 现在按 debug / warn 两档输出装配日志 —— 正常装配走 debug(生产被过滤),出现 overBudgetomittedHistoryCount > 0 时升到 warn 并附完整 budget payload,可被生产日志捕获、与 provider 4xx context-length 报错相关联。
  • Feedback Loops 计数:新增 lib/services/context-budget-stats.service.ts,按 session 维度计数 totalAssemblies / contextBuilderFallbacks / budgetShrinkEvents / overBudgetEvents,并将最近 50 次 shrink/overBudget 事件存到 chrome.storage.session(key cordy:context-budget-stats),用于本地 dogfood 观察上下文预算收缩行为。日志正文仍不写入。
  • 默认值集中historyCount 默认值(20)现在由 lib/ai/context-budget.tsDEFAULT_HISTORY_COUNT 单点持有,transport 镜像引用,两层不再各自硬编。
  • 明确未做:v2.3.2 不生成 AI 摘要、不引入 RAG / 长期记忆、不做历史消息内部截断。Dashboard 显示 budget stats 也推迟到后续聚焦改进,只要 dogfood 信号有价值。
  • 质量门pnpm compilepnpm test -- run(981/981 tests, 146 files)、pnpm lint(0 warnings / 0 errors over 601 files)、pnpm build:chrome(Chrome MV3 production build)全绿。

🖥️ 本地原生 runtime(Local-Native Runtimes)

  • 本地原生 runtime 配置层(commits 24c5df08b0ab25):新增 OpenAI 兼容 localhost runtime 抽象,覆盖 Ollama / LM Studio / llama.cpp --server / vLLM / LocalAI / 任意 OpenAI 兼容端点。Runtime 不再伪装成 cloud provider —— 在 Local AI 类别内独立成卡,与 Chrome Built-in AI 和浏览器内 Gemma/Kokoro 并列。
  • Loopback no-key 政策lib/local-native/local-native-url.ts):localhost / 127.0.0.1 / [::1] 允许无 API key 直接 detect / run;非 loopback base URL(LAN / 公网)必须填 API key,UI 在 detect 前就 fail-fast,不容用户误把远端无密钥端点配进来。
  • Runtime 健康与发现服务lib/services/local-native-runtime.service.ts):/v1/models 是 v2.3.3 唯一发现面;超时 / CORS / 错误 payload / 非兼容 JSON 各自映射到 not-running / connection-blocked / wrong-api-surface / no-compatible-model。无未知态兜底,每条错误都对应一个用户可操作的提示。
  • Runtime store + dogfood 计数lib/stores/local-native-runtime.store.tslib/services/local-native-runtime-stats.service.ts):runtime 配置走 chrome.storage.local 持久化;本地 dogfood 计数(successfulConnections / discoveredModels / failedHealthChecks / runtimeCategorySelections)走内存态 session-scoped,无外传 telemetry。
  • Model catalog bridgelib/stores/local-model-catalog.store.ts):发现的 local-native 文本模型与现有 local model 选择面并列,沿用 sidepanel / dashboard 同一模型选择 UX,slot 选择器零分叉。
  • Slot resolver local-native 执行lib/services/model-slot-resolver.ts):chat / translation / explanation / summarization 四个文本 slot 均可路由到 local-native 模型,resolver 输出携带 base URL + 可选 API key 的执行态。
  • AI Service 通过 OpenAI 兼容 provider 执行lib/services/ai-service.ts + entrypoints/background/handlers/ai-handlers.ts):local-native chat 复用 background 的 streamText 路径,与 cloud chat 同源;不走 offscreen不做 cloud fallback、运行时不可达时显式失败,与 spec §5 一致。Local runtime 未上报 contextWindow 时退回保守缺省 4096;supportsTools 默认 false,v2.3.3 不暴露工具调用开关。
  • Local AI 设置 UI 三类entrypoints/dashboard/pages/settings/local-ai-tab.tsx):Local AI tab 重组为 Chrome Built-in AI / 本地 native runtime / 浏览器内 Gemma & Kokoro 三个类别;runtime 卡显示 base URL、状态、发现模型数、可操作 hint;loopback 不要求填 API key 字段。i18n 覆盖 12 语言。
  • 不做的事:v2.3.3 不引入 Native Messaging / 桌面伴侣 / 自动安装 / LAN 或公网 no-key 放行 / RAG / 长期记忆。Dashboard 显示 dogfood 计数推迟,运行时 stats 仅作为后续本地 runtime 深挖的反馈来源。
  • 质量门(v2.3.3 close 时刷新):参见 docs/releases/v2.3.3-local-native-runtimes/tracking.md Release Gates。Manual smoke 协议见 docs/releases/v2.3.3-local-native-runtimes/manual-smoke.md,覆盖 Ollama / LM Studio loopback、chat 与 workbench slot、不可达 runtime、非 loopback 拒绝六个场景。

🗜️ 会话自动压缩(Conversation Compaction)

  • Phase 0 · 预算省略提示669ed20):上下文预算早已算出为塞进窗口而丢弃的最早 N 条消息,但此前只写日志。现按 segment 记录最近一次「budget」省略,并在会话顶部显示 ContextOmissionNotice 横幅(「上下文已满 —— 最早 N 条消息未发送给模型」);仅 budget 原因上屏,command-skip / history-count-zero 属预期不提示。13 语言 i18n,零额外 token 成本。
  • Phase 3 · 结构化摘要c603491):CordyDB v7 新增 segmentCompactions,按 segment 持久化结构化摘要,latest-wins 重压缩,编辑 / 删除 / 切分支即失效。Transport 把最早若干轮折叠进摘要、在原始历史挤压之上为其预留 token、作为前导上下文轮注入。触发方式:/compact 命令、预算驱动自动压缩、resume 时的 stale-recap;设置在 General tab,聊天内显示压缩提示与撤销按钮。
  • 健壮性c348b05 / 9767cb6 / 8021215,E2/E7/E8/E9):自动压缩异常安全、失败冷却(COMPACTION_FAILURE_COOLDOWN_MS)、staleness 基准排除在途消息、压缩态从 Dexie 读取、isCompacting 贯穿 transport。

☁️ 新增云端 Provider(f90a9b3

  • Vercel AI Gateway:单 key 把 creator/model id 路由到任意上游;getCredits() 探针在信任公开 catalog 前先校验 key。
  • 火山方舟 Ark:OpenAI 兼容 chat provider(Doubao / DeepSeek),把 reasoning_content 原始 chunk 作为 reasoning delta 暴露到流。
  • 内置 provider 单一来源:枚举规范化的 builtin provider 列表,新增 provider 自动出现在选择器中;两者均动态拉取 /models catalog。
  • Sidepanel 感知bb74e2f / 8021215):side panel 在 mount 与每次 visibility / focus 重获时从 Dexie reconcile 设置,按 base URL 识别 火山方舟(volces.com)/ AI Gateway(ai-gateway.vercel.sh),错过的广播能自愈,不再卡在「需要密钥」。

🔊 火山云端 TTS(语音合成 2.0)(6667a7f

  • offscreen 火山引擎(HTTP 单向、PCM),独立 cloud-TTS provider 命名空间、音色目录与 API key 存储。
  • background 把 TTS slot 优先路由到云端(独立于 Local AI 开关),失败回退 Kokoro / Web Speech,各路径都尊重语速。
  • Dashboard 云 TTS 设置卡(key + 音色 + 试听)与 sidepanel 紧凑变体;朗读入口在选词菜单中常驻。
  • 正确性653fdbd,E4/E10):COMPLETE 由 AudioBufferSourceNode.onended 驱动而非墙钟定时器(暂停不会触发早到的 COMPLETE);错误帧中止 fetch 并 flush TextDecoder 尾部。

⌨️ 命令面板重构

  • 结构化查询 + 排序 + 历史 + tab 导航eb30213):新增 parse-palette-query / rank-palette-results 服务与 command-palette 类型;background 接入 command metadata、历史与 tab 导航 handler;content 面板 UI 呈现排序结果与最近历史。
  • 合并重复导航结果4ef0e6a)、破坏性操作内联二次确认1bcb363)。

📜 历史与书签

  • 历史去重 + frecency 排序1ab2513):Tabs 面板浏览历史原为 200 条原始 chrome.history 的扁平倒序列表。dedupeHistoryItems 按归一化 URL(去 hash)合并、累加访问次数、保留最近变体;sortHistoryByFrecency 按 log 阻尼的 visitCount × 分桶 recency 排序;抓取上限 200 → 1000。
  • 书签自动同步进 Dashboardd4db195):新增 BookmarkSyncService.autoSyncFromChrome()——节流(30s)、去重、保留 Cordy 富化(AI 标签 / slug / 描述 / 文件夹)的增量同步,并发调用共享一次在途运行;在书签页与首页概览 mount 时先同步再读 store;修正误连到「全量同步不清空」(会重复书签)的增量按钮。

♿ 无障碍

  • 模型源选择器 roving-tabindex9395811):把云 / 本地切换升级为 WAI-ARIA radiogroup,单一 tab 停靠在选中项,方向键在启用项间移动并选中、环绕、跳过禁用项。
  • 工具栏 group-focus-within 键盘可达(8021215,B11)。

⚡ 性能与可靠性

  • D1 · content script 真分包,入口降到 6KB48ab89f):把重 UI(React + AI Elements + 划词 UI + content CSS)从每页注入的 content 主 bundle 剥离为 unlisted 入口 content-ui.js(958KB)+ content-ui.css(127KB),经 web_accessible_resources 暴露,划词 / 激活时才 import() 按需加载;manifest content_scripts 不再带 CSS(126KB 不再每页注入)。实测 content.js = 6,282 字节(performance-budgets 硬锁 <10KB)。
  • D2 · 长会话渲染窗口化b4dffc2):trace 实测打开 500 条会话挂载 ~427ms,瓶颈为 React 挂载 + Streamdown markdown 解析。改为只挂载最近 60 条(流式 / 最新恒在窗口尾),更早经「加载更早」按钮分步展开;不触碰贴底逻辑,切换会话时窗口重置。
  • D6 · 流跨 SW 重启的中途态恢复3fc84b0):MV3 SW 被回收后内存里的 streamMetadata 清空。新增 stream-session-snapshots,按 conversationId 把累积内容镜像到 chrome.storage.session(首帧即写、每 chunk debounce 200ms、终止清除);watchdog 超时从「直接报错」升级为 recoverFromStreamWatchdog——拉快照、补 missingTail、平滑 enqueue END(stopped),仅在毫无内容时才报错。
  • D9-V5 · persist 异步水合门控e0b34c8):newtab / wallpaper 两个 persist store 经异步 storage 适配器水合,首帧曾闪默认值且水合前的写入会被回灌覆盖。新增 use-store-hydrated,未水合时渲染中性骨架;store 用 module boolean 门控所有写 action,未水合静默拒绝(zustand v5 postRehydration 先于 finishHydration,无丢写窗口)。
  • 条件 keep-alive + 懒探测637d04d,C3/F25):keep-alive 改为仅在有流 / TTS 活动时用 chrome.alarms 启动,空闲时 SW 可休眠;Chrome-AI 能力探测改懒执行,启动不再创建 offscreen 文档。
  • 流式正确性653fdbd / 9767cb6 / 637d04d,A2/A3/B3/B5/B9):传播 fullStream error / abort、ERROR chunk 用 errorText;两个 offscreen 引擎按 ~50ms / 2KB 批量 AI_STREAM_TEXT_DELTA 并在终止事件前强制 flush;cloud 路径 END sentinel 保证 END 前 flush 末段;按 conversationId 过滤 tool-call / source 事件;用确定性 streamId 持久化 + GET_STREAM_SNAPSHOT 种子化被动累加器。

🐛 Bug 修复

  • provider 接线与 slot 收敛c348b05,A1/A4/A5/C4):共享 provider-factory、isSameConfig 比对 apiKey、inline AI 按 providerId 分发、日志 redact apiKey;model-slot-resolver 统一委托 resolveProviderConfigState,删除 ai-service 遗留的 openrouter baseUrl 嗅探重映射。
  • 模型发现鉴权c348b05,C5#2):Anthropic 用 x-api-key + anthropic-version、Google 用 x-goog-api-key 头(移出 URL query)。
  • DB 错误不再被吞c348b05,C5#3):MessageServiceError extends Error,getMessage / updateMessage 不再静默吞 DB 错误。
  • secure-storage 解密失败保留密文c348b05,B11-9):抛 CorruptedApiKeyError 而非自动删除可恢复的 key。
  • 数据重置广播8021215,B12/F23):sidepanel 响应 ALL_DATA_DELETED / IMPORTED(重置 segment 缓存 / chat store / 重载 tabs),selectiveDelete 删聊天时重广播。
  • 跨 context 同步 / 设置并发9767cb6,B2/B13):9 个 persist store 经 watchPersistKey + storage-adapter listener 在另一 context 写入时重水合;saveSettings read-before-write 三路合并,避免并发写互相覆盖。
  • 孤儿 content script 语言存储守卫35aed5e):扩展 reload 后残留的 content script 失去 chrome.storageisExtensionStorageAvailable() 在不可达时跳过 defineItem / 持久化、回退默认语言,消除未捕获的 storage 权限 rejection。
  • Local AI 设置页崩溃 + i18n2634857 / c790e54):useLocalNativeRuntimeStore((s) => s.listRuntimes()) selector 每次返回新数组触发 React #185 死循环;根因修复为从 store 移除 listRuntimes footgun API,改订阅 runtimes record 并在组件体内 Object.values;补齐 localAI.sections.localNative.* 等 i18n 子树。
  • runtime 模板 i18n keyfecb729):runtime 种类带连字符(llama-cpp),而 Chrome messages.json key 只允许 [a-zA-Z0-9_],改携带显式下划线 labelKeyllama_cpp)。

🗄️ 数据库 schema 演进(bdc340d

  • CordyDB:v7 新增 segmentCompactions(会话压缩);v8 去掉 *searchTokens multi-entry 索引 + tokenize hook;v9 新增 &streamId 唯一索引(确定性 stream 持久化 upsert 去重)。
  • wallpaper-db v2(D7):把整图 blob 拆到独立 imageBlobs 表(列表只读缩略图,应用时才懒取整图),无损 v1→v2 迁移。
  • secure-key-dbdbPromise 单例去重;移除 master-password-only 元数据方法。
  • storage stats(B7/F17):unified-storage-stats 改用 navigator.storage.estimate() 取真实用量,最新消息用 .last()(不再 O(N) 扫描)。
  • deleteOldestDay 链修复(D4/F17):清理最旧一天时把幸存者 dangling parentId 置空以修复链,并 GC 空 segment 的 anchor + 压缩记录。

🧹 死代码清理与治理(637d04d / ae61bb0 / 4c9aecf / 5c0c606

  • 删除消息队列 / 通知层(C1,此前 dequeue 因 HandlerRegistry 恒空而全部被丢弃);删除 resilience 子系统与 master-password 特性(C2,secure-storage 的 auto-key API-key 路径保留);background 重构合计下线 ~4,500 行
  • knip 死代码归零(C5#1):收窄 knip entry 至 entrypoints / scripts / tests,删除真死符号 / 文件(state-sync、孤立 chrome-adapter 函数、master-password crypto 簇等),其余仅文件内使用的去 export。
  • 删除 6 个薄 façade 服务(tabs / windows / action / sidepanel / storage / messaging),调用点直接 import chrome 适配器。
  • build-protected.cjs 注明仅供自分发、绝不上架 CWS(混淆会被 Web Store 审核拒)。

📦 依赖与工具链

  • 工具链升级cd1cdcd):ai-sdk、React 19.2.7、Vite 8.0.16、Vitest 4.1.8、oxlint 1.68、oxfmt 0.53、Lexical 0.45、Tailwind 4.3;新增 pnpm minimumReleaseAgeExclude 白名单。
  • 移除未用的 pino-prettya66ee06);删除过期 GEMINI.md0a5caac)。
  • DXa0cfe23 / d1389b0):新增 commit gate、调试 / 范围规则、逐文件 format-on-edit hook(容忍非 lintable 文件)。

📊 范围

  • 72 commits(24 feat / 17 fix / 17 docs / 9 chore / 3 refactor / 1 perf / 1 test)· 495 files · +39,860 / −21,340
  • 质量门(v2.3.3 close 时刷新,184aae3):pnpm compile 干净、pnpm lint 0/0、npx vitest run 1088/1088、pnpm build:chrome MV3 production;后续提交各自 gate 绿(vitest 增至 1148+)。
v2.2.52026-05-11

🔒 安全加固(消息路由 sender-auth + 载荷 schema + 一次性确认令牌 + 主密码按需派生)+ 🌊 流式协议统一(segmentId 串通) + 🔊 TTS 修复 + 🛠️ 工具链与质量门(自 v2.2.3 起 20 commits,206 files,+24,848 / −3,512)

🔒 Security(核心主题)

  • 🔴 消息路由 sender 鉴权 + 全量载荷 Zod schemab264911):之前任意带 string type 的对象都能到达 GET_API_KEYS / EXPORT_ALL_DATA / DELETE_ALL_DATA / IMPORT_ALL_DATA / SELECTIVE_DELETE / UPDATE_AI_SETTINGS / 主密码等特权路由 — content-script XSS 或 confused-deputy 即可走通密钥外泄/数据删除路径。新增 sender-auth.ts(区分 content-script / extension-page / offscreen)+ EXTENSION_PAGE_ONLY_MESSAGE_TYPES 白名单;新增 message-payload-schemas.ts 对每条注册消息做 Zod 校验,未注册类型 fail-closed(添加路由忘加 schema 直接是硬错误)。background/index.ts 抽出 register-message-routes.ts + message-router-payload.ts,瘦身 ~80 行
  • 🔴 主密码改为按需派生、不再持久化ddd65d5):之前会把 PBKDF2 派生的 CryptoKey 序列化到 IndexedDB 作为"主密钥材料",等同于把口令副本留在磁盘。重设计为每次解密按需 PBKDF2 派生、内存即用即弃;持久化只保留 KDF 参数 + verifier。重置流程同步走单次 reset token
  • 🔴 破坏性操作需一次性确认令牌e400b7a):DELETE_ALL_DATA / SELECTIVE_DELETE / IMPORT_ALL_DATA / RESET_MASTER_PASSWORD 仅靠 sender-auth 仍存在 dashboard 域内 confused-deputy 风险。新增 destructive-confirmation.ts 签发 60s 过期、单次消费、按 action 作域的 token(落地在 chrome.session.storage,重启即清)。Dashboard data tab 先 REQUEST_DESTRUCTIVE_TOKEN 再带 token 调用,handler 缺/过期/错配直接拒
  • 🔴 内容脚本 Markdown 渲染收紧 + URL scheme 白名单2efeb4f / b66ccf3):浮卡 sanitizer 改为元素 + 属性双白名单(href 仅放行 http/https/mailto/tel/#anchor),DOM 方法在模块装载期捕获并 .call() 调用以抗主世界 DOM clobbering;assertSafeNavigationUrl 拒绝 javascript: / chrome: / file: / chrome-extension:openUrl / cleanSiteData 等工具受其约束
  • 回退 LLM 工具二次确认 + 修复 dashboard 鉴权a9873e1):b66ccf3 把每次 LLM 工具调用都包一层确认,AI SDK 6 把"待确认"当成正常 tool result,模型立刻说"已打开 …"但副作用从未执行 — 用户的 prompt 本身就是授权,二次点击纯属噪音。删除 tool-confirmation.tsCONFIRM/DENY_TOOL_ACTIONToolCallRow 的确认 UI,恢复 browser/tab/bookmark/history/data 工具直连。同时修 b264911 的回归:isExtensionPageSender 把"sender.tab 为空"作为排除条件,dashboard / newtab 跑在普通 tab 里 sender.tab 是非空的,导致 dashboard 所有特权操作被拒;改为仅按 URL(chrome-extension://${id}/ 且非 offscreen)分类

✨ New Features

  • 🌊 流式协议统一 + segmentId 全链路1dcd7ec):解决三处 deep-review 流式正确性问题。P1-1:用户在流过程中切换 tab,回答曾落到错误 conversation tab — 改为 dispatch 时把 segmentId 锁在 activeRequestSegmentRef,onFinish / 持久化 / 缓存失效 / passive accumulator 都消费这个 captured 值,不再读 focus-time 单例。P1-3 / P2:TabBar 流式指示器原本读 in-process registry,云端流在 SW realm 启停,sidepanel 看不到 — 改为 background 广播 AI_STREAM_START/END 携带 segmentId,useActiveStreamSegments 订阅 + 挂载时 GET_ACTIVE_STREAMS 拉快照;Gemma streamText / Chrome AI Prompt API 本地路径也接入 StreamingRegistry
  • 🪟 Text Action 窗口感知 + SIDEPANEL_READY 握手0395fb6):context-menu / 命令面板触发的 text action 之前用 broadcastRuntimeMessageEXECUTE_TEXT_ACTION,所有窗口的 sidepanel 都会响应;inline-ai fallback 也只看全局 focused segment。改为 receiver 比对 sidepanel 自身 windowId 主动丢弃跨窗消息;inline-ai 把发起窗口 windowId 串到 fallback。同步把原来 150ms setTimeout 等 sidepanel mount 的 hack 替换成 SIDEPANEL_READY 握手(同时清掉 CLAUDE.md 禁止的 "调试用 inline timer")
  • TTS pause/resume 全链路 + 取消遗留音频4df40b2):新增 sanitizeTextForTTS,剥离代码块/强调/链接/标题/列表/表格/HTML/emoji/零宽字符,接入 chat-message 和 content/App 两条入口,避免把语法符号当字面量发音;补全 SIDEPANEL_TTS_PAUSE/RESUME(之前 sidepanel 暂停因 ttsPlaybackController.mode=null 完全无效);KokoroTTSEngine.softCancel 释放 preloadedAudioElement,消除取消时尾音泄漏;sidepanel 端口 onDisconnectfinishAllSidepanelTTSRuns 并向 offscreen 取消,关闭即停

♻️ Refactored

  • AI 设置类型拆分092d315):单体 ai.types.ts 拆成 types/defaults/migrations 三模块,降低 import 圈和重新编译成本
  • Sidepanel 按需加载设置面板 + i18n adaptera45388b):deep-review P2 — MainContent 之前首屏即预加载 tabs / history / bookmarks / settings,settings 是其中体积最大的。抽出 settings-panel-loader.tsx 按需加载;新增 lib/chrome/i18n-adapter.ts,UI 不再直接读 chrome.i18n;扩展 tabs-adapter / windows-adapter 的 helper 覆盖前面提交引入的新调用点

🐛 Bug Fixes

  • fix(background,offscreen) 路由载荷不再做 routerPayload ?? msg 兜底dc270f6):兜底会把整个消息信封灌进 noPayloadSchema 这类期待 undefined 的 schema,造成无关字段污染校验。同时 chrome-ai-chat-engine 通过 normalizeChromeAIOutputLanguage(navigator.language) 传入 expectedOutputLanguage,Prompt API session 在受支持的 locale 上创建
  • Chrome AI 语言警告8c42db1):消除 Chrome 内置 Prompt API 在不传 expectedOutputLanguage 时的告警噪音
  • chat.toolCall.action.unknown i18n 缺失c5d37fb):hooks 在 toolName 缺失时 default 为字符串字面量 "unknown"ToolCallRow 拼出的 chat.toolCall.action.unknown 键在 12 个 locale 全部缺失,每次渲染都吐 missing-key warn 然后落 .default。12 个 locale 补齐(1136 → 1137 keys)
  • fix(lint) content/App 直调 chrome.* + 测试 toThrow 留空803da28):chrome-api-boundary 脚本(见 26ff17d)收紧 content/ 例外仅限 entrypoints/content/index.tsxApp.tsx 5 处 browser.runtime.* 暴露 — 改走 addRuntimeMessageListener / sendRuntimeMessage,两个 useEffect 简化为单行(adapter 直接返回 cleanup)。oxlint vitest/require-to-throw-message 标出的 5 处空参 .toThrow() 全部钉上 /.+/
  • 本地 AI 模型已缓存却闪 100% 下载进度46d798a):inline-ai 在命中本地缓存的模型时仍触发一次 100% 进度推送,UI 误显示"刚下载完成"。短路掉缓存命中路径,避免假阳性

🧪 Quality Gates & Tooling

  • chore(tooling) pin pnpm + CI gates26ff17d):固定 pnpm@11.0.8、恢复 lockfile;新增 chrome-api-boundary 脚本,仅 entrypoints/{background,offscreen}/**entrypoints/content/index.tsx 允许直调 chrome.* / browser.*,UI/Sidepanel/Dashboard/Newtab 一律走 adapter;与 CLAUDE.md 写定的例外清单 1:1 对齐
  • 测试覆盖:route registration 完整性、sender-auth 白名单契约、每条 schema 覆盖、按特性的 payload 校验矩阵(AI / chrome-AI / command-palette / database / security / settings-panel / storage / text-action);新增 tests/use-text-action-receiver-window.test.tsx / tests/use-text-selection-initial.test.tsx;新增 tsconfig.test.json 隔离测试编译配置

📦 Dependencies

  • chore(deps) 小版本升级 + docs/upgrades/ai-sdk-v7-plan.md AI SDK v7 升级计划落档(7143e09

📚 Documentation

  • 新增 docs/reviews/2026-05-07-deep-review.md(Codex 深审,P1/P2 findings 覆盖 conversation routing / sender auth / 主密码 / AI tools / content-script Markdown / release pipeline / sidepanel preload / streaming registry / import-export / content-script scope)
  • 新增 docs/reviews/2026-05-07-code-quality-review.md(orchestration 文件体积、runtime contract 弱点、stream 协议重复、quality gate 可信度、UI 密度、browser API boundary 漂移、测试架构、tooling 配置)

📋 Manual QA Checklist

  • Dashboard 重置/导出/删除/选择性删除/导入数据 → 都能完成(验证 destructive token 流程没把正路堵死)
  • Dashboard 配置 / 编辑 / 删除 API key → UPDATE_AI_SETTINGS / GET_STORAGE_STATS 不再被"Sender not authorized"拒(a9873e1 回归)
  • Content script 通过 chrome.runtime.sendMessage 伪造 GET_API_KEYS → 应被 router 直接拒(sender-auth 生效)
  • Sidepanel 选中文本朗读 → 暂停 → 恢复(验证 SIDEPANEL_TTS_PAUSE/RESUME 链路);关闭 sidepanel → offscreen 即停
  • 同时开两个 Chrome 窗口的 sidepanel → 在窗口 A 选中文字"翻译" → 仅窗口 A 的 sidepanel 接收
  • 流式对话期间切换 conversation tab → 回到原 tab 看到回答完整、未串到错误 tab
  • 选中本地 Gemma 4 + 已缓存模型 → 不再出现 100% 下载进度闪现
v2.2.32026-04-29

🤖 本地 AI 工具调用 + 🪜 时序内联工具渲染 + 🔄 跨 context 设置同步(自 v2.2.1 起 33 commits)

✨ New Features

  • 本地 Gemma 4 工具调用支持:之前 Gemma 4 E2B / E4B 在 sidepanel 聊天时无法调用项目里注册的 tools(getCurrentTab / searchBookmarks / searchHistory / openUrl 等),模型会回答 "I don't have access to tools"。现在通过 @browser-ai/transformers-js v2.2.2 把 transformers.js 包装成 AI SDK 6 LanguageModelV3 provider,本地路径调用 streamText({ tools: allTools }) 与云端路径走完全相同的入口,Gemma 4 的 native function calling (<|tool_call>call:fn{k:v}<tool_call|> 格式) 由 provider 内置的 ToolCallFenceDetector 自动解析
  • 🪜 内联时序消息渲染:引入 MessagePart[] 数据模型,让助手消息的 text 段和 tool 调用按真实时序穿插渲染,而不是「全部文字 → 所有工具行堆底」。新组件 <ToolCallRow> 单行内联指示器(绿色勾 / spinner / URL detail)取代旧的 grouped collapsible <ToolCallProgress><MessageBody>parts[] 顺序遍历交替渲染 <MessageResponse><ToolCallRow>buildMessageParts 把 AI SDK 6 message.parts[] 翻译为内部 MessagePart 形式(text / tool-call 两种类型,丢弃 reasoning/source 但用作 text 合并边界)
  • 🔄 跨 context AI 设置同步:在 dashboard 配置完 API 密钥后,sidepanel / newtab 立即感知到「已配置」状态。新增 AI_SETTINGS_UPDATED runtime 广播 — saveSettings 写完 Dexie 后通过 chrome.runtime.sendMessage 通告所有 extension document,接收方从 Dexie 重读并 setState。listener 在模块顶层注册(不再依赖 loadSettings() 调用),sidepanel 不显式 hydrate 时也能即时同步。复用现有 lastSelfSaveTimestamp 防止发起者收到自己回声

♻️ Refactored

  • entrypoints/offscreen/streamtext-engine.ts(新建):在 offscreen 文档运行 streamText,迭代 result.fullStream 把 AI SDK 6 stream parts 翻译为现有的 AI_STREAM_* 广播契约。云端和本地路径的下游事件形状现在完全一致,sidepanel transport 零改动
  • handleSendAIMessage 路由简化:删除 handleSendAIMessageViaGemma4(~300 行)+ activeGemma4Cancels map + subscribeToGemmaChunks import。新路径 handleSendAIMessageViaStreamText 把 chat 消息通过 local-llm-stream-text action 派发到 offscreen,AbortSignal 通过 local-llm-stream-text-cancel 桥接
  • Lazy-load StreamTextEngine:offscreen 启动时不加载 @browser-ai/transformers-js(~37 KB)和工具链,直到第一次本地 chat 请求才动态 import。从未使用本地 AI 的用户 offscreen entrypoint 仍是 38 KB(与之前一致)
  • <ToolCallProgress> 移除:196 行 grouped/collapsible 工具卡(顶部块状渲染)下线,全部消费者迁移到 <MessageBody> + <ToolCallRow> 内联渲染。passive-mode 累加器 + onFinish 持久化路径同步维护 MessagePart[],保证流式与历史回放视觉一致
  • AI Settings store 三路同步重组startAISettingsLiveSync(Dexie liveQuery,同 context)/ startStartupSnapshotSync(localStorage storage event)/ 新增 startCrossContextSettingsSync(chrome.runtime 广播,跨 context)三条通道并存,统一通过 lastSelfSaveTimestamp 去重;MessageRouter 注册 AI_SETTINGS_UPDATED no-op handler 消除 background 侧 "Unknown message type" 噪音

📦 Dependencies

  • 新增:@browser-ai/transformers-js@^2.2.2(HuggingFace 官方推荐的 transformers.js + AI SDK 6 适配器,Apache-2.0)

🐛 Bug Fixes

  • 冷启动后选中本地 Gemma 显示 "AI功能尚未配置"enhanced-chat-layout.tsxchatSlotState useMemo 没有订阅 useLocalModelCatalogStore,cache 探测完成后 catalog 翻成 "downloaded" 但组件不重渲染。订阅 state.catalog 并加入 useMemo 依赖;同时修复 chat-model-routing-card.tsx 的同类问题。回归测试见 tests/chat-slot-state-catalog-subscription.test.tsx
  • 🔴 工具调用从未渲染ChromeExtensionTransport 误用 AI SDK 4 时代的 tool-call / tool-result chunk 类型,AI SDK 6 的 uiMessageChunkSchema 不识别这些类型并静默丢弃,导致 chatResult.messages.at(-1).parts[] 永远没有 tool 部分。修正为 tool-input-available / tool-output-available(参考 node_modules/ai/dist/index.d.ts:2027
  • 🔴 工具行被甩到消息最底部而非按时序穿插:transport 整流共用一个 textId,AI SDK 6 把所有 text-delta(工具前后的文字)合并到同一个 text part,结果 parts[] 变成 [text(全部), tool, tool, …]。修复:tool 边界处发 text-end,下一次 text-delta 用新 textId 发 text-start,让 AI SDK 6 建立 text → tool → text 多段独立 part
  • 🔴 「已读取页面内容」工具行 URL 闪一下才出现ai-service.ts tool-result 分支按 AI SDK 4 老语义只发 state: completed(不带 input/output),等 fullStream 结束后通过 result.steps 二次补发 input/output。AI SDK 6 的 fullStream tool-result chunk 直接携带 input + output(StaticToolResult / DynamicToolResult),无需补发。修复:第一遍就发 input + output,ToolCallRow 立即拿到 URL detail
  • 🔴 流式结束工具行闪一帧消失streaming → ready 切换瞬间,streamingAIMessage 立即变 null,但写入 persistedToolCalls 的 useEffect 在 commit 之后才跑,那一帧 aiElementsMessages 中最后一条助手消息 toolCalls: undefined,MessageBody 跳过所有 tool rows。修复:aiElementsMessages 派生时对最后一条 assistant 消息 fallback 到 activeToolCalls,闭合过渡帧空窗
  • 🔴 Dashboard 配置密钥后 sidepanel 仍显示「需要密钥」:(a) Dexie liveQuery 不跨 document 触发;(b) sidepanel 从未在启动时调用 loadSettings()(仅靠 localStorage 启动快照),所以 cross-context listener 也没注册。修复:广播听器移到模块顶层 + 不再依赖 hydrated 守卫
  • metadata.parts 缺失导致活跃会话工具调用不渲染extractMetadata 只抽 toolCalls/reasoning/attachments,从未写 chronological parts[] 到 ChatMessage metadata。流刚结束的活跃会话消息 metadata.parts: undefined → MessageBody 落入 legacy 兜底(故意不渲染 toolCalls[])。修复:extractMetadata 调用 buildMessageParts(sdkMessage.parts) 并写入 metadata.parts

🧪 Testing

  • 新增 tests/offscreen-streamtext-engine.test.ts — 6 个单元测试覆盖 stream parts → broadcast 契约(text-delta / tool-call / tool-result / tool-error / error / abort)+ cancel() 中断
  • 新增 tests/chat-slot-state-catalog-subscription.test.tsx — 锁定 catalog 订阅契约的回归测试
  • 新增 tests/build-message-parts.test.ts / tests/parts-onfinish-extraction.test.ts / tests/parts-accumulator-reducer.test.ts — buildMessageParts 转换、onFinish 持久化、passive-mode 累加器
  • 新增 tests/message-body.test.tsx — MessageBody 时序渲染(text 与 tool 交替、缺失 toolCalls 静默跳过)
  • 新增 tests/ai-sdk-adapter-parts.test.tsextractMetadata 把 AI SDK 6 tool 部分翻译为 metadata.parts 的契约
  • 扩展 tests/ai-settings-store-cross-page-sync.test.ts — 增加 runtime broadcast 路径覆盖(broadcast → re-read 与防御 malformed payload)

📋 Manual QA Checklist

  • 选中 Gemma 4 E2B → 询问 "我现在在哪个标签页?" → 验证模型调用 getCurrentTab 并返回结果
  • 询问 "搜一下我的书签里有没有关于 React 的" → 验证 searchBookmarks 调用
  • 云端模型 + 工具调用:验证「已打开 / 已读取页面内容」工具行夹在调用前后两段文字之间(不是堆在底部)
  • 「已读取页面内容」工具行首次出现就带 URL,渲染收尾时不闪烁
  • 中途按 Stop → 验证 generation 立即中断(不是等到 max_new_tokens)
  • Dashboard 配置 OpenRouter 密钥 → 切回 sidepanel → 立即重开模型选择器 → 「需要密钥」消失(无需切 tab 也无需 reload)