docs: complete Chinese proofreading and generated reference pairing

This commit is contained in:
xjt
2026-08-09 11:02:16 +08:00
parent 0543a6f79d
commit 7ff0cbcb7e
565 changed files with 10606 additions and 2171 deletions

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md
2026-07-24-web-gui-browser-e2e-lane.md: 095ad9055c8462ecec67609ed9af55df2a2d7adb
2026-07-24-web-gui-browser-e2e-lane.zh.md: eb20ee378eb5154bafcb7a448202c66dacda9199
2026-07-24-web-gui-browser-e2e-lane.zh.md: 7c9b97da1810a81a0b7574c50b819892f9686695

View File

@@ -6,29 +6,29 @@ Status: implemented
## 问题
Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bundle → HTTP 单次 RPC + 两条 SSEServer-Sent Events流 → `toFetchHandler`/apiproxy → host 端的 agent loop智能体循环、工具与 JSONL 持久化——却没有任何测试无密钥且确定性地检验这条链。[GUI 测试体系](../process/2026-07-20-gui-testing-system.md)覆盖第 1 层Node 中的协议同构)、第 2 层(对象层状态机)与第 3 层冒烟测试,但无密钥冒烟驱动的是 `FixtureApiClient`——没有 host、没有 wire、没有 agent loop——而全链路冒烟需要 `DEEPSEEK_API_KEY` 和真实模型,因此不确定、在无密钥 CI 中自行跳过。[docs/testing.md](../../../../docs/testing.md) 的快照哲学——带密钥录制一次、永久无密钥回放、格式变动时刷新——已覆盖 ACPAgent Client Protocol、headless `stream-json` 与 TUI 三个文本记录(transcript表面web 表面是唯一没有这层保障的组装形态。而缺口恰恰是两起已实证 GUI P0 藏身之处fixture测试前置数据客户端短路掉的 wire 承载链。
Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bundle → HTTP 单次 RPC + 两条 SSEServer-Sent Events流 → `toFetchHandler`/apiproxy → host 端的 agent loop智能体循环、工具与 JSONL 持久化——却没有任何测试无密钥且确定性地检验这条链。[GUI 测试体系](../process/2026-07-20-gui-testing-system.md)覆盖第 1 层Node 中的协议同构)、第 2 层(对象层状态机)与第 3 层冒烟测试,但无密钥冒烟驱动的是 `FixtureApiClient`——没有 host、没有 wire、没有 agent loop——而全链路冒烟需要 `DEEPSEEK_API_KEY` 和真实模型,因此不确定、在无密钥 CI 中自行跳过。[docs/testing.md](../../../../docs/testing.md) 的快照哲学——带密钥录制一次、永久无密钥回放、格式变动时刷新——已覆盖 ACPAgent Client Protocol、headless `stream-json` 与 TUI 三个 transcript(文本记录表面web 表面是唯一没有这层保障的组装形态。而缺口恰恰是两起已实证 GUI P0 藏身之处fixture测试前置数据客户端短路掉的 wire 承载链。
## 决策
`pnpm run test:web` 携带 `apps/web/tests/` 下的无密钥、确定性浏览器 e2e 车道:录制的会话日志 fixture 经 `@deepseek-ai/dsh-llm-replay` 对真实进程内 web 组合回放;用户可见状态使用规范化的 aria 预期输出,持久世界状态则使用进程内断言。配套的产品约包括 `dsh-llm-replay` 的节奏控制、消费检查与已校验的索引式覆写 patch跨包的 `dsh-llm` 失败通过自有数据属性保留经校验的提供方信息;已交付的 web 组合挂载 `llm-retry`,以处理瞬态模型失败。
`pnpm run test:web` 携带 `apps/web/tests/` 下的无密钥、确定性浏览器 e2e 车道:录制的会话日志 fixture 经 `@deepseek-ai/dsh-llm-replay` 对真实进程内 web 组合回放;用户可见状态使用规范化的 aria 预期输出,持久世界状态则使用进程内断言。配套的产品约包括 `dsh-llm-replay` 的节奏控制、消费检查与已校验的索引式覆写 patch跨包的 `dsh-llm` 失败通过自有数据属性保留经校验的提供方信息;已交付的 web 组合挂载 `llm-retry`,以处理瞬态模型失败。
### Scaffold`apps/web/tests/scaffold.ts`
一个普通的共享 fixture 模块([测试政策认可的形态](../../../../docs/testing.md)),不是包:值得门禁把守的逻辑——回放推导、会话解析、日志脱敏、持久化——都在已受门禁的包 `dsh-llm-replay``dsh-acp-snapshot``dsh-session-persistence-jsonl` 中;剩下的只是启动接线和浏览器胶水,而驱动 chromium 的源码在无浏览器的覆盖率 runner 上无法诚实保持逐文件 100% 覆盖率。
`launchWebScaffold()` 通过 vendored Loader 的 include 机制,从交付的 `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml` 启动真实 web 组合——与 `AppCLIEntry``dsh web` 驱动的是同一棵树、同一套机制。差异全部经 include patch 覆盖在这棵树上,即 ACP `cordis.snapshot.yml` 模式的进程内表达:临时 `persistenceRoot`;每个主机级 `skill-local` 根目录(`dshHome``agentsHome``bundledSkillDir`)都钉在临时工作区下并禁用监听,因为环境 skill技能目录是模型可见输入禁用 `workspace-context`(录制的 fixture 不得嵌入本仓库的 AGENTS.md禁用 `session-title-llm`其发后不管的标题调用会与循环争抢会话的回放游标webserver 行钉到端口 0 加已构建 dist无密钥模式下禁用 `llm-deepseek`。patch 的 id 一旦不再匹配任何行boot 扫描会大声失败而不是漂移。boot 在临时工作区 `chdir` 下运行,使 api-gateway 的 `process.cwd()` 会话默认值、工具 cwd 与 fixture 一致;`dsh web` bin 自身的胶水argv、profile json、AppCLIEntry仍由 `smoke-real.e2e.ts` 中的无密钥 CLI 冒烟把守。初始化回滚和正常关闭都会先对 Cordis 树执行 dispose资源释放再删除 scaffold 持有的两个临时根目录;每项清理都会独立尝试,并会报告清理失败而不掩盖初始化失败。
`launchWebScaffold()` 通过 vendored Loader 的 include 机制,从交付的 `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml` 启动真实 web 组合——与 `AppCLIEntry``dsh web` 驱动的是同一棵树、同一套机制。差异全部经 include patch 覆盖在这棵树上,即 ACP `cordis.snapshot.yml` 模式的进程内表达:临时 `persistenceRoot`;每个主机级 `skill-local` 根目录(`dshHome``agentsHome``bundledSkillDir`)都钉在临时工作区下并禁用监听,因为环境 skill技能目录是模型可见输入禁用 `workspace-context`(录制的 fixture 不得嵌入本仓库的 AGENTS.md禁用 `session-title-llm`其发后不管的标题调用会与循环争抢会话的回放游标webserver 行钉到端口 0 加已构建 dist无密钥模式下禁用 `llm-deepseek`。patch 的 id 一旦不再匹配任何行boot 扫描会大声失败而不是漂移。boot 在临时工作区 `chdir` 下运行,使 api-gateway 的 `process.cwd()` 会话默认值、工具 cwd 与 fixture 一致;`dsh web` bin 自身的胶水argv、profile json、AppCLIEntry仍由 `smoke-real.e2e.ts` 中的无密钥 CLI(命令行界面)冒烟把守。初始化回滚和正常关闭都会先对 Cordis 树执行 dispose资源释放再删除 scaffold 持有的两个临时根目录;每项清理都会独立尝试,并会报告清理失败而不掩盖初始化失败。
无密钥的模型替换 = 禁用适配器行的 patch 加 `installLlmReplay` 在停稳的根 ctx 上以提供方目录providers-catalog模式填充开放的 seam——绝不用 catch-all适配器行被禁用后不存在任何适配器catch-all 会让 `resolveModelInfo` 无路由可走,`compact-basic` 的步压力检查将步步告警,而不是被可证明地闲置(发布的 128k `contextWindow` 使该路径对小 fixture 保持闲置)。选择直接安装而非插入回放插件行是刻意的:直接安装返回收尾消费检查所需的 `ReplayHandle`。没有 fixture 的场景让 seam 保持空置,任何离群的流式调用都会以 NO_ADAPTER 大声失败。
无密钥的模型替换 = 禁用适配器行的 patch 加 `installLlmReplay` 在停稳的根 ctx 上以提供方目录providers-catalog模式填充开放的 seam——绝不用 catch-all适配器行被禁用后不存在任何适配器catch-all 会让 `resolveModelInfo` 无路由可走,`compact-basic` 的步骤后的压力检查会在每个步骤都告警,而不是被可证明地闲置(发布的 128k `contextWindow` 使该路径对小 fixture 保持闲置)。选择直接安装而非插入回放插件行是刻意的:直接安装返回收尾消费检查所需的 `ReplayHandle`。没有 fixture 的场景让 seam 保持空置,任何离群的流式调用都会以 NO_ADAPTER 大声失败。
`seedSession()` 通过真实持久化 API 播种冷会话——一次性 `Context` 挂载 `SessionStore` + `SessionPersistenceJsonl` 指向 host 的根目录,`create()` + `append()`,一次 `utimes` 回拨保证侧栏顺序确定(`semantic-checkpoint.snapshot.ts` 先例——绝不裸写文件因此播种器对桶哈希、文件名编码、压缩一无所知host 的 zstd 默认值也无需任何启动开关。种子在播种时即校验(可解析、以 `turn/end` 结尾——未闭合的最终轮次会被恢复resume的崩溃修复改写)。
`seedSession()` 通过真实持久化 API 播种冷会话——一次性 `Context` 挂载 `SessionStore` + `SessionPersistenceJsonl` 指向 host 的根目录,`create()` + `append()`,一次 `utimes` 回拨保证侧栏顺序确定(`semantic-checkpoint.snapshot.ts` 先例——绝不裸写文件因此播种器对桶哈希、文件名编码、压缩一无所知host 的 zstd 默认值也无需任何启动开关。种子在播种时即校验(可解析、以 `turn/end` 结尾——未闭合的最终轮次会被恢复的崩溃修复改写)。
### 确定性规则
回放模式下浏览器断言的屏障栈按序1host 侧 `await agent.whenIdle()` 加超时,以进程内 `turn/end` 为锚——空闲翻转发生在持久化落盘之后一次等待同时覆盖轮次完成与持久性2浏览器安定轮询流式输出节点已卸载、最终文本可见。录制模式下日志采收在 `whenIdle()` 之后、scaffold 释放之前进行,此时运行中的会话仍然可用。单独监听进程内 `turn/end` 是错误屏障(它先于 SSE 帧到达浏览器、先于 fsync 触发禁止轮询持久化文件来充当轮次完成或持久性屏障NFS 上慢,且被 `whenIdle` 取代),但工具控制的临时就绪标记可以仅作为该完成屏障之前的交互门控进行轮询;`networkidle` 被彻底禁止SSE 流保持打开时它永不解析)。导航断言会在页面加载前同时监听 `session.list``workspace.list` 的初始响应,随后等待播种数据投影到 DOM仅凭 shell 已挂载不能判定就绪,因为较晚完成的 bootstrap 可能替换受控状态。
回放模式下浏览器断言的屏障栈按序1host 侧 `await agent.whenIdle()` 加超时,以进程内 `turn/end` 为锚——空闲翻转发生在持久化落盘之后一次等待同时覆盖轮次完成与持久性2浏览器安定轮询流式输出节点已卸载、最终文本可见。录制模式下日志采收在 `whenIdle()` 之后、scaffold 释放之前进行,此时运行中的会话仍然可用。单独监听进程内 `turn/end` 是错误屏障(它先于 SSE 帧到达浏览器、先于 fsync 触发禁止轮询持久化文件来充当轮次完成或持久性屏障NFS 上慢,且被 `whenIdle` 取代),但工具控制的临时就绪标记可以仅作为该完成屏障之前的交互门控进行轮询;`networkidle` 被彻底禁止SSE 流保持打开时永远不会达到该状态)。导航断言会在页面加载前同时监听 `session.list``workspace.list` 的初始响应,随后等待播种数据投影到 DOM仅凭 shell 已挂载不能判定就绪,因为较晚完成的 bootstrap 可能替换受控状态。
不做单次瞬态 DOM 断言:从回放产出到 React 提交的每一跳都可能合并分片,采样 `[data-streaming]` 天然就是竞态。流式输出的增量性由持久化的 `assistant/chunk` 事件断言(模型可见 ⟺ 已记录,使日志成为权威证据)。`dsh-llm-replay` 的可选 `paceMs`(默认缺省 = 突发)只是让浏览器观察到真正增量 SSE 的真实感旋钮;正确性绝不依赖它,且节奏等待期间中止会即时取消。
每个场景都会因任何 pageerror 或客户端的连接丢失/间隙修复控制台警告而失败:否则重连机制加历史重同步会把一条死掉的 SSE 通路自愈掉,套件反而认证了坏 wire。Scaffold 的 `close()` 调用 `ReplayHandle.assertConsumed()` 收尾检查(每个已录脚本都被绑定、每个游标都耗尽),把静默的少放与错绑变成清晰诊断。车道不设 vitest 重试;每文件一个 chromium、每场景一个新 context、每场景一个 host视口固定交互选择器锚定 role、`data-*` 属性和可见文本,而 frame 与会话区采集则使用既有的 CSS 模块局部类名锚点。常规场景在客户端启动前设置 `dsh.locale=en`,使本地化的 role 定位器和预期输出统一采用明确指定的语言;断言中文文案的场景则不预设该存储项,改为开启 `zh-CN` 浏览器,因为客户端的初始 locale 由 `navigator` 推导([由浏览器推导初始 locale](../feature/2026-07-31-browser-derived-initial-locale.md)),而 `settings-chrome.e2e.ts` 还额外覆盖双向切换与英文浏览器默认态。
每个场景都会因任何 pageerror 或客户端的连接丢失/间隙修复控制台警告而失败:否则重连机制加历史重同步会把一条死掉的 SSE 通路自愈掉,套件反而认证了坏 wire。Scaffold 的 `close()` 调用 `ReplayHandle.assertConsumed()` 收尾检查(每个已录脚本都被绑定、每个游标都耗尽),把静默的消费不足与错绑变成清晰诊断。车道不设 vitest 重试;每文件一个 chromium、每场景一个新 context、每场景一个 host视口固定交互选择器锚定 role、`data-*` 属性和可见文本,而 frame 与会话区采集则使用既有的 CSS 模块局部类名锚点。常规场景在客户端启动前设置 `dsh.locale=en`,使本地化的 role 定位器和预期输出统一采用明确指定的语言;断言中文文案的场景则不预设该存储项,改为开启 `zh-CN` 浏览器,因为客户端的初始 locale 由 `navigator` 推导([由浏览器推导初始 locale](../feature/2026-07-31-browser-derived-initial-locale.md)),而 `settings-chrome.e2e.ts` 还额外覆盖双向切换与英文浏览器默认态。
### 预期输出
@@ -40,19 +40,19 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
`DSH_SNAPSHOT` 选择 replay默认无密钥、record带密钥或 refresh无密钥。发起提示的 spec 将所有模式共用的驱动步骤与仅供 replay/refresh 使用的断言分开record 模式驱动真实输入框,采收内存中的会话 header 与事件,脱敏请求头,并 token 化当次运行的会话、cwd 与 RPC 标识。随后一次无密钥 refresh 重新生成 aria 预期输出。每条提示词都会与 fixture 中录制的 `user/message` 核对;每个场景目录都采用封闭清单,其中每个 JSONL 都是脱敏不动点。Web fixture 全部脱敏请求头且不钉任何 header 类别;见「暂缓」。
### 覆盖
### 覆盖约
该车道覆盖三类行为。实时轮次场景钉住普通工具执行、取消、不可重试失败、瞬态重试、常驻提问与轮次中途 steering同步依赖持久事件、`whenIdle()` 或显式回放标记,而不使用延时。冷历史场景通过真实持久化 API 播种在不调用模型的情况下覆盖历史渲染、侧栏搜索、Trajectory 与 Waterfall 视图及工具详情。浏览器生命周期场景覆盖首次发送时物化工作区、重新加载恢复、布局重置、主题与语言偏好,以及工作区的创建、重命名和视图操作。每类场景都断言浏览器表面和权威的 host 状态;离群的模型调用或未耗尽的 fixture 会使拆卸失败。必需车道还包含一份合成的 88 Chat 滚动约,其中混合了换行 Markdown、围栏代码以及成对的 bash 调用/结果。真实 wheel、输入框、工具、tab、会话与 viewport 交互会在并发历史前插加带节奏流式输出、贴底/离底流式输出、工具 disclosure 离屏循环、扩展历史后的视图/会话重新挂载、宽度重排、贴底后立即重新挂载、输入框尺寸变化以及 textarea wheel 链场景中,断言一个具名已结算行相对 transcript scrollport 的顶部位置和到真实底部的距离;真实键盘翻页与触摸式惯性滑动模拟额外钉住不依赖 wheel 的贴底跟随所有权([读者滚动归因笔记](../bug-fix/2026-08-06-reader-scroll-attribution-observed-top-ledger.md));它刻意不钉 DOM 基数或绝对 `scrollTop`,因此同一约可以验收虚拟化实现。另一份基于同一 fixture 的交互约钉住异构行顺序、相邻工具 disclosure 的独立状态、用户消息剪贴板内容的精确值、以轮次为边界的消息 fork、源会话/子会话隔离以及子会话中的一次真实追问轮次wheel 输入只用于导航到语义目标,不承载几何预期。一份简短的实时历史约从空白工作区开始,连续驱动输入框轮次,其中包括真实的 bash 调用/结果轮次和一段带节奏的长篇最终响应;它钉住单一会话身份、每事件精确归属、浏览器回显唯一性与输入框恢复,不设置时间阈值。
该车道覆盖三类行为。实时轮次场景钉住普通工具执行、取消、不可重试失败、瞬态重试、常驻提问与轮次中途 steering(中途引导);同步依赖持久事件、`whenIdle()` 或显式回放标记,而不使用延时。冷历史场景通过真实持久化 API 播种在不调用模型的情况下覆盖历史渲染、侧栏搜索、Trajectory 与 waterfall(瀑布式事件)视图及工具详情。浏览器生命周期场景覆盖首次发送时物化工作区、重新加载恢复、布局重置、主题与语言偏好,以及工作区的创建、重命名和视图操作。每类场景都断言浏览器表面和权威的 host 状态;离群的模型调用或未耗尽的 fixture 会使拆卸失败。必需车道还包含一份合成的 88 个轮次的 Chat 滚动约,其中混合了换行 Markdown、围栏代码以及成对的 bash 调用/结果。真实 wheel、输入框、工具、tab、会话与 viewport 交互会在并发历史前插加带节奏流式输出、贴底/离底流式输出、工具 disclosure 离屏循环、扩展历史后的视图/会话重新挂载、宽度重排、贴底后立即重新挂载、输入框尺寸变化以及 textarea wheel 链场景中,断言一个具名已结算行相对 transcript scrollport 的顶部位置和到真实底部的距离;真实键盘翻页与触摸式惯性滑动模拟额外钉住不依赖 wheel 的贴底跟随所有权([读者滚动归因笔记](../bug-fix/2026-08-06-reader-scroll-attribution-observed-top-ledger.md));它刻意不钉 DOM 基数或绝对 `scrollTop`,因此同一约可以验收虚拟化实现。另一份基于同一 fixture 的交互约钉住异构行顺序、相邻工具 disclosure 的独立状态、用户消息剪贴板内容的精确值、以轮次为边界的消息 fork、源会话/子会话隔离以及子会话中的一次真实追问轮次wheel 输入只用于导航到语义目标,不承载几何预期。一份简短的实时历史约从空白工作区开始,连续驱动输入框轮次,其中包括真实的 bash 调用/结果轮次和一段带节奏的长篇最终响应;它钉住单一会话身份、每个轮次的事件精确归属、浏览器回显唯一性与输入框恢复,不设置时间阈值。
### CI 立场
根据[浏览器快照 CI 决策](2026-07-30-web-browser-snapshot-ci-gate.md),该车道是 Linux 拉取请求必需的只比较门禁。`node 24 / snapshots and artifacts` 消费方任务在[消费方独立构建](../process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,安装锁文件选定的 Chromium恢复以操作系统和锁文件为键的缓存并用 `DSH_SNAPSHOT=replay` 运行该车道。这是有意的平面切分host 与 spec 使用 [tsx 源码启动](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md),浏览器则消费 `apps/web/dist` 和包的 `lib/client.js` 产物,因此门禁依赖 `built-package-invariants` 提供这些客户端产物。托管和自托管的默认分支 Linux 串行任务运行同一门禁;托管任务生成供 PR 消费的浏览器缓存,持久化自托管池则不需要托管侧缓存。CI 从不录制或刷新预期输出。场景仍面向 POSIX并继续置于 Windows 和 macOS 矩阵之外。
根据[浏览器快照 CI 决策](2026-07-30-web-browser-snapshot-ci-gate.md),该车道是 Linux PRPull Request必需的只比较门禁。`node 24 / snapshots and artifacts` 消费方任务在[消费方独立构建](../process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,安装锁文件选定的 Chromium恢复以操作系统和锁文件为键的缓存并用 `DSH_SNAPSHOT=replay` 运行该车道。这是有意的平面切分host 与 spec 使用 [tsx 源码启动约](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md),浏览器则消费 `apps/web/dist` 和包的 `lib/client.js` 产物,因此门禁依赖 `built-package-invariants` 提供这些客户端产物。托管和自托管的默认分支 Linux 串行任务运行同一门禁;托管任务生成供 PR 消费的浏览器缓存,常驻自托管池则不需要托管侧缓存。CI 从不录制或刷新预期输出。场景仍面向 POSIX并继续置于 Windows 和 macOS 矩阵之外。
高基数性能诊断使用单独按需启用的 `apps/web/tests/**/*.perf.ts` 清单,并且只由 `vitest.web.perf.config.ts` 选中。`complex-history.perf.ts` 的隔离用例复用真实 scaffold工作区用例播种 1,000 个紧凑会话以及一份包含 500 次工具调用的 500 轮次历史,在 Chat 中穷尽并重新挂载该历史,并报告 Chromium 主线程、DOM、监听器、堆内存、分页、搜索和 Trajectory 测量结果。两个续聊用例播种同一份长历史,但比较默认的 24 轮次 Chat 窗口与展开全部 500 轮次的状态然后各自通过真实输入框、agent loop、SSE wire、工具和持久化继续进行 8 个相同轮次;其中两执行真实 `bash` 调用并断言其持久化结果,最后一则填入一条包含 8,232 个字符的混合语言提示词,并回放 120 个带节奏的文本增量。一个单独的 soak 用例从空白会话开始,通过真实输入框连续驱动 100 ,每 10 执行一次 `bash` 调用并产生结果,每 10 强制执行一次 GC并报告每 10 的延迟窗口及保留的浏览器状态。随后它通过受信任的浏览器点击提交第 101 个纯文本轮次,并使用浏览器时钟分别测量发送到 transcript DOM 和发送到绘制后的延迟,排除输入框的草稿镜像,并与完整轮次完成时间分开。逐轮诊断涵盖输入框填入、点击到用户消息回显、点击到首个分片、完成、浏览器变更、持久化分片和工具事件;合成回放模型拥有足够的上下文容量,可使 fixture 基数保持稳定而不会因压缩compaction消耗脚本化调用。结构性断言钉住预期的负载、流和工具形状但时间仍不设阈值因为机器速度不属于正确性约。必需的 `vitest.web.config.ts` 清单仍仅限 `*.e2e.ts``*.snapshot.ts`,因此 `test:web:built` 及其 CI 门禁都不会收集性能用例。
高基数性能诊断使用单独按需启用的 `apps/web/tests/**/*.perf.ts` 清单,并且只由 `vitest.web.perf.config.ts` 选中。`complex-history.perf.ts` 的隔离用例复用真实 scaffold工作区用例播种 1,000 个紧凑会话以及一份包含 500 次工具调用的 500 轮次历史,在 Chat 中穷尽并重新挂载该历史,并报告 Chromium 主线程、DOM、监听器、堆内存、分页、搜索和 Trajectory 测量结果。两个续聊用例播种同一份长历史,但比较默认的 24 轮次 Chat 窗口与展开全部 500 轮次的状态然后各自通过真实输入框、agent loop、SSE wire、工具和持久化继续进行 8 个相同轮次;其中两个轮次执行真实 `bash` 调用并断言其持久化结果,最后一个轮次则填入一条包含 8,232 个字符的混合语言提示词,并回放 120 个带节奏的文本增量。一个单独的 soak 用例从空白会话开始,通过真实输入框连续驱动 100 个轮次,每 10 个轮次执行一次 `bash` 调用并产生结果,每 10 个轮次强制执行一次 GC并报告每 10 个轮次的延迟窗口及保留的浏览器状态。随后它通过受信任的浏览器点击提交第 101 个纯文本轮次,并使用浏览器时钟分别测量发送到 transcript DOM 和发送到绘制后的延迟,排除输入框的草稿镜像,并与完整轮次完成时间分开。各轮次诊断涵盖输入框填入、点击到用户消息回显、点击到首个分片、完成、浏览器变更、持久化分片和工具事件;合成回放模型拥有足够的上下文容量,可使 fixture 基数保持稳定而不会因压缩compaction消耗脚本化调用。结构性断言钉住预期的负载、流和工具形状但时间仍不设阈值因为机器速度不属于正确性约。必需的 `vitest.web.config.ts` 清单仍仅限 `*.e2e.ts``*.snapshot.ts`,因此 `test:web:built` 及其 CI 门禁都不会收集性能用例。
## 业界先例
调研了 AI 聊天/agent web UI 与 mock 层LibreChat、vercel/ai-chatbot + AI SDK、lobe-chat、open-webui、OpenHands、Chainlit、continue、cline、langfuse、gradio/streamlitPlaywright HAR/route、MSW、Polly/nock、WireMock、aimock。自有后端的应用的主流成熟架构是真实后端 seam 后放一个进程内伪造/回放模型下游全部真实LibreChat 的 `LIBRECHAT_TEST_RUN_HOOK` 伪模型ai-chatbot 的 `MockLanguageModelV3` + `simulateReadableStream`continue 的脚本化 mock 提供方类)——这正是 `dsh-llm-replay` 已然所是。浏览器层 SSE 拦截无法检验增量渲染(`route.fulfill` 一次性交付整个响应体playwright#33564),且服务端 SSE 栈完全失测,因此各项目只把它用于边缘用例。分片节奏作为 fixture 参数反复出现LibreChat 默认 10ms 附慢速档ai-chatbot 500msCI 里的真实模型会腐烂open-webui 的套件长出 120 秒超时先被禁用后被删除会话在持久化层以受控时间戳播种LibreChat 直插回拨时间的 Mongo 文档langfuse 播种其数据库)。没有任何被调研项目为 UI 测试把录制的 agent 事件日志经真实后端回放——最接近的是提供方层录制 fixtureaimock与前端层 socket 历史发射OpenHands MSW——因此会话日志即 fixture 的设计沿着本仓库「模型可见 ⟺ 已记录」不变式所指的方向比业界先例多走了一步。
调研了 AI(人工智能)聊天/agent web UI 与 mock 层LibreChat、vercel/ai-chatbot + AI SDK、lobe-chat、open-webui、OpenHands、Chainlit、continue、cline、langfuse、gradio/streamlitPlaywright HAR/route、MSW、Polly/nock、WireMock、aimock。自有后端的应用的主流成熟架构是真实后端 seam 后放一个进程内伪造/回放模型下游全部真实LibreChat 的 `LIBRECHAT_TEST_RUN_HOOK` 伪模型ai-chatbot 的 `MockLanguageModelV3` + `simulateReadableStream`continue 的脚本化 mock 提供方类)——这正是 `dsh-llm-replay` 已然所是。浏览器层 SSE 拦截无法检验增量渲染(`route.fulfill` 一次性交付整个响应体playwright#33564),且服务端 SSE 栈完全失测,因此各项目只把它用于边缘用例。分片节奏作为 fixture 参数反复出现LibreChat 默认 10ms 附慢速档ai-chatbot 500msCI 里的真实模型会腐烂open-webui 的套件长出 120 秒超时先被禁用后被删除会话在持久化层以受控时间戳播种LibreChat 直插回拨时间的 Mongo 文档langfuse 播种其数据库)。没有任何被调研项目为 UI 测试把录制的 agent 事件日志经真实后端回放——最接近的是提供方层录制 fixtureaimock与前端层 socket 历史发射OpenHands MSW——因此会话日志即 fixture 的设计沿着本仓库「模型可见 ⟺ 已记录」不变式所指的方向比业界先例多走了一步。
## 曾考虑的替代方案
@@ -62,15 +62,15 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
**扩展 `?fixture` 客户端。** 已否决:分层纪律——`FixtureApiClient` 的存在意义就是脱离服务器测试客户端 shellclient API seam 以下按构造即失测。
**用占位 `DEEPSEEK_API_KEY` + 回放拦截替代禁用适配器行。** 尽管零组合改动且树内有两处先例仍被否决:它谎言满足 `llm-deepseek`快速失败密钥检查还留下一个挂载却被拦截的死适配器禁用行ACP overlay 的同款做法)是诚实的无密钥,并在最早可解析点快速失败。
**用占位 `DEEPSEEK_API_KEY` + 回放拦截替代禁用适配器行。** 尽管零组合改动且树内有两处先例仍被否决:它谎言绕过 `llm-deepseek` 会明确报错的密钥检查还留下一个挂载却被拦截的死适配器禁用行ACP overlay 的同款做法)才是真正的无密钥方案,并在最早可判定点明确失败。
**`packages/support/web-snapshot` 包 + `defineWebSnapshotSuite` 工厂。** 已否决:驱动 chromium 的源码在无浏览器的覆盖率 runner 上无法诚实保持逐文件 100%,且除受门禁的包已导出的辅助工具与本地 scaffold 外,这些场景专用交互尚未形成稳定的无浏览器约。出现第二个 web 形态消费方,或被证实重复的生命周期代码确立该约后,再重新考虑。
**`packages/support/web-snapshot` 包 + `defineWebSnapshotSuite` 工厂。** 已否决:驱动 chromium 的源码在无浏览器的覆盖率 runner 上无法诚实保持逐文件 100%,且除受门禁的包已导出的辅助工具与本地 scaffold 外,这些场景专用交互尚未形成稳定的无浏览器约。出现第二个 web 形态消费方,或被证实重复的生命周期代码确立该约后,再重新考虑。
**第二份提交的规范化会话日志预期输出。** 已否决:日志表面已由 ACP/headless/TUI 套件经同一循环与持久化钉住;在此只会翻倍刷新成本并重复测试下层。内联在根上下文事件上的世界状态断言保住了验证世界的义务。
**以 `DSH_SNAPSHOT` 回放分支拉起 `dsh web` bin。** 已否决:它需要在交付的 CLI 中增加测试专用回放分支和环境变量管道。进程内 scaffold 已加载同一份 `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml`;只剩 argv、profile JSON 和 `AppCLIEntry` 胶水不在其覆盖范围内,而这些路径已由无密钥 CLI 冒烟覆盖。
**为可测试性改 wire 协议。** 已否决:约已有第一等的无密钥同构 seam`InProcessApiClient(toFetchHandler(api))`),逐事件不合批的 SSE 恰是回放在浏览器中可观测的原因,测试一条不再交付的 wire 会颠倒该层的存在意义。
**为可测试性改 wire 协议。** 已否决:约已有第一等的无密钥同构 seam`InProcessApiClient(toFetchHandler(api))`),逐事件不合批的 SSE 恰是回放在浏览器中可观测的原因,测试一条不再交付的 wire 会颠倒该层的存在意义。
**以真实模型浏览器测试充当无密钥车道。** 已否决按构造即不确定被调研的前车之鉴open-webui长出无界超时后被删除。带密钥的 W5 冒烟仍是真实模型侧的补充。
@@ -78,18 +78,18 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
**客户端 `data-dsh-busy` 安定信号。** 暂缓host 侧 `whenIdle` 屏障配合稳定 DOM 轮询,足以覆盖当前场景。第一次安定轮询抖动,或必要状态在 DOM 中不可观察时,再重新考虑。
## Testing
## 测试
`pnpm run test:web` 构建并无密钥运行该车道;`test:web:built` 基于现有构建产物运行。`pnpm run test:web:perf` 构建并运行手动性能清单;`test:web:perf:built` 复用现有产物。`DSH_SNAPSHOT=record pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/<spec>` 对真实模型录制一个发起提示的场景,`DSH_SNAPSHOT=refresh pnpm run test:web` 则无密钥重写 aria 预期输出。CI 显式选择回放模式。live-interactions AUTH 场景会把不可重试的终态失败钉为 Chat 内联状态,其中携带适合展示的消息与错误码,并验证提供方回显的凭据片段不会出现在 Chat 或 Trajectory 中;该场景同时覆盖输入框恢复与 `turn/end` 错误。scaffold 环境隔离场景会在全部 3 个环境 skill 根目录中分别填入不同条目,并要求这些条目都不得进入组装后的目录。`dsh-llm-replay` 单元覆盖率钉住节奏控制、取消、消费诊断、sidecar 校验、按索引替换与唯一的追加位置。
`pnpm run test:web` 构建并无密钥运行该车道;`test:web:built` 基于现有构建产物运行。`pnpm run test:web:perf` 构建并运行手动性能清单;`test:web:perf:built` 复用现有产物。`DSH_SNAPSHOT=record pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/<spec>` 对真实模型录制一个发起提示的场景,`DSH_SNAPSHOT=refresh pnpm run test:web` 则无密钥重写 aria 预期输出。CI 显式选择回放模式。live-interactions AUTH 场景会把不可重试的终态失败钉为 Chat 内联状态,其中携带适合展示的消息与错误码,并验证提供方回显的凭据片段不会出现在 Chat 或 Trajectory 中;该场景同时覆盖输入框恢复与 `turn/end` 错误。scaffold 环境隔离场景会在全部 3 个环境 skill 根目录中分别填入不同条目,并要求这些条目都不得进入组装后的目录。`dsh-llm-replay` 单元覆盖率钉住节奏控制、取消、消费诊断、伴随文件校验、按索引替换与唯一的追加位置。
## 暂缓
- **Web 头类别钉住**web fixture 处处 token 化 `{{system}}`/`{{tools}}`,没有场景钉住 web 组合的提示词/工具 schema`TODO(web-header-pin)`——scaffold 的 `recordFixture` JSDoc 有标记)。沿用 TUI 处处脱敏先例;当 web 组装的请求头与其镜像的 repl 组合进一步分叉时重审。
- **Web 头类别钉住**web fixture 处处 token 化 `{{system}}`/`{{tools}}`,没有场景钉住 web 组合的提示词/工具 schema`TODO(web-header-pin)`——scaffold 的 `recordFixture` JSDoc 有标记)。沿用 TUI 处处脱敏先例;当 web 组装的请求头与其镜像的 repl 组合发生分叉时重审。
- **恢复后追问场景**:真实 wire 上的历史/实时缝合路径;当该代码变更或回归时作为独立场景补充。
- **输入框 steering 手势**:输入在运行期间锁定(只能停止或等待),因此 steering 场景从页面走 wire 做 steer`TODO(web-steer-composer)` 待产品长出真实的输入框手势后,把驱动步骤升级为该手势。
- **拖拽会话重排**`workspace.insertSessionBefore` 尚无浏览器场景;它需要在同一个工作区里物化两个会话,并合成 HTML5 拖拽事件。当该表面变更或回归时再补充。无行为的会话 Rename/Fork/Delete 和工作区 Delete 菜单行待获得行为后再补充场景。
- **长历史 Chat 到 Trajectory 的 Inspect**:独立的检查数据源会在视图打开后穷尽历史,而所选记录由一个派生的表格索引定位;随着较早页面前插,该索引可能移动。短历史 Inspect 仍有覆盖;在选中项具有稳定的语义身份之前,长历史交互约不包含这项交接。
- **长历史 Chat 到 Trajectory 的 Inspect**:独立的检查数据源会在视图打开后穷尽历史,而所选记录由一个派生的表格索引定位;随着较早页面前插,该索引可能移动。短历史 Inspect 仍有覆盖;在选中项具有稳定的语义身份之前,长历史交互约不包含这项交接。
## 后果
Web 表面获得了录制一次/永久回放的层级:真实 chromium → SSE → apiproxy → 循环 → 工具 → 持久化的链路以约 10-30 秒无密钥运行重复运行结果确定fixture 由车道自身持有并可重录。接受的成本:每次有意的会话 UI 变更都以一次无密钥 `DSH_SNAPSHOT=refresh` 收尾(预期输出变动是受评审的 diff锚断言保住语义绿色aria 格式归 Playwright 所有——仓库唯一不受自己控制的提交快照格式——因此 playwright 版本升级必须是刻意的升级加刷新提交(依赖在 `apps/web/package.json` 中浮动为 `^1.49.0`;若变动伤人则改为精确锁定);回放的首次调用顺序绑定把每个场景限制为至多一个发起提示的会话,消费断言是绊线;`compact-basic` 与会话共享回放游标,仅在布的 128k 目录窗口下保持闲置;必需的消费方任务承担 Chromium 供给与一次浏览器运行的成本,使改动组装后 UI 的 PRPull Request持有相应的预期输出 diff。按需启用的性能车道保留了可重复的诊断工作负载又不会向 CI 添加受 host 差异影响的时长或内存预期;在仓库拥有经校准的基准测试环境之前,性能回归仍是需要人工解读的信号。
Web 表面获得了录制一次/永久回放的层级:真实 chromium → SSE → apiproxy → 循环 → 工具 → 持久化的链路以约 10-30 秒无密钥运行重复运行结果确定fixture 由车道自身持有并可重录。接受的成本:每次有意的会话 UI 变更都以一次无密钥 `DSH_SNAPSHOT=refresh` 收尾(预期输出变动是受评审的 diff锚断言保住语义绿色aria 格式归 Playwright 所有——仓库唯一不受自己控制的提交快照格式——因此 playwright 版本升级必须是刻意的升级加刷新提交(依赖在 `apps/web/package.json` 中浮动为 `^1.49.0`;若变动伤人则改为精确锁定);回放的首次调用顺序绑定把每个场景限制为至多一个发起提示的会话,消费断言是绊线;`compact-basic` 与会话共享回放游标,仅在目录中公布的 128k 窗口下保持闲置;必需的消费方任务承担 Chromium 供给与一次浏览器运行的成本,使改动组装后 UI 的 PR 持有相应的预期输出 diff。按需启用的性能车道保留了可重复的诊断工作负载又不会向 CI 添加受 host 差异影响的时长或内存预期;在仓库拥有经校准的基准测试环境之前,性能回归仍是需要人工解读的信号。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-30-vitest-jsdom-webstorage-ownership.md
2026-07-30-vitest-jsdom-webstorage-ownership.md: 3956a7566fa1c79a767636bce9a19f16588126e2
2026-07-30-vitest-jsdom-webstorage-ownership.zh.md: 9080ee2762b74bf2efdaccd7a5905672001bc0e8
2026-07-30-vitest-jsdom-webstorage-ownership.zh.md: bd01ffdc68a45802ae403bdf44533d39126b021a

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
受支持的 Node 版本范围包含会预留进程级 `globalThis.localStorage` 的版本。未设置 `--localstorage-file`Node 26 将该属性暴露为 `undefined`Vitest 检测到这个预留键后,不会用 jsdom 的隔离 `Storage` 对象覆盖该属性。因此,组件测试套件尚未验证产品行为便会失败,而主要的 Node 24 覆盖率分支仍能通过,因为该运行时默认不会预留此键。
受支持的 Node 版本范围包含会预留进程级 `globalThis.localStorage` 的版本。未设置 `--localstorage-file`Node 26 将该属性暴露为 `undefined`Vitest 检测到这个预留键后,不会用 jsdom 的隔离 `Storage` 对象覆盖该属性。因此,组件测试套件尚未验证产品行为便会失败,而主要的 Node 24 覆盖率通道仍能通过,因为该运行时默认不会预留此键。
## 决策
@@ -23,4 +23,4 @@ Node 兼容性汇总任务会在每条声明支持的兼容版本线上运行专
## 后果
同一条 `pnpm test` 命令在有无内置 Web Storage 的 Node 版本上均可运行。测试 worker 被有意禁止使用 Node 的进程级 Web Storage未来若产品需要该 API必须使用独立且显式的测试配置而不能削弱 jsdom 隔离。兼容性分支只增加一个专项 Vitest 进程,无需在每个 Node 版本上重复整套单元测试。
同一条 `pnpm test` 命令在有无内置 Web Storage 的 Node 版本上均可运行。测试 worker 被有意禁止使用 Node 的进程级 Web Storage未来若产品需要该 API必须使用独立且显式的测试配置而不能削弱 jsdom 隔离。兼容性通道只增加一个专项 Vitest 进程,无需在每个 Node 版本上重复整套单元测试。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md
2026-07-30-web-browser-snapshot-ci-gate.md: 14402485034cd85ec5781477ce67481165d47e62
2026-07-30-web-browser-snapshot-ci-gate.zh.md: f214c524253d2ad8a43e8543dc65ddfcfd7c065c
2026-07-30-web-browser-snapshot-ci-gate.zh.md: 28a7ef9a7046516a853b3e18a44163c01d43a318

View File

@@ -14,22 +14,22 @@ Linux PR 的 `node 24 / snapshots and artifacts` 必须运行完整 Web 浏览
消费方 job 在[消费方独立构建](../process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,因此 `apps/web/dist` 和包的 `lib/` 目录会保留在其工作区中供浏览器套件使用。在托管运行器上CI 按锁文件中的 Playwright 版本安装 Chromium 及其系统依赖。在持久化故障切换 VM 上,镜像负责预装 Linux 系统软件包CI 只安装 Chromium避免每次运行都通过 `apt` 改动系统。托管的默认分支 Linux 串行 job 运行该套件并生成以操作系统和锁文件为键的浏览器缓存PR 恢复该缓存,使必需路径无需承担压缩和上传开销,并可在锁文件变化时按操作系统前缀回退。自托管热备运行相同的比较,但不执行托管缓存操作。
本地 `pnpm run test:web` 仍先构建再运行浏览器全集`test:web:built` 是已有构建产物的执行入口。开发者只在确认用户可见输出有意变化后显式运行 `DSH_SNAPSHOT=refresh pnpm run test:web`,评审每一处 expected diff再以 replay 模式复验不再写文件。
本地 `pnpm run test:web` 仍先构建再运行完整的浏览器套件`test:web:built` 是已有构建产物的执行入口。开发者只在确认用户可见输出有意变化后显式运行 `DSH_SNAPSHOT=refresh pnpm run test:web`,评审每一处预期输出 diff再以 replay 模式复验不再写文件。
对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX其他 PR job 不供给 Chromium。托管和自托管的默认分支 Linux 串行聚合作业也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器。PR 的 `all checks passed` 已依赖消费方 job因此浏览器比较失败会阻止合并无需新增 branch-protection check 名称。
对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX其他 PR job 不安装 Chromium。托管和自托管的默认分支 Linux 串行聚合作业也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器。PR 的 `all checks passed` 已依赖消费方 job因此浏览器比较失败会阻止合并无需新增 branch-protection check 名称。
一次自托管消费方运行中,`web-snapshot` 实测耗时 112.15 秒,完整消费方聚合实测耗时 114.97 秒。gate 调度器会在 `built-package-invariants` 成功后立即启动它,并发运行彼此独立的 gate因此既不需要专用 job 超时,也不需要手动制定 YAML 顺序规则。
## 曾考虑的替代方案
**继续只要求本地运行。** 已否决:执行依赖开发者记忆,正是旧 golden 跨 PR 漂移的原因,不能保证产生行为变化的 PR 自己携带 expected diff。
**继续只要求本地运行。** 已否决:执行依赖开发者记忆,正是旧 golden 跨 PR 漂移的原因,不能保证产生行为变化的 PR 自己携带预期输出 diff。
**让 CI 以 `refresh` 模式运行后检查工作树。** 已否决:写后比较把断言机制变成生成器,若工作树检查接线失效就会把回归更新成绿色replay 直接比较已有 golden失败面更小。
**让 CI 以 `refresh` 模式运行后检查工作树。** 已否决:写后比较把断言机制变成生成器,若工作树检查接入有误,就可能把回归变成能够通过的预期输出更新replay 直接比较已有 golden失败面更小。
**新建独立 browser job 并重新构建全仓。** 已否决:它会重复依赖安装和发布构建。现有 Linux 消费方 job 已负责该构建,并已被统一的 required verdict 聚合。
**用 jsdom 快照代替真实 Chromium。** 已否决jsdom 不覆盖浏览器、HTTP/SSE 承载及真实 client plugin bundle 组合;它保留为快速的下层反馈,不能替代 assembled browser chain
**用 jsdom 快照代替真实 Chromium。** 已否决jsdom 不覆盖浏览器、HTTP/SSE 承载及真实客户端插件包的组合;它仍可用于快速的下层反馈,不能替代组装后的浏览器链路
## 后果
每个 PR 都在合并前证明当前 Web 组装与所有已提交的浏览器 expected 一致,漏刷从“后续 PR 的无关变化”变成引入 PR 自己的失败。成本是消费方 job 需要供给 Chromium并串行运行一轮浏览器场景消费方独立构建与浏览器缓存避免重跑时重复构建和下载。门禁仍不声称跨平台浏览器一致性Playwright/Chromium 升级若改变 aria 格式,升级 PR 必须显式 refresh 并评审 churn。
每个 PR 都在合并前证明当前 Web 组装与所有已提交的浏览器预期输出一致,漏刷从“后续 PR 的无关变化”变成引入 PR 自己的失败。成本是消费方 job 需要安装 Chromium并串行运行一轮浏览器场景消费方独立构建与浏览器缓存避免重跑时重复构建和下载。门禁仍不声称跨平台浏览器一致性Playwright/Chromium 升级若改变 ARIA 格式,升级 PR 必须显式 refresh 并评审 churn。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md
2026-08-03-opt-in-reasoning-chunk-browser-stress.md: 70c200c7ade6ef995c68b68ddc21e4c85edf0da8
2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md: aa1d29bffd3dcba54925b43eab13ef4d9a4f7cff
2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md: 9fb4be27369d4489fc90a6151fcbd0861ef83f2c

View File

@@ -1,4 +1,4 @@
# Agent Note: 推理分片的逐帧累计发布与浏览器压力验证
# Agent Note: 推理reasoning分片的逐帧累计发布与浏览器压力验证
Status: implemented
@@ -12,15 +12,15 @@ Status: implemented
## 决策
`Session.acceptLiveEvent()` 立即追加每个原始事件,并同步更新 transcript、`PartialAccumulator` 及其他会话派生状态。可见的 `block-start``text-delta``reasoning-delta``tool-call-delta``block-end` 分片通过 `Notifier.markFrameDirty()` 发布:第一项变化调度一次 `requestAnimationFrame`,后续分片只继续更新累积器;帧回调从最新状态重建一个累计快照并通知订阅者一次。`usage``finish` 及未知的不可见分片保留在事件窗口中,但不触发无效的 React 通知。会话与历史检查共用同一可见分片分类。
`Session.acceptLiveEvent()` 立即追加每个原始事件,并同步更新 transcript(文本记录)`PartialAccumulator` 及其他会话派生状态。可见的 `block-start``text-delta``reasoning-delta``tool-call-delta``block-end` 分片通过 `Notifier.markFrameDirty()` 发布:第一项变化调度一次 `requestAnimationFrame`,后续分片只继续更新累积器;帧回调从最新状态重建一个累计快照并通知订阅者一次。`usage``finish` 及未知的不可见分片保留在事件窗口中,但不触发无效的 React 通知。会话与历史检查共用同一可见分片分类。
`Notifier` 用调度种类和代际标记管理待发布工作。普通结构事件继续通过 `markDirty()` 在微任务发布;如果定稿消息、工具事件或错误到达时仍有待执行的帧发布,微任务会取代它,旧帧回调因代际不匹配而失效。`notifyNow()` 同样使旧调度失效,以保留受控输入的同步回响。没有 `requestAnimationFrame` 的环境退回微任务合批。定稿事件可以跳过一次尚未显示的中间 partial但发布的定稿内容和原始事件序列保持完整。
`Notifier` 用调度种类和代际标记管理待发布工作。普通结构事件继续通过 `markDirty()` 在微任务发布;如果定稿消息、工具事件或错误到达时仍有待执行的帧发布,微任务会取代它,旧帧回调因代际不匹配而失效。`notifyNow()` 同样使旧调度失效,以保留受控输入的同步回响。没有 `requestAnimationFrame` 的环境退回微任务合批。定稿事件可以跳过一次尚未显示的中间 partial但发布的定稿内容和原始事件序列保持完整。
实时 Think 行对累计文本的横向跟尾属于纯视觉对齐,不需要在每次 React 提交中同步读取布局。组件内调度器将连续请求合并为每三帧一次,从最新 DOM 读取 `scrollWidth``clientWidth` 并将 `scrollLeft` 直接更新到最新位置;固定的视觉节奏让摘要变化可读,又不会积压浏览器平滑滚动动画。该节流只作用于 Think 的横向摘要,不延迟 Chat 正文滚动、历史 prepend 锚定或用户触发的 `scrollIntoView`
`pnpm run test:web:stress` 保留为无密钥、需显式启用的浏览器性能证据。确定性的 `?fixture` 会话以独立于绘制的节奏发出 100,000 个 `reasoning-delta`,结尾标记证明事件经过生产会话归并并到达实时 Think 行50 毫秒心跳和预先调度的 DOM 事件分别测量主线程停顿与交互延迟250 毫秒预算用于识别明显回归。`DSH_WEB_STRESS_HEADFUL=1` 允许开发者在可见浏览器中使用 Performance 面板分析同一场景。该压力车道是手动性能诊断与修复验收证据,不是默认 CI 门禁,也不替代确定性的调度单元测试。
`pnpm run test:web:stress` 保留为无密钥、需显式启用的浏览器性能证据。确定性的 `?fixture` 会话以独立于绘制的节奏发出 100,000 个 `reasoning-delta`,结尾标记证明事件经过生产会话归并并到达实时 Think 行50 毫秒心跳和预先调度的 DOM 事件分别测量主线程停顿与交互延迟250 毫秒预算用于识别明显回归。`DSH_WEB_STRESS_HEADFUL=1` 允许开发者在可见浏览器中使用 Performance 面板分析同一场景。该压力车道是手动性能诊断与修复验收证据,不是默认 CI 门禁,也不替代确定性的调度单元测试。
聚焦测试固定 `Notifier` 的逐帧合并、结构事件抢占、失效回调和无 rAF 回退,并在 `Session` 层证明一帧只发布一次最新累计文本且定稿不会被旧帧回调重复通知。fixture 的小型单元测试继续固定输入校验、外部到达节奏、并发拒绝、精确事件数和结尾标记交付,无需把 100,000 分片工作负载带入默认测试套件。
聚焦测试固定 `Notifier` 的逐帧合并、结构事件抢占、失效回调和无 rAF 回退,并在 `Session` 层证明一帧只发布一次最新累计文本且定稿不会被旧帧回调重复通知。fixture(测试前置数据)的小型单元测试继续固定输入校验、外部到达节奏、并发拒绝、精确事件数和结尾标记交付,无需把 100,000 分片工作负载带入默认测试套件。
## 曾考虑的替代方案
@@ -28,7 +28,7 @@ Status: implemented
**在接收或日志层丢弃、抽样或拼接原始分片。** 不予采纳:原始 `assistant/chunk` 是可重放的会话事实,改变它会损失诊断与 UI 保真度,并把展示频率策略混入数据权威层。
**只使用微任务合批。** 不予采纳:连续异步 `yield` 会在相邻分片间排空微任务队列,使一个微任务调度近似退化为一次分片一次通知
**只使用微任务合批。** 不予采纳:连续异步 `yield` 会在相邻分片间排空微任务队列,使微任务合批近似退化为每个分片通知一次。
**按动画帧控制测试生产方节奏。** 不予采纳:生产方会在渲染变慢时同步减速,使页面获得真实网络流不存在的隐式背压,并掩盖主线程饥饿。