Files
goodbuddy/docs/roadmap/product-performance-experience-improvement-plan.md
T

84 KiB
Raw Blame History

GoodBuddy 产品、性能与体验综合改进计划

文档信息

项目 内容
文档类型 跨功能实施路线图
状态 规划中
版本 1.0
日期 2026-08-31
适用范围 GoodBuddy Desktop、GoodBuddy Agent、发布与质量流程
评审基线 Desktop 0.11.13、Agent lock 0.11.14

1. 目的

本文把 2026-08-31 对当前仓库进行的功能、性能、体验和交付评审结果整理为一份可分批实施、 持续更新的权威清单。后续每一批工作都从本文选择明确范围,完成后更新状态、验证结果和相关 产品文档,不再维护平行的临时待办。

本计划优先完成已经存在但尚未形成真实用户闭环的能力,其次处理经过测量确认的性能问题, 再安排依赖外部平台或产品选择的增强能力。实施继续遵循以下原则:

  1. 先保证完整、可用的生产路径,再扩展能力范围。
  2. 性能优化必须有基线、目标和优化后的对比数据。
  3. 复用现有合同、IPC、Worker、通知、错误边界和界面组件,不引入不必要的框架。
  4. Ask 保持只读;Execute 继续代表当前本机或 SSH 账号的完整授权。
  5. 每批只处理列明的范围,不借机进行无关重构。

2. 明确排除的工作

本计划不包含初始 Renderer Bundle 优化,包括但不限于:

  • 不为了缩小入口包继续拆分轻量同级页面。
  • 不单独延迟 KaTeX、全局样式或其他入口依赖。
  • 不调整现有 Bundle 预算来制造通过或失败。
  • 不以 Bundle 大小为理由改变当前页面交互语义。

现有构建预算和按路由拆包继续作为回归保护保留。只有后续获得首屏解析或执行耗时证据,并由 用户单独确认范围后,才重新讨论该项。

3. 状态与优先级

3.1 状态

状态 含义
待开始 范围已确认,尚未实施
进行中 已进入当前实施批次
阻塞 存在明确外部依赖,已记录解除条件
已完成 代码、真实路径、测试和文档均完成
不做 经重新评审后明确移出范围,并记录原因

3.2 优先级

优先级 含义
P0 影响正式交付、安装或已声明生产路径,发布前处理
P1 阻断核心工作流或造成明显错误、卡顿、误导
P2 重要增强或规模化问题,可在核心闭环后实施

4. 分批实施总览

批次表示建议顺序,不表示必须一次完成整批。每次可以从当前批次选择一组边界清晰、能够独立 验收的事项。

批次 主题 事项 开始条件 完成门槛
0 正式交付闭环 REL-01 至 REL-03、QA-03 发布资源和目标平台明确 正式产物可安装、可启动,版本与验证声明准确
1 核心工作流与恢复体验 FUN-01、FUN-04、UX-01 至 UX-12、OPS-01 至 OPS-03 无 用户可理解状态、从常见失败中直接恢复,并能导出有界诊断
2 通道、更新和真实 Runtime FUN-02、FUN-03、FUN-05、QA-01、QA-02 对应平台测试配置可用 文件办公闭环、更新下载闭环和真实 Runtime E2E 均通过
3A 性能基线 PERF-01、PERF-03、PERF-05、PERF-07、PERF-09 固定测试数据和设备 形成可重复基线、目标阈值和原始结果
3B 已测量性能优化 PERF-02、PERF-04、PERF-06、PERF-08、PERF-10 对应 3A 指标确认存在瓶颈 指标改善且功能、取消、恢复语义不回退
3C 架构级界面性能改造 PERF-11 至 PERF-17 PERF-11 完整 App 基线已产生 第 7.1 节五条架构规则成立,并由基准和 lint 持续守护
4 增强能力 FUN-06 至 FUN-09 核心闭环稳定;首个电脑控制平台已选定 每项形成独立、完整、可真实使用的工作流

5. 发布与交付

REL-01 Desktop 与 Agent 版本闭环

  • 优先级 / 状态: P0 / 待开始
  • 目标: 当前 Desktop 源码依赖的 Agent 能从正式渠道取得并完成真实 Host 验证。
  • 范围:
    • 发布并验证 Agent 0.11.14,或在 Desktop 发布前恢复到已发布 Agent 契约。
    • 验证 Linux x64、Linux arm64 正式候选包。
    • 覆盖安装、Attach、Execute、取消和断线恢复。
    • 覆盖 GitHub、镜像和离线 GoodBuddy 传输路径。
  • 验收:
    • 干净用户目录能获取预期版本。
    • Desktop 到 Agent 的正常路径和恢复路径均使用当前候选产物通过。
    • 发布说明准确标注已验证架构和仍未验证范围。
  • 外部依赖: Agent 发布审批、双架构正式产物、真实 Linux arm64 环境。
  • 起始证据: package.json、agent-runtime-lock.json、 docs/features/remote-host/technical-design.md。

REL-02 正式 Latest 签名门槛

  • 优先级 / 状态: P0 / 待开始
  • 目标: 官网正式推荐下载不指向可能被系统默认安全策略阻止的测试产物。
  • 范围:
    • macOS 缺少签名或公证凭据时,不更新正式 Latest。
    • Windows 未签名构建不作为默认推荐安装包。
    • 未签名构建继续作为明确标注的测试 artifact。
  • 验收:
    • 发布工作流在签名条件不满足时不会切换正式 Latest。
    • 正式下载在全新 macOS、Windows 默认安全策略下完成首次启动。
  • 外部依赖: Apple 和 Windows 代码签名凭据。
  • 起始证据: .github/workflows/packages.yml、BUILD.md、README.md。

REL-03 发布包启动冒烟

  • 优先级 / 状态: P0 / 待开始
  • 目标: 发布成功代表打包应用能够启动,而不只是文件结构完整。
  • 范围:
    • 在对应 CI Runner 启动解包应用。
    • 等待 Main、Preload、Renderer ready 健康标识后退出。
    • Windows x64 接入现有 packaged DeepSeek Harness smoke。
    • 为启动设置总超时,并保留无用户内容的失败诊断。
  • 验收:
    • 缺 DLL、ASAR unpack、Preload 或 Main 启动错误会使发布任务失败。
    • 探针无凭据、无外网也能执行。
  • 起始证据: .github/workflows/packages.yml、build/build-release.cjs、 build/run-packaged-deepseek-harness-smoke.cjs。

6. 核心与增强功能

FUN-01 定时任务编辑

  • 优先级 / 状态: P1 / 待开始
  • 目标: 用户无需删除并重建计划任务即可纠正配置。
  • 范围:
    • 新增共享 Update schema、Preload 方法和可信 IPC handler。
    • 复用定制任务创建对话框作为编辑模式。
    • 支持修改名称、Prompt、Ask/Execute、Runtime、首次运行时间和周期。
    • 保留原 Task、Conversation、Schedule 和历史运行关联。
  • 验收:
    • 从任务界面进入编辑,保存后列表和下一次运行时间立即更新。
    • 编辑不产生重复 Task 或 Schedule。
    • 正在运行的实例继续使用启动时快照,后续触发使用新配置。
  • 起始证据: CustomTaskDialog.tsx、TaskScheduleActions.tsx、src/main/ipc.ts。

FUN-02 企业微信附件收发

  • 优先级 / 状态: P1 / 待开始
  • 目标: 企业微信完成“接收文件或图片、处理、回传成果”的办公闭环。
  • 范围:
    • 接收官方接口支持的图片和普通文件。
    • 将附件接入现有远程消息上下文。
    • 支持回传当前任务生成的图片或文件。
    • 复用 ChannelMediaAttachment、现有大小限制和 outbox 清理。
  • 验收:
    • 收到支持的附件不会再返回 attachment_not_supported。
    • 任务生成的有界成果能发送到原会话。
    • 超限、下载失败和发送失败提供明确错误且临时文件得到清理。
  • 起始证据: src/main/channels/wecom-driver.ts、 src/main/channels/wechat-sidecar.ts。

FUN-03 钉钉附件收发

  • 优先级 / 状态: P1 / 待开始
  • 目标: 钉钉非文本消息不再静默丢失,并完成文件办公闭环。
  • 范围:
    • 支持官方接口允许的图片和普通文件接收。
    • 支持任务成果附件回传。
    • 对暂不支持的消息类型返回明确说明。
  • 验收:
    • 支持的非文本消息进入对应远程 Conversation。
    • 不支持的消息有可见反馈,不被静默忽略。
    • 收发限制和清理行为与其他通道一致。
  • 起始证据: src/main/channels/dingtalk-driver.ts、 src/main/channels/dingtalk-channel-driver.ts。

FUN-04 浏览器错误分类

  • 优先级 / 状态: P1 / 待开始
  • 目标: 浏览器任务失败后,用户知道失败阶段和下一步操作。
  • 范围:
    • 保留代理、DNS、会话创建、CDP、导航和截图等失败阶段。
    • 增加少量稳定错误码及一句修复建议。
    • 在任务记录和界面中呈现,不建立通用错误协议框架。
  • 验收:
    • 典型失败不再全部显示为“工具执行失败”。
    • 原始敏感内容不进入 Renderer 或诊断日志。
    • 取消继续被识别为取消,而不是错误。
  • 起始证据: src/main/agent/model-runtime.ts、 docs/status/computer-control-implementation-status.md。

FUN-05 更新下载闭环

  • 优先级 / 状态: P1 / 待开始
  • 目标: 用户能在应用中取得当前平台安装文件并确认文件完整。
  • 范围:
    • 从当前更新源读取当前平台产物。
    • 下载到用户选择目录。
    • 使用现有发布清单校验大小和 SHA-256。
    • 校验成功后允许打开文件或所在目录。
    • 安装继续由用户手动触发。
  • 验收:
    • 下载源、版本、架构和当前系统匹配。
    • 损坏或哈希不匹配的文件不会被呈现为可安装。
    • 失败保留目标和错误上下文,可重新下载。
  • 起始证据: UpdateSettingsSection.tsx、src/main/version-checker.ts、 src/main/ipc.ts。

FUN-06 定时任务常用周期

  • 优先级 / 状态: P2 / 待开始
  • 目标: 覆盖常见办公日历,而不要求用户编写 Cron。
  • 范围: 增加工作日和每月周期,并持续显示下一次实际执行时间。
  • 验收: 创建、编辑、重启恢复和到期触发均按本地日历执行且不重复。
  • 依赖: FUN-01、FUN-07。
  • 起始证据: src/shared/assistant-contracts.ts、 docs/features/task-and-job/scheduled-task-prd.md。

FUN-07 时区和错过执行策略

  • 优先级 / 状态: P2 / 待开始
  • 目标: 用户明确控制任务在哪个时区运行,以及错过后是否补跑。
  • 范围:
    • 默认本机 IANA 时区。
    • 支持 skip 和 run_once。
    • 显示 DST 影响后的下一次实际运行时间。
  • 验收:
    • 重启或系统恢复后不会形成补跑风暴。
    • 时区和策略可创建、编辑、持久化并在界面复现。
  • 起始证据: src/main/assistant/assistant-database.ts、 docs/features/task-and-job/scheduled-task-prd.md。

FUN-08 旧版 Office 文件

  • 优先级 / 状态: P2 / 待开始
  • 目标: DOC、XLS、PPT 能进入聊天附件和知识库解析路径。
  • 范围:
    • 检测本机 LibreOffice。
    • 使用 headless 转换到 OOXML 或 PDF 后复用现有解析器。
    • 未安装时提供明确安装或手工转换建议。
  • 验收:
    • 三种旧格式各用真实文件完成聊天解析和知识库导入。
    • 转换取消、超时和失败后清理 GoodBuddy 临时目录。
    • 不内置第二套文档解析引擎。
  • 起始证据: src/main/knowledge/document-parser.ts、 DocumentParsingSettingsSection.tsx。

FUN-09 首个原生电脑控制适配器

  • 优先级 / 状态: P2 / 待开始
  • 目标: 在一个明确的主要部署平台上真实控制桌面应用。
  • 范围:
    • 首先从 Windows UIA、macOS AX 或 Linux 原生方案中选择一个平台。
    • 完成窗口快照、点击、输入、取消和退出清理。
    • 接入现有 computer-control broker。
  • 验收:
    • 在真实桌面应用完成一次可重复的查看、点击和输入任务。
    • Ask 仍不可执行桌面变更,Execute 路径记录活动。
    • 取消和关闭后不遗留 GoodBuddy 控制会话。
  • 外部依赖: 首个平台产品决策及对应真实设备。
  • 起始证据: src/main/capabilities/capability-service.ts、 docs/status/computer-control-implementation-status.md。

7. 性能

PERF-01 启动性能测量

  • 优先级 / 状态: P1 / 待开始
  • 目标: 得到冷启动各阶段的可重复耗时,而不是依据代码顺序猜测。
  • 指标: app.whenReady、窗口创建、数据库、知识库、Provider/Gateway、Runtime hydration、 IPC 注册、loadMainWindow、ready-to-show 和 Renderer 首帧。
  • 验收: 固定设备至少运行五次冷启动,报告中位数和 p95;诊断不包含用户内容。

PERF-02 启动路径优化

  • 优先级 / 状态: P1 / 待开始
  • 目标: 尽早显示可理解的应用壳,并延后经测量确认的非首屏工作。
  • 实施条件: PERF-01 证明具体阶段构成主要延迟。
  • 验收: 首帧和可交互时间达到批次开始时确定的目标,初始化失败仍有明确状态和恢复入口。

PERF-03 长流式回答基准

  • 优先级 / 状态: P1 / 待开始
  • 场景: 100KB、500KB、1MB Markdown 回答,包含推理、代码块和常见富文本。
  • 指标: React commit 数和 p95、Long Tasks、Markdown 渲染时间、输入延迟、滚动帧率、 Renderer 堆内存。
  • 验收: 测试数据、运行步骤和原始结果可重复执行。

2026-09-28 Measurement Evidence (Partial)

npx vitest run tests/streaming-markdown.electron.test.ts runs the real MarkdownRenderer in Electron 43.2.0 / Chromium 150.0.7871.129. It compares the original normalizer with fast paths that skip unchanged text, retaining live Markdown, math, code and HTML behavior. It also measures a 102,400-byte answer at six growing prefixes, alternating original/current execution order in the same page, with one warm-up sweep and three measured sweeps (18 samples each).

Measurement Original Current
100KiB growing-prefix render median, isolated run 54.80 ms 54.65 ms
Same run p95 95.50 ms 106.10 ms
Growing-prefix render median, full-suite run 53.50 ms 53.10 ms
Same run p95 93.70 ms 83.40 ms

These development-mode measurements include synchronous React rendering, DOM commit and style/layout, but exclude frame waiting and paint. With 18 samples, nearest-rank p95 is the maximum sample. The results do not establish an overall rendering improvement. Smaller 6.4-7.2K-character corpora do show lower normalizer cost: prose median 33-34 microseconds to below 1 microsecond, and mixed math/code median 32-35 to 16-20 microseconds. The production changes remain limited to this preprocessing and memoizing unchanged subagent-derived collections. In the four-subagent regression, initial render plus six updates builds the lookup map once instead of seven times; block-dependent scans still run when blocks change.

Validation: 550 prefix normalization comparisons, 1,100 short-prefix DOM comparisons and 24 long-prefix DOM comparisons passed. The real Electron terminal-status probe covers cancellation, failure, retry and persistence via three loopback model-stub requests; the Mermaid viewer covers nine scenarios and ten PNG downloads. No external model calls were used for these probes. Full npm test passed 459 files / 5,397 tests, with 11 files / 84 tests skipped. Type checking and scoped lint passed. Full lint remains blocked by the existing untracked sidebar-final probe files.

This is partial evidence for PERF-03, not completion: full-App input latency, frame rate, heap measurements and 500KB/1MB scenarios remain unmeasured.

PERF-04 流式更新合并

  • 优先级 / 状态: P1 / 待开始
  • 目标: 降低长回答期间的状态复制和完整 Markdown 重解析。
  • 实施条件: PERF-03 确认 commit 或 Markdown 渲染是瓶颈。
  • 范围:
    • 同一请求的 text/reasoning delta 按 requestAnimationFrame 合并。
    • 每帧最多提交一次相关 React 状态。
    • 流结束后再执行完整富文本渲染。
  • 验收: 目标指标改善;事件顺序、停止、重试、引用、工具和 Subagent 块不回退。

PERF-05 向量检索规模基准

  • 优先级 / 状态: P1 / 耗时基线已完成,取消响应与峰值内存待测
  • 场景: 1k、10k、50k chunks 的全文、向量和混合检索。
  • 指标: 总耗时、vectorScannedCount、Main event-loop lag、取消响应和峰值内存。
  • 验收: 固定语料和查询集产生可比较结果,并保留检索质量指标。

2026-10-02 耗时基线

npm run perf:knowledge(build/run-knowledge-perf.cjs)用 esbuild 打包生产 KnowledgeDatabase,在临时 SQLite 中写入确定性随机语料(每文档 100 chunks,每 chunk 80 词, 384 维向量,与内置嵌入模型一致),重开连接后对每个规模各测 15 次 search、vectorSearch 和 hybridSearchWithDiagnostics(图谱关闭)。这些调用是同步的,在 App 中由 Main 线程直接 执行,所以单次耗时就是 Main 被阻塞的时长。同一台设备(Node 24.18),p95:

chunks 全文 向量 混合
5,000 14 ms 54 ms 93 ms
20,000 59 ms 173 ms 219 ms
50,000 130 ms 396 ms 520 ms

向量扫描与 chunk 数线性相关,FTS 也随规模增长。按 50 ms 预算,约 5k chunks 起混合检索就会 使 Main 可感知地卡顿;5 万 chunks 时每次检索约冻结 Main 0.5 s,期间全部 IPC(包括流式输出、 终端和界面操作)排队。结论:PERF-06 的实施条件已满足;检索应先移出 Main(PERF-06, 随后并入 PERF-16)。取消响应和峰值内存尚未测量。

PERF-06 向量计算移出 Main

  • 优先级 / 状态: P1 / 待开始
  • 目标: 大知识库向量扫描不阻塞 Electron Main。
  • 实施条件: PERF-05 确认 Main event-loop lag 或取消响应不达标。
  • 范围: 优先复用已有 Worker,减少重复扫描和复制;本项不自动引入 ANN。
  • 验收: 检索排序和质量不回退,Main 延迟与取消指标达到目标。

PERF-07 知识库 Snapshot 测量

  • 优先级 / 状态: P2 / 待开始
  • 指标: SQL 时间、对象数量、structured-clone 估算字节、IPC 往返、Renderer commit 和 峰值堆内存。
  • 场景: 小型、典型和大型文档与图谱数据集。
  • 验收: 明确首屏实际需要的字段和超过目标阈值的数据规模。

PERF-08 知识库按需加载

  • 优先级 / 状态: P2 / 待开始
  • 实施条件: PERF-07 证明完整 Snapshot 是可见瓶颈。
  • 范围:
    • 首屏只返回列表字段、状态和计数。
    • 文档详情按选择加载。
    • 图实体、关系和证据仅在图谱视图加载。
    • 复用已有列表和 chunk 接口。
  • 验收: 首开与切换指标改善,选择、刷新、取消和局部错误状态不回退。

PERF-09 Keep-alive 压力测试

  • 优先级 / 状态: P2 / 文本会话部分已完成,图片与工具记录待补
  • 场景: 连续打开 12 个会话,每个会话包含 80 条富 Markdown、图片和工具记录。
  • 指标: DOM 节点、Renderer heap、GC pause 和流式 commit。
  • 验收: 明确隐藏会话缓存是否构成实际内存或渲染问题。

2026-10-02 文本会话测量

npm run perf:app 在流式场景之后连续打开 24 个未访问过的种子会话(每个 20 条富 Markdown), 每 4 次记录挂载的面板数、DOM 节点数和强制 GC 后的 JS heap(CDP HeapProfiler.collectGarbage + Runtime.getHeapUsage)。

已访问 挂载面板 DOM 节点 GC 后 heap
0(两次流式之后) 12 39,657 54.6 MB
4 / 8 12 39,656 54.6–55.1 MB
12 12 15,884 27.4 MB
16 / 20 / 24 12 15,884 27.3–27.9 MB

缓存上限 12 个面板有效:两条长流式回答被挤出缓存后,DOM 和 heap 回落并保持不变,后半段 增长为 0。此前观察到的“4 万节点、210 MB”是两条 40 KB 流式回答仍在缓存中时的瞬时值 (未 GC),不是持续泄漏。结论:文本会话场景下 keep-alive 不构成内存问题,PERF-10 不需要 调整缓存数量或有效期;含图片和工具记录的会话尚未测。

PERF-10 Keep-alive 轻量优化

  • 优先级 / 状态: P2 / 已完成输入与热切换渲染隔离,压力测试待完成
  • 实施依据: 2026-09-30 的真实 App 组件回归确认输入和热切换会重复渲染缓存内容; PERF-09 的 Electron 内存与 GC 压力测试仍待执行。
  • 范围:
    • ChatHistoryPane、任务条和 Runtime 清单使用 memo,稳定快捷输入、重试、图片恢复、 定时任务回调及任务条 JSX。异步草稿操作仍绑定发起时的会话。
    • 仍不达标时才调整缓存数量或一小时有效期。
    • 暂不引入完整虚拟化框架。
  • 验收: 内存和 commit 指标改善,同时保留会话切换位置和快速恢复体验。

App.test.tsx 修复前回归记录:运行请求期间连续输入三次,历史面板、任务条、清单各 渲染三次,会话迁移存储读取三次;两个已缓存会话往返一次,Markdown 渲染 324 次。 长会话已展开 160 条消息,另一会话含一条消息,计数包含展开的推理正文。修复后上述 计数均为零。最终用例还覆盖可见且展开的运行中任务条、实时文本与清单更新、停止、 会话草稿、图片素材选择期间切换会话,以及滚动位置和推理展开状态保留。 这些是 jsdom 中真实 App 的渲染次数,使用 mock preload;尚未测量 Electron 输入延迟、 绘制耗时或内存。流式事件未延后,缓存容量和有效期未改变。

7.1 架构级界面性能改造(批次 3C)

2026-10-02 的架构评审确认,界面卡顿的主要来源是架构组织方式,而不是 Electron 或 GPU:

  • Renderer 单组件状态: App.tsx 约 11k 行,App() 内有 123 个 useState、69 个 useEffect。会话、消息、草稿、任务和设置都在其中,输入和每次流式 flush 都会重新执行 整个 App 函数体;多个大面板未 memo,隐藏的 keep-alive 视图也会参与渲染。
  • Main 同步存储: 全部 SQLite 访问使用 Main 线程的 DatabaseSync,包括全表向量扫描、 会话列表与搜索、Story Graph 读取;PDF/Office 解析和附件同步文件 IO 也在 Main。
  • 全量回写: 活动记录在任何变化后 250 ms 全量 replace;会话每 500 ms 定时保存; *Changed 通知不带内容,Renderer 再重新查询完整列表。

改造完成后须同时满足以下规则,并由工具守护,避免回退:

  1. Main 只负责窗口、生命周期、权限和消息路由,不执行数据库查询、文档解析或批量计算。
  2. Renderer 状态按领域拆成外部 store,组件通过 selector 订阅;App.tsx 只保留布局壳。
  3. 流式文本和终端输出走专用通道,只更新正在显示的行或面板,按帧合并提交。
  4. 数据只有一个权威来源,跨进程只传增量;不再全量回写,也不再“通知后重新查全量”。
  5. 消息、会话、笔记、文件树等长列表虚拟化,DOM 数量不随数据量增长。

实施方式:先建立 PERF-11 基线,再沿 Renderer(PERF-12→PERF-13→PERF-14)和 数据进程(PERF-15→PERF-16)两条线并行推进,最后由 PERF-17 固化守护。每一步完成后 重新运行 PERF-11 场景并记录对比;不换技术栈,不一次性重写 App.tsx。

PERF-11 完整 App 交互基准

  • 优先级 / 状态: P1 / 进行中
  • 目标: 在当前生产构建、隔离 profile 和本地假模型下,可重复测量用户能感知的卡顿。
  • 运行: npm run build:bundle 后执行 npm run perf:app;结果写入 GB_PERF_OUTPUT 或系统临时目录,不含用户内容,不发出外部模型请求。
  • 场景: 空闲基线;Composer 连续输入;切换会话;打开长会话;大量会话列表下的流式 回答;流式回答期间同时输入。
  • 指标: Renderer Long Tasks(>50 ms)数量与总时长、按键到下一帧延迟、帧间隔 p95 与 掉帧比例、React commit 无法直接取得时以 DOM mutation 批次近似、Main event-loop 延迟 (monitorEventLoopDelay)、Main 与 Renderer 进程 CPU 和内存、Renderer JS heap。
  • 验收: 同一设备多次运行结果可比较;报告记录源码版本、环境、数据规模和原始样本摘要。

实现

  • build/run-app-perf.cjs:独立 supervisor。在 127.0.0.1:11434(新 profile 默认模型地址) 启动 OpenAI 兼容假模型,清除 ELECTRON_RUN_AS_NODE 和模型环境变量,设置整轮期限(默认 600 s),汇总报告并删除隔离 profile。端口被占用时直接失败。
  • tests/support/app-perf-driver.mjs:在导入 out/main/index.js 前设置 appPath、 userData、sessionData 和日志目录,然后由生产 Main 创建窗口、注册 IPC 并加载真实 Preload/Renderer。全部交互使用 sendInputEvent 原生输入;数据通过生产 conversations.saveLocal IPC 写入,再 reload 走正常启动加载。
  • Renderer 探针只观察:longtask、long-animation-frame、Event Timing、rAF 帧间隔、 keydown 到下一帧、MutationObserver 计数、DOM 节点数和精确 JS heap。Main 用 monitorEventLoopDelay,进程用 app.getAppMetrics()。
  • 默认规模:300 个会话 × 20 条消息,另加一个 2,000 条消息的长会话;输入 120 个字符, 按键间隔 60 ms;流式回答约 40 KB 混合 Markdown(标题、列表、代码、表格、行内/块级公式、 引用),每个 SSE 事件 12 个字符,2,000 字符/秒。可用 GB_PERF_* 环境变量调整, 见脚本开头。
  • GB_PERF_PROFILE=1 为有历史输入和流式场景采集 Renderer CPU profile 并汇总自身耗时 热点;剖析有开销,只用于归因,不作为基线数据。
  • GB_PERF_CPU_THROTTLE=<倍数> 用 CDP 降低 Renderer CPU 速度模拟低端设备; GB_PERF_MEMORY_VISITS(默认 24,0 跳过)控制 PERF-09 内存场景。

2026-10-02 基线

源码 8280158(工作区含其他未提交改动),Windows x64,Intel Core Ultra X7 358H ×16, 32 GB,Electron 43.2.0 / Chromium 150,DPR 1,内容区 1280×800,GPU 合成与光栅化均启用。 连续运行 3 次,假模型请求每次 2 次,外部模型请求 0 次。帧间隔基线为 8.3 ms(120 Hz)。

场景 Long Tasks 次数 / 总时长 帧间隔 p95 掉帧比例 交互延迟 p50 / p95 Main lag 最大
空闲(新 profile / 有历史) 0 / 0 8.5 ms 0% — 17–21 ms
输入 120 字(新 profile) 0 / 0 8.5 ms ≤0.2% 按键→帧 4.8–5.1 / 8.3–8.5 ms ≤17 ms
输入 120 字(有历史) 0 / 0 8.5 ms 0% 按键→帧 6.5–6.7 / 8.4–8.7 ms ≤18 ms
首次切换会话 ×15 1 / 77–81 ms 8.6 ms 3.7–4.5% 点击→显示 49–50 / 102–105 ms 29–32 ms
再次切换(缓存命中)×10 0 / 0 8.6 ms 3.3–4.1% 点击→显示 52–53 / 56–59 ms 19–22 ms
打开 2,000 条消息会话 1 / 94–102 ms 8.5 ms 1.6% 点击→显示 134–144 ms ≤21 ms
滚动长会话 3 s 0 / 0 8.5 ms 0% — ≤17 ms
流式 40 KB 回答 126–137 / 8.6–9.3 s 83 ms 45–48% — 30–36 ms
流式期间输入 100 字 128–182 / 8.8–11.9 s 83 ms 50–52% 按键→帧 12.5–13.5 / 35–36 ms 29–144 ms

其他观察:

  • 启动:窗口创建约 0.58 s,ready-to-show 约 1.29 s,Composer 可用约 1.42 s;带 300 个 会话 reload 到侧栏出现约 0.19 s。
  • 流式回答约 20 s,Long Tasks 占记录时间约 40%;按四等分统计为 0 / 2.5 / 4.7 / 4.6 s, 随回答变长而增加,说明每次提交的成本与已生成长度成正比。
  • 有历史时侧栏一次渲染全部 302 行,DOM 约 4,400 节点;访问过若干会话后约 1.6 万;两次 流式后约 4 万,JS heap 最高约 210 MB(keep-alive 缓存持续增长)。
  • 长会话只渲染最近 80 条,打开耗时主要是一次约 100 ms 的 Long Task。
  • KaTeX 的 data: 字体被 CSP 拒绝(缺少 font-src),每轮 8 条控制台错误,字体回退 (已在 PERF-12 第一步修复)。
  • app.getAppMetrics() 的 CPU 百分比在 Windows 上按全部逻辑核归一,只作相对比较。

归因(剖析运行): 流式期间 Renderer 自身 CPU 约 74% 在应用 bundle 中,热点是 hast-util-from-dom 的 parseFragment/fromDom(rehype-katex 把 KaTeX HTML 再解析成 hast)、micromark 分词、hast→React 转换和 React reconcile。对照实验确认根因:默认语料的 块级公式写在一行($$...$$),命中 MarkdownRenderer.tsx 的 unsafeSegmentLine, splitMarkdownSegments 退回整篇单段,每次 flush 都重新解析整篇回答并重新渲染全部公式。 仅把 $$ 改成独占一行(GB_PERF_CORPUS=multiline-math)后,同一流式场景 Long Tasks 从约 130 次 / 9 s 降到 1 次 / 0.15 s,帧间隔 p95 从 83 ms 回到 8.5 ms,流式期间按键→帧 p95 从约 36 ms 降到 15.5 ms。

结论与对后续批次的影响:

  1. 当前最严重、用户最易感知的卡顿是流式长回答,且由 Markdown 分段失效后的整篇重解析 主导;输入、切换、滚动和空闲在本机均未出现明显问题。PERF-12 应优先处理:让分段在 单行 $$、HTML 行、链接定义等情况下也能安全工作或局部回退,并对已完成段缓存渲染结果。
  2. 假模型 2,000 字符/秒高于常见真实模型输出速度,但成本随长度线性增长,真实速度下回答 足够长时同样会出现。
  3. 会话首次切换 p95 约 100 ms、打开长会话约 140 ms,可在 PERF-13/PERF-14 中改善, 优先级低于第 1 项。
  4. 本机为高性能设备;低端设备上的绝对值会更差,后续应补一台低配 Windows 设备基线。

PERF-12 流式与输入快速止血

  • 优先级 / 状态: P1 / 进行中(Markdown 分段已完成)
  • 范围: 流式 delta 在 Renderer 按 requestAnimationFrame 合并后提交,并以 transition 降低优先级;缓存 Markdown 预处理结果;RightAssistantSidebar、ProjectSwitcher 和 keep-alive 路由使用 memo 并稳定回调;滚动跟随只在需要时执行。
  • 验收: PERF-11 流式与输入场景的 Long Tasks 和输入延迟下降;事件顺序、停止、重试、 工具与 Subagent 块不回退。该项吸收 PERF-04 的范围。

2026-10-02 第一步:Markdown 分段与 KaTeX 字体

  • 分段(MarkdownRenderer.tsx): splitMarkdownSegments 按 micromark-extension-math 的实际规则识别块级公式:$$ 等两个以上 $ 开头、其后不含 $ 的行才开启公式块,并需要 不短于开启长度的 $ 行关闭;$$x^2$$ 这类单行公式按普通段落处理,不再使整篇退回单段。 链接定义和脚注改为逐行检查,位于顶格代码围栏内的行视为字面内容。原始 HTML 行、\r、 缩进围栏内的定义仍整篇退回。
  • 字体(electron.vite.config.ts): Renderer 构建不再把字体内联为 data: URL。CSP 没有 font-src,原先 4 个小于 4 KB 的 KaTeX 字体被拒绝并回退;现在 60 个 KaTeX 字体 全部作为同源文件输出,CSP 不变。
  • 等价性验证: 现有逐前缀对照语料新增单行公式、长 $ 围栏、代码围栏内的定义等用例; 新增 1,200 份确定性随机文档(半数取随机前缀)对照,要求分段渲染与整篇渲染的 DOM 完全 一致。开发期另跑了约 4.8 万份随机文档与前缀,其中约 6,300 份走分段路径,全部一致。
  • npm run perf:app 对比(同机,各 3 次,80579ee + 本改动):
场景 改动前 改动后
流式 40 KB:Long Tasks 126–137 次 / 8.6–9.3 s 1 次 / 0.16 s
流式 40 KB:帧间隔 p95 / 掉帧 83 ms / 45–48% 8.6–8.9 ms / 3–5%
流式期间输入:按键→帧 p50 / p95 12.5–13.5 / 35–36 ms 11.7–11.9 / 19–21 ms
流式期间输入:掉帧 50–52% 7.7–8.5%
KaTeX 字体 CSP 错误 每轮 8 条 0

输入、切换、长会话和滚动场景与基线持平。流式期间输入仍有约 8% 掉帧、按键 p95 约 20 ms, 剩余开销属于 App 级重渲染,留给 PERF-12 后续项和 PERF-13。

  • 剩余: 流式 delta 按帧合并、大面板 memo、滚动跟随优化仍待实施。

2026-10-02 低端设备模拟(Renderer CPU 4× 降速)

GB_PERF_CPU_THROTTLE=4 npm run perf:app 通过 CDP Emulation.setCPUThrottlingRate 把 Renderer 主线程降速 4 倍(Main 不降速,每次 reload 后重新应用),源码 5dc6545 (含 PERF-12 第一步),同一设备。不降速一栏为同一轮构建的对照。

场景 不降速 4× 降速
输入 120 字(新 profile)按键→帧 p95 8.7 ms 42 ms,掉帧 21%
输入 120 字(有 300 个会话) 0 Long Task,p95 9 ms 37 次 / 3.2 s,p95 355 ms,最大 483 ms
首次切换会话 ×15 点击→显示 p50 / p95 52 / 117 ms 434 / 922 ms
再次切换(缓存命中)p50 / p95 55 / 66 ms 482 / 534 ms
打开 2,000 条消息会话 140 ms 1.2 s
滚动长会话 帧 p95 8.5 ms 帧 p95 17 ms,正常
流式 40 KB 1 次 / 0.17 s,帧 p95 17 ms 156 次 / 16.7 s,帧 p95 183 ms,掉帧 100%
流式期间输入 按键→帧 p50 / p95 11.5 / 17 ms 575 / 1,513 ms

高性能机上不可见的问题,在降速后全部暴露:

  • 新 profile 输入只到 42 ms,有 300 个会话后升到 355 ms,差异来自每次按键重新渲染整个 App 和完整侧栏。降速剖析:resizeComposerTextarea(每次输入同步读 scrollHeight 强制 布局)约 8%;i18next formatLanguageCode/translate 约 12%(Intl.getCanonicalLocales 每次调用 t() 都执行,侧栏 302 行各自多次调用);其余是 App 与侧栏行的 React 渲染。
  • 流式期间约 18% 是 ChatHistoryPane 每次 flush 的滚动跟随 layout effect(读布局后 scrollTo),i18next 约 8%,Markdown 解析与 React reconcile 其余。
  • 缓存命中切换也约 0.5 s,说明慢的不是挂载新面板,而是切换触发的 App 级整体重渲染。

结论: 低端设备上输入、切换和流式都会明显卡顿,主因是 App 级重渲染和侧栏全量渲染, 证实 PERF-13(领域 store)和 PERF-14(侧栏虚拟化)有必要。PERF-12 剩余项应先做三处 低成本热点:Composer 高度改为只在需要时测量、t() 结果或 i18next 语言码规范化做缓存、 滚动跟随按帧合并且只在贴底时执行。

2026-10-02 第二步:三处低成本热点

  • Composer 高度(App.tsx resizeComposerTextarea): 记住上次的值与高度。值未变不测量; 在原文本末尾追加时不再先设 height: auto(只会变高),已到最大高度直接跳过测量;删除、 替换、清空仍按原方式重置并测量。
  • i18next 语言码(i18n/index.ts): i18next 25 每次 t() 都经 toResolveHierarchy→formatLanguageCode 调用 Intl.getCanonicalLocales。初始化后对 languageUtils.formatLanguageCode 加按输入缓存(结果只取决于代码与静态初始化选项,上限 64 项),翻译结果不变。
  • 滚动跟随(ChatHistoryPane.tsx): 激活面板时仍同步滚到底;之后内容更新时改为每帧最多 一次 requestAnimationFrame 中读取 scrollHeight 并 scrollTo,回调里再次确认仍贴底; 卸载时取消。读者离开底部时不滚动,恢复阅读位置逻辑不变。
  • 测试: 新增“流式三次更新只滚动一帧”(旧实现 3 次,失败)、Composer 追加与删除的测量 次数、t() 不再调用 Intl.getCanonicalLocales。全量测试 5,621 通过,2 失败与本改动 无关(真实 OpenCode 集成测试因免费额度返回 403;Supervisor 超时设置用例此前已失败)。
  • npm run perf:app A/B(同机,改动前为临时 worktree 中的 983cdd4 构建,交替运行):
场景(4× 降速,3 轮) 改动前 按键/点击 p50 / p95 改动后
输入 120 字(新 profile) 28–80 / 33–361 ms 19–36 / 24–57 ms
输入 120 字(有 300 个会话) 265–2,372 / 1,486–4,266 ms 27–161 / 32–346 ms
首次切换会话 442–761 / 1,063–1,384 ms 360–398 / 820–1,334 ms
再次切换(缓存命中) 472–749 / 501–1,143 ms 390–420 / 456–951 ms
流式 40 KB:Long Tasks 总时长 15.9–20.6 s 15.1–17.4 s
流式期间输入 432–6,778 / 1,146–9,634 ms 96–466 / 297–1,567 ms

不降速时(2 轮)两版各场景持平,输入 p95 均约 9 ms,流式 1 个 Long Task。4× 降速下 方差很大(同版本轮间可差数倍),应看量级:有历史时输入从秒级回到百毫秒内,切换略有改善, 流式本身仍是每帧卡顿——剩余成本是 App 级重渲染与 Markdown/React 提交,需 PERF-13。

  • 剩余: 流式 delta 按帧合并后以 transition 提交、大面板 memo 仍待实施;之后进入 PERF-13。

PERF-13 Renderer 领域 Store 迁移

  • 优先级 / 状态: P1 / 进行中(流式缓冲、Composer 草稿已迁移)
  • 顺序: 流式缓冲 → Composer 草稿 → 会话与消息 → 任务与通知 → 设置及其余界面状态。
  • 每个领域的完成步骤: 建立 store 并把 IPC 订阅移入 store;调用方改为 selector;删除 App 中对应 state/ref 同步代码;运行测试与 PERF-11。
  • 验收: 输入和流式更新不再重新渲染 App 根组件;App.tsx 最终降到布局壳规模。

2026-10-02 第一步:流式缓冲与 Composer 草稿

  • 流式缓冲(live-message-store.ts): text/reasoning delta 不再逐条 setConversations, 而是写入按消息分组的外部 store;ChatMessageRow 用 useSyncExternalStore 订阅自己的消息, 只有这一行重新渲染。App 状态每 250 ms 吸收一次,或在任何其他会话更新时先吸收。
    • 每个 delta 带序号和应用后的正文/思考长度。store 生成的消息对象记录已包含的序号;持久化 快照等外来��象按长度判断(delta 只追加),因此不会重复或丢失。
    • 一次更新只吸收派发时已到达的 delta,工具、审批等块与文本保持原有顺序。
    • 吸收后裁剪已提交的 delta,行内显示增量折叠,长回答期间缓冲区保持很短。
    • 吸收用普通更新而非 transition:transition 会被行的同步更新反复打断,导致缓冲区一直增长 (首版实测出现过流式 Long Tasks 翻倍)。
  • Composer 草稿(composer-draft-store.ts): 按会话的草稿移出 App state;输入框和发送按钮 分别订阅草稿文本和“是否有内容”,按键不再重渲染 App。高度调整改为草稿提交后的 layout effect。
  • 测试: store 单测覆盖折叠等价、提交前显示、工具块顺序、派发时快照、替换快照不重复、 React 重放幂等、无基线不隐藏、长流裁剪;App 测试改为断言流式文本不再渲染历史面板与任务条。
  • npm run perf:app A/B(同机,改动前为临时 worktree 中的 90ea33e 构建,交替运行):
场景(4× 降速,3 轮) 改动前 p50 / p95 改动后
输入 120 字(新 profile) 18–21 / 24–26 ms,掉帧 7–13% 9–10 / 11–16 ms,掉帧 0–1%
输入 120 字(有 300 个会话) 27–81 / 35–227 ms 10–19 / 12–36 ms
流式 40 KB:Long Tasks 总时长 6.5–20.8 s 3.6–19.3 s(3 轮中 2 轮约 4–5 s)
流式期间输入 73–1,808 / 174–3,498 ms 34–62 / 59–573 ms
打开 2,000 条消息会话(见下方更正) 1.07–1.80 s,帧 p95 9–42 ms 0.47–0.76 s,帧 p95 75–1,008 ms
首次 / 缓存命中切换会话 持平 持平

更正(归因): “改动后”构建包含了当时工作区中另一项未提交的改动——新会话面板分两步 渲染(先最后 20 条,首帧后补齐 80 条,后提交为 09c415b)。打开长会话的耗时下降主要来自 它,而不是流式缓冲;同时补齐那一帧变成长帧,4× 降速下该场景帧间隔 p95 从 9–42 ms 升到 75–1,008 ms。输入与流式期间输入的改善来自本步骤。分两步渲染只是过渡,补齐帧的成本需由 PERF-14 虚拟化解决。

不降速时(2 轮)各场景持平或略好,流式期间输入 p95 从 15–17 ms 降到 11–12 ms。流式本身在 4× 降速下仍每帧掉帧:剩余成本是行内 Markdown 解析与 KaTeX 渲染,以及每 250 ms 一次的 App 级吸收;切换会话的 App 级重渲染未改善,留给后续领域迁移。

  • 远程 Runtime: 远程事件与本地事件走同一 agent:event 通道和同一 Renderer 处理函数, 上述 Renderer 改进同样生效。远程事件在 Main 中逐条同步写 SQLite(每个 delta 约 2 个写事务, 会话恢复路径还会重写整条消息)后再转发,这部分未改,归入 PERF-15/PERF-16。
  • 基准工具: GB_PERF_MODEL_PORT 可把假模型移到其他端口(例如本机已运行 GoodBuddy 占用 11434 时),隔离 profile 通过 GOODBUDDY_MODEL_* 环境变量指向它。

2026-10-02 其他改动的架构核对

对同期其他功能改动按本批次 5 条规则核对(监督者功能除外):

  • 一致: 剪贴板改为异步读写(d773f5e);未保存修改通过 context 上报、记录不进 App state, 有未保存修改的路由不被缓存淘汰(43be465);新会话面板分两步渲染(09c415b,过渡方案)。
  • 偏离规则 1: Story Graph 检索(e97263c)仍在 Main 内经 DatabaseSync 读取后,在 JS 中 逐条 JSON.stringify 并按多个关键词匹配,单次成本高于原整句匹配;工具描述又鼓励 Agent 主动调用,调用次数会增加。纳入 PERF-15/PERF-16,在此之前补测检索耗时并限制扫描规模。
  • 偏离规则 2: 侧栏 5 秒重排(ee4eb73,use-conversation-list-order.ts)由 App 持有状态, 渲染期间 setState,定时器每 5 秒检查;顺序不变时跳过,当前成本低。交互上悬停、滚动、菜单 打开时不重排。侧栏迁移和虚拟化(PERF-13/PERF-14)时移入侧栏组件。
  • React 规则: 缓存会话保持滚动位置(bf6c4a9)在渲染期间写 ref,即当前 lint 的 3 条 react-hooks/refs 错误;启用 React Compiler 前需改为由状态派生顺序。

2026-10-02 并行修复:补全分帧、远程批量写库、架构守护

  • 补全分帧(ChatHistoryPane.tsx): 新面板补全改为每帧 +20 条(20→40→60→80),每步后 贴底或按高度差保持阅读位置;“加载更早”、批次变化或笔记跳转到达时立即渲染完整批次。
  • 渲染期 ref(App.tsx、pane-order.ts): 会话面板顺序改为 state,用纯函数 reconcilePaneOrder 在渲染期按需调整,顺序不变时返回原数组;消除 3 条 react-hooks/refs。
  • 远程批量写库(remote-event-batcher.ts、assistant-database.ts、ipc.ts): 远程语义事件 按 checkpoint 一个事务提交(Agent 在 checkpoint 之后才可能 ACK),可恢复会话每批只读写一次 消息;非远程事件、question、done/error、显式 flush、256 条或 50 ms 计时器也会先提交。 只转发新写入的事件,顺序不变;提交失败整批回滚且之后拒绝继续提交。交互、后台/定时/通道 和重启恢复三条路径都已改用。当前 checkpoint 粒度是每条 transcript 记录而非每页,按页批量 需要改 acp-remote-runtime.ts。
  • 架构守护(提前实施 PERF-17 的一部分): eslint.config.js 新增只减不增的白名单规则: DatabaseSync(19 个文件)、src/main 同步 fs(8 个文件)、组件直接订阅 window.goodbuddy.*.on*(6 个文件)、App.tsx max-lines 11700;文档演示和 vendor 脚本 不再参与 lint,npx eslint . 现为 0 错误。build/perf-thresholds.json + npm run perf:check(或 GB_PERF_CHECK=1 npm run perf:app)检查不降速报告的阈值。
  • npm run perf:app A/B(改动前为 9d8477b): 不降速 2 轮两版均通过 60 项阈值;打开长会话 从 1 个 Long Task / 56–61 ms、最大帧 50–58 ms 降到 0 个、最大帧 25–33 ms,其余持平。4× 降速 3 轮:打开长会话最大帧从 475–567 ms 降到 225–283 ms(帧 p95 因多出几帧小提交升到 175–258 ms);切换会话持平;流式场景两版方差都很大,未见一致变化。远程批量写库不在该基准 覆盖范围内,由单元测试和 ipc.test.ts 验证事务次数、顺序与回放去重。

2026-10-03 会话与消息领域(第一片)

  • 会话 store(conversation-store.ts、conversation-selectors.ts): 会话数组移出 App state, getState() 同步可读,取代 conversationsRef 及其 5 处直接写入;set() 先吸收流式 delta 再同步更新。按会话 id 和列表两级通知,选择结果未变时保持同一对象。
  • 侧栏(ConversationSidebar.tsx): 搜索匹配、5 秒重排、行级派生值移入侧栏自身订阅; 切换会话时侧栏只重渲染状态翻转的两行。顺便修复 React 19.2 中 memo 组件内 useEffectEvent 不刷新导致重排用旧列表的问题(有回归测试)。
  • App 只订阅: 活动会话、缓存面板的会话、数量,以及标题/审批/项目活动的按值比较 选择器。App.tsx 从 11,636 行降到约 10,960 行。
  • 仍在 App: 未读、展开、菜单、重命名、删除等界面 ID;持久化、历史加载与合并; 活动会话的更新(含 250 ms 流式吸收)仍重渲染 App。

2026-10-03 PERF-13 进展:Composer、路由边界、任务 store、切换会话

提交 41be9f8、2e3d2e7、651475d、492f1d2、6b634c5。

  • Composer(Composer.tsx,memo): 输入区页脚(约 930 行 JSX、23 个内联回调)移出 App。 草稿读 composer-draft-store,会话字段读 useComposerConversationView(不含消息数和结果 ID), 弹出菜单状态放进 composer-menu-store.ts;动作经 use-composer-actions.ts 保持同一引用。 其他会话更新、当前会话新增消息、别的会话输入都不重渲染 Composer(有测试)。
  • 路由边界: KeepAliveRoute 改为 memo,隐藏路由在 App 重渲染时保持上次结果,再次显示时 补一次渲染。知识库、设置、监督、笔记、右侧栏、项目切换器改为 memo,知识库 26 个回调移到 use-knowledge-workspace-actions.ts,其余用 useStableHandlers;笔记面板元素 useMemo。
  • 任务与结果(task-store.ts / task-selectors.ts / task-sync.ts): assistantTasks、 assistantArtifacts 移出 App state,assistantTasksRef 删除。App 只选取顶层任务状态、活动摘要 所需字段和当前会话是否忙碌,子代理进度不再重渲染 App;结果刷新时内容相同的对象保留引用, 会话面板自己读结果表。agent-event-handler.ts 只改了任务/结果调用。队列状态未迁移:它和发送、 恢复、中断流程交织,单独拆风险高于收益。
  • 切换会话: 面板顺序改为渲染期派生(usePaneOrder),Composer 弹窗按上下文键关闭, 不再在渲染中 setState,切换一次只渲染 App 一次(测试在旧代码上为 2 次)。标题表在标题不变时 保持同一 Map。
  • 规模: App.tsx 9,576 → 7,904 行(含期间合入的监督者改动);App 内 useState 120→114、 useEffect 62→56、useRef 79→73、useCallback 89→83。lint 上限 9,600 → 8,000。
  • npm run perf:app A/B(改动前为 b28ddd4 临时 worktree 构建,与 6b634c5 交替各 3 轮, 4× 降速,取中位数,括号为 3 轮范围):
场景 改动前 改动后
首次切换会话 延迟 p95 525 ms(520–674) 489 ms(470–607)
首次切换 Long Tasks 数 / 总时长 37 / 3.9 s 24 / 2.0 s
缓存切换(warm)延迟 p95 254 ms(208–256) 139 ms(135–149)
缓存切换 Long Tasks 数 / 总时长 21 / 1.7 s 12 / 0.7 s
热切换(hot)延迟 p95 139 ms 94 ms(77–154)
流式 40 KB:Long Tasks 数 / 总时长 / 帧 p95 90 / 7.0 s / 208 ms 49 / 3.1 s / 108 ms
流式期间输入:延迟 p95 / Long Tasks 总时长 / 帧 p95 110 ms / 8.3 s / 233 ms 53 ms / 3.5 s / 100 ms
输入(有 300 个会话)延迟 p95 11.7 ms 14.1 ms(12.6–15.9,噪声范围内)

流式两个场景的掉帧率仍接近 100%(4× 降速下每帧仍有 Markdown 解析和 250 ms 吸收),但 Long Tasks 减半。不降速一轮 GB_PERF_CHECK=1:60 项阈值全部通过。

PERF-14 长列表虚拟化

2026-10-03 消息时间线窗口化

  • 实现(chat-message-window.ts、ChatHistoryPane.tsx、ChatTimeline.tsx): 自研窗口化, 未引入依赖。只挂载视口附近的行(上限 60 行,外加获得焦点的行),其余用按实测高度或 平均高度估算的占位块代替;行高按会话缓存(最多 24 个会话)。以视口内的一行为锚点, 行高变化或挂载/卸载时修正滚动位置,读者看到的内容不动。
  • 保留: 96 px 贴底阈值和流式时每帧跟随;发送消息回到底部;“加载更早”仍按 80 条扩展 可滚动范围;笔记跳转先挂载目标再滚动;隐藏/显示和重新挂载恢复位置;隐藏面板不测量。 分帧补全(09c415b/855b941)已删除,打开面板一次提交有界行数。
  • 滚动滞后窗口: 视口离开已挂载范围 160 px 内才重新取窗,取窗时挂载 1.5 屏余量,避免 每个滚动事件都重挂行。
  • 毛玻璃锚点: 锚点以 .chat-content 的上内边距为基准,切换毛玻璃时阅读位置不变 (由 tests/frosted-glass.electron.test.ts 在真实 Electron 中验证,该测试改为按消息 id 跟踪 锚点行)。
  • 已知限制: 页内查找搜不到未挂载的消息;快速甩动时可能短暂看到空白占位;单条超长 消息仍整行挂载;估算高度会让滚动条滑块在首次经过时轻微变化。

PERF-15/16 第一片:热点读路径移出 Main

2026-10-03 只读查询 worker

  • 实现(readonly-query-reader.ts、readonly-query-worker.ts): 每个数据库文件一个只读 worker 线程,沿用 execution-stats-reader 的模式,复用 KnowledgeDatabase / AssistantDatabase 的生产方法,结果与同步路径同源。最多 32 个并发请求;worker 崩溃或 启动失败时转为同步执行,30 秒后再重启;:memory: 始终同步。AbortSignal 通过共享标志 取消(向量扫描每 256 行检查)。没有新增 DatabaseSync 白名单。
  • 已移出 Main: 知识库检索(retrieve、searchHybrid);Story Graph 读取(Agent 通过 MCP 调用,先在 SQL 中按范围预筛,超过 5 万个对象返回 scan_limit_exceeded);会话搜索(一条 SQL + 注册函数,保持 toLocaleLowerCase().includes() 语义)。会话列表的 N+1 查询改为两次 批量查询,IPC 结构不变。
  • npm run perf:knowledge(改用 Electron 运行):
文本块 混合检索时 Main 最大事件循环延迟(同步 → worker) 单次耗时(同步 / worker)
5,000 54.9 → 19.5 ms 47.7 / 53.3 ms
20,000 204.3 → 19.6 ms 188.6 / 198.9 ms
50,000 489.9 → 18.6 ms 466.8 / 478.6 ms
  • 剩余: 向量检索仍是全表扫描(只是不在 Main 上);写入和其余约 530 处同步调用仍在 Main;独立数据进程和 DatabaseSync 白名单缩减未开始。

2026-10-03 三项合并后的 npm run perf:app A/B(改动前为 53b9ecb)

不降速:新版本通过全部 60 项阈值。4× 降速(改动前 3 轮,改动后 3 轮,滚动场景另取调整后 2 轮):

场景 改动前 改动后
首次切换会话 p50 / p95 335–377 / 677–730 ms 198–312 / 518–634 ms
切回刚看过的会话 p95 379–386 ms 179–240 ms
切回最近的会话 p95 161–164 ms 89–140 ms
打开 2,000 条消息会话 448–484 ms,帧 p95 200–225 ms 290–354 ms,帧 p95 8–9 ms
滚动长会话:帧 p95 / 掉帧 17 ms / 7% 58–67 ms / 15–16%
流式输出、流式时打字 两版波动都很大,未见一致变化 同左
打字、空闲 持平 持平

滚动是唯一变差的场景:改动前长会话一次性挂载所有已加载行,滚动只是移动画面;窗口化后 每隔几屏要挂载新行(含 Markdown 渲染)并测量。初版每个滚动事件都重新取窗,掉帧约 70%; 加入滞后窗口后降到 15–16%。剩余开销主要在新行渲染和 getBoundingClientRect 测量,后续 可把锚点测量合并到每帧一次,或给 Markdown 结果加缓存。

2026-10-03 第二轮并行:滚动、Markdown 缓存、侧栏窗口化、App 继续瘦身、活动记录

  • 滚动(ChatHistoryPane.tsx、chat-message-window.ts): 滚动事件只读 scrollTop 等 滚动容器指标;是否重新取窗改为每帧最多判断一次,并用“锚点行 + 缓存行高”推算视口位置, 稳定滚动时不读元素几何。提交后只有挂载集合或占位高度变化时才校正锚点;上内边距缓存, 在 resize、激活和重新取锚点时刷新(真实 Electron 毛玻璃测试曾因缓存旧边距偏 105 px,已修)。
  • Markdown 渲染缓存(markdown-render-cache.ts): 按源文本和管线配置缓存解析后的 hast 树(LRU),重新挂载的行跳过 micromark/remark/rehype/KaTeX,只做 hast → React。
  • 侧栏窗口化(use-conversation-list-window.ts): 60 个会话以上只挂载视口附近的行, 活动会话、菜单、重命名、获得焦点的行保持挂载;aria-setsize/aria-posinset 保留总数。
  • App 继续瘦身: 会话持久化与历史加载(conversation-persistence.ts)、刷新调度 (conversation-refresh.ts)、Agent 事件处理(agent-event-handler.ts)、历史面板 (ConversationHistorySlot.tsx)移出 App。App.tsx 从约 10,960 行降到 9,566 行,lint 上限 改为 9,600。
  • 活动记录(activity-history-plan.ts): Main 的整份替换只校验、序列化变化的记录; 新增增量 IPC activity-history:update。Renderer 还没有改用增量接口。
  • 会话读取: conversations:list-summaries 和 conversations:get 也改由只读 worker 执行。
  • npm run perf:app 驱动: 侧栏窗口化后不再一次挂载全部会话,驱动改为按 aria-setsize 判断加载完成,点击会话前逐屏滚动侧栏直到该行挂载。

npm run perf:main(1,000 个会话 / 2 万条消息 / 5,000 条活动记录,Main 事件循环最大延迟):

路径 改动前 改动后
会话摘要列表 25.4 ms 0.8 ms(单次耗时 31 ms,在 worker 上)
活动记录整份替换,无变化 13.8 ms 9.3 ms
活动记录增量更新 1 条 — 5.3 ms

npm run perf:app,4× 降速,改动前(169941c)与改动后各 3 轮:

场景 改动前 改动后
滚动长会话:帧 p95 / 掉帧 25–50 ms / 10–11% 17–41 ms / 9–10%
打开 2,000 条消息会话:长任务 / 点击 p95 165–170 / 238–247 ms 97–105 / 211–218 ms
首次切换会话:长任务 / 点击 p95 1,969–2,115 / 444–460 ms 1,256–1,341 / 425–435 ms
切回刚看过的会话:长任务 / 点击 p95 1,084–1,183 / 151–158 ms 152–205 / 110–146 ms
流式输出 40 KB:长任务总时长 / 掉帧 1.6–2.4 s / 90–99% 0.33–0.41 s / 63–72%
流式时打字:长任务 / 按键 p95 3.0–3.6 s / 41–48 ms 0.53–0.64 s / 29–43 ms
有历史时打字:DOM 节点 4,382 705–719

不降速:全部阈值通过;流式输出和流式时打字的长任务均为 0。上一轮滚动的退化已消除(本轮基线 已含窗口化,故与 53b9ecb 的 17 ms / 7% 不完全可比,帧 p95 在两版中都波动较大)。

2026-10-03 本期收尾:已做、暂缓与理由

本期收尾只做低风险改动:

  • 侧栏 id 列表保持同一数组(ConversationSidebar.tsx):流式更新时,会话对象每次都会变, 原来每次都生成新的 id 数组,导致侧栏窗口化的布局 effect 重跑并读取 scrollTop。4× 降速剖析 显示,这段在流式场景中占 562 ms self time。现在 id 不变时,数组也保持不变。
  • 会话标题跑马灯不在挂载时强制布局(OverflowMarquee.tsx):挂载时不再同步读取 scrollWidth,改由 ResizeObserver 在首次报告时测量,有回归测试。悬停时的测量方式不变。
  • 4× 降速下复测,这两项的变化都在波动范围内(同版本多次运行,差距可达 2 倍),没有出现退化; 不降速时 60 项阈值全部通过。

本期暂缓,理由如下:

项目 理由
Renderer 改用 activity-history:update 需要在 App 中跟踪每次变化并处理顺序与失败重试;Main 端已经只处理变化的记录,Main 卡顿从 13.8 ms 降到 9.3 ms,剩余收益小,而出错会丢活动记录
数据库写入异步化、独立数据进程(PERF-15/16 剩余部分) 涉及约 530 处调用和写入顺序语义,属于高风险大改动;本期已把最重的读取路径(检索、会话列表、搜索)移出 Main,需要单独立项
首次切换会话(4× 降速 p95 约 430 ms) 剖析显示,开销分散在 React 渲染、首次 Markdown 解析和 i18n 上,没有单一热点;不降速时 p95 约 70 ms,已在阈值内
流式输出 Markdown 放进 worker 需要重写渲染管线,并处理流式中途结果和公式;不降速时流式场景已无长任务
页内查找覆盖未挂载的消息 当前 App 没有页内查找功能(未调用 findInPage),属于新功能,不是回归
性能阈值接入 CI 仓库目前没有 CI 工作流;npm run perf:check 可以在本地运行

7.2 按性能原则补齐差距(2026-10-03,第三轮并行)

统一标准见 docs/architecture/performance-principles.md(P1–P10、性能预算和评审清单)。本轮按其中“当前差距”并行推进:

原则 改动 测量结果
P1 文档解析、PPTX 提取、分块、大文件(≥ 1 MiB)哈希移到 document-parse-worker 线程,FIFO 队列;worker 不可用或崩溃时退回原来的 Main 内联执行,30 秒后重试;中止正在执行的任务会终止 worker。数据库写入仍由调用方按原顺序执行 npm run perf:document-parse,各 3 轮:Main 最长阻塞 large.pdf 498–679 → 16–17 ms,large.docx 313–335 → 17–20 ms;总耗时基本不变
P3 终端输出:Main 端按 8 ms 窗口合并(空闲时第一块立即发出,不增加回显延迟),单批不超过 64 KiB;控制事件(退出、错误、状态)发出前先清空缓冲,顺序不变;背压按“已发出 + 已缓冲”字节计算,暂停阈值留 1/4 余量。Renderer 端每帧最多写一次 xterm,写完后按最大序号累计 ACK;窗口不可见时 100 ms 定时兜底,保证 ACK 不中断 单元测试覆盖顺序、UTF-8 边界、背压、退出前清空;perf:app 未覆盖终端场景
P5 笔记列表(按卡片行窗口化,CSS subgrid)、工作区文件树(展开后拍平,按缩进显示层级)、知识库文档表格只渲染可见部分;抽出通用 use-list-window / use-grid-columns,侧栏会话列表改用同一实现。选中项、打开菜单的行、焦点行保持挂载 每个列表新增窗口化单元测试;真实 Electron 文件树布局测试改为验证窗口化后的停靠、滚动和预览返回
P6 Main 启动打点(startup-marks.ts),新增 npm run perf:startup:首次运行 / 空配置 / 300 个会话三种配置,各 5 轮 见下表
P9 package.json 新增 perf:startup、perf:document-parse —

冷启动基线(标准机,中位数 / p95,ms):

阶段 首次运行 空配置 300 个会话
启动到输入框可用 1284 / 1322 1024 / 1040 1028 / 1040
Main 模块加载(index.js 求值) 414 / 420 411 / 420 413 / 423
本地工具环境初始化 154 / 169 160 / 163 153 / 160
知识库初始化 240 / 249 19 / 22 32 / 42
助手数据库初始化(同步) 205 / 206 4 / 4 14 / 15
Renderer 导航到输入框出现 228 / 233 226 / 244 224 / 228

启动的主要开销是 Main 模块加载(约 415 ms)和本地工具环境初始化(约 155 ms);首次运行另有约 450 ms 的建库开销。这些只做了测量,没有改动,后续按 P6 拆分延迟加载。

本轮回归:perf:app 60 项阈值全部通过(不降速,流式和流式打字长任务均为 0);typecheck、全量 lint 通过;全量测试中 2 个失败分别为 OpenCode 免费额度 403 的集成用例和 magic-notes-* 真实 Electron 测试在整套并行负载下的偶发失败,后者单独运行和 6 个 Electron 布局测试并行连跑 3 次均通过。

7.2.1 活动记录增量与分页(2026-10-04,P2/P4/P5)

之前启动时整份读取活动记录(本机约 2.4 万条、10 MB),任何变化 250 ms 后把整份列表 replace 回 Main;Main 每次在列表前面插入记录都要重写顺序 JSON。分三次提交改完:

  1. 增量保存(6aab78b)。 活动记录移出 App 状态,放进 activity-store.ts。领域动作(新增、更新请求、审批、按请求删除、按调用删除、结束请求、对账、清空)修改状态,同时排入对应的契约变更;activity-sync.ts 每 250 ms 用 activityHistory.update 发送(每次最多 2 万条),失败的变更按原顺序留在队列里,退避重试;退出和卸载前也会刷盘。清空改为新增的 activityHistory.clear(),Main 在一个事务里完成。
  2. Main 分页、汇总、对账(352da66)。 第 57 版 schema 重建 activity_history_records:order_seq(越新越大,移到最前就分配新的最大值)加上从记录 JSON 生成的存储列(状态、类型、请求、会话、项目、创建时间)和索引。顺序从 record_order_json 原样迁移;旧数据里的重复 ID 保留,仍然只显示第一条。ActivityHistoryRepository 负责全部 SQL:按索引倒序分页 page,summary 返回面板原来从整份列表算出的计数、会话标题和会话状态,reconcile 用 SQL 关联任务表(追加的说明行由界面传入本地化文本),以及 clear。分页和汇总走只读 worker。
  3. 界面只保留已加载的页(bf9850a)。 启动顺序:发送排队的变更,Main 对账,再加载首页(200 条)。打开运行记录页时补足一批(500 条,和以前首屏一样多),“继续显示”先显示下一批,再在后台加载下一页;切换筛选会重新取首页;离开页面只留一批。计数、分组、标题、会话状态和清空数量都来自 Main 的汇总。没加载的记录也照样增量更新:update 新增按内容匹配的变更(upsert-call、update-request、resolve-approval、settle-request、remove-request、remove-call),由 Main 在 SQL 里执行,界面对已加载的页用同一份参考实现处理。任务刷新后的对账也改在 Main 里用 SQL 完成。旧 localStorage 记录改成增量变更迁移,界面不再调用 replace。

npm run perf:main,2.4 万条活动记录,两轮结果一致(括号内为 Main 事件循环最大延迟):

路径 之前 之后
启动读取 整份 get:14.2 MB,178 ms(198 ms),在 Main 上执行 首页 + 汇总:114 KB,16 ms,在 worker 上执行(Main 0.6 ms)
首页 200 条(同步路径,参考) — 1.0 ms(1.3 ms)
汇总(同步路径,参考) — 14 ms(15.6 ms)
保存 1 条变化 整份 replace:98 ms(114 ms) update:3.9 ms(6.9 ms)
每次保存的载荷 整份列表(约 14 MB) 只含变化的记录,与总量无关(单元测试:3 条变化 < 1.5 KB)

测试:随机动作序列(含翻页、截断、刷盘和注入的保存失败),在真实数据库上与改动前的整份列表逻辑逐步对比,最终数据库与整份 replace 的结果一致(含重复 ID 和移到最前);启动后界面不超过一页;保存载荷只取决于变化的记录(含未加载的);随机数据集(含重复 ID)下,分页加汇总渲染出的计数、分组、标题、状态和清空数量与整份列表渲染一致;重启后遗留的进行中记录被标为中断,说明文本相同;清空后数据库、界面和汇总都为空;SQL 变更语义与参考实现一致。perf:app 60 项阈值全部通过。

后续(2026-10-04):汇总覆盖全部已加载会话,超过 5,000 个时分多次请求再合并,不再截断(单元测试:5,003 个会话分 5,000 + 3 两次请求,全部有标题和状态)。分组里“会话活动数”统计的仍是已加载的记录,和以前统计已显示的记录一致。

7.2.2 Main 写库与启动:真实数据测量(2026-10-04,PERF-15)

用便携版 dist/GoodBuddy-windows-x64/data 的副本测量(源目录从不被打开):assistant.sqlite 2.4 GB,175.5 万条任务事件(其中 148 万条子任务进度、1.1 GB),4,812 条消息(metadata 约 400 MB,单条最大 4 MB),2.4 万条活动记录。新增 npm run perf:write(build/run-write-perf.cjs,按真实数据中最忙一秒的速率 270 条/秒回放真实事件),perf:startup 新增 real 变体(GB_PERF_STARTUP_REAL_DATA,每次启动用新副本,外网请求全部拦截)。

测到的问题:

  1. 每次写库都按 synchronous = FULL 同步落盘,单次约 2.4 ms,与写入内容无关:子任务进度、工具、文本批、会话保存、活动记录都一样。
  2. SQLite 自动 checkpoint 发生在越过 1,000 页的那次提交里:大库上每隔几百次写入就有一次卡住 Main 100–350 ms(3 轮里 18 次超过 50 ms)。
  3. 启动时两条恢复语句、一条图片操作恢复语句各扫描全部消息的 metadata:在 Main 上同步约 2.3 秒,启动到输入框可用 3.7 秒。

改动:

  • 频繁的本地写入改用 synchronous = NORMAL:本地运行事件(生命周期事件 status/done/error 除外)、渲染进程 500 ms 一次的会话保存、活动记录增量。WAL 模式下仍是原子提交,应用崩溃不丢;断电最多丢最近一段这类写入,任何 FULL 提交及其之前的数据不受影响。远程事件(Agent ACK 依赖其持久性)、通道投递、任务状态等其余写入仍为 FULL。
  • WAL checkpoint 移到 worker 线程:复用只读查询 worker 的入口,每 250 ms 做一次 PASSIVE checkpoint;运行期间 Main 的自动 checkpoint 阈值提到 4,000 页作为兜底(持续写入时被动 checkpoint 追不上 WAL 重置点),worker 退出即恢复默认 1,000。
  • 启动恢复按索引读候选消息:第 58 版 schema 新增部分索引 messages_recovery_idx(流式中,或 metadata 含未结束的工具、子任务、图片操作状态);没有用户队列项时跳过按 queueItemId 的全表删除。metadata 只由 JSON.stringify 写入,状态总以紧凑形式出现,精确判断仍在候选行上用原逻辑执行。一次性迁移在升级 worker 中执行,真实数据约 2 秒。

结果(真实数据):

指标 之前 之后
单次写库 p50(子任务 / 工具 / 文本 / 会话保存 / 活动记录) 2.2–2.6 ms 0.1–0.3 ms
写入期间 Main 最长阻塞(270 条/秒,3 轮) 8–346 ms,超过 50 ms 共 37 次 2–34 ms,超过 50 ms 0 次
启动:Main 上的初始化(assistant-db-init) 2,564 ms 232 ms
启动:到输入框可用(中位数,4 次) 3,702 ms 1,359 ms
启动:侧栏出现会话 4,081 ms 1,749 ms
启动期间 Main 最长阻塞 2.5–2.8 s 236–257 ms

部分索引的代价:metadata 更新时如果新旧值都命中候选条件,要多维护一次索引(4 MB metadata 的更新 36 → 59 ms,233 KB 的 0.7 → 1.6 ms);不命中时没有额外开销。启动时剩下的约 0.6 s 在本地工具环境探测(已在后台初始化,不阻塞 Main)。

未做:读取最大的会话(407 条消息、42 MB metadata)在 worker 上约 170 ms,不阻塞 Main,但切换到这个会话时要等这么久;需要按需加载 metadata,单独立项。

7.3 体验倒退复查(2026-10-03)

逐一复查了所有性能提交对交互体验的影响,并新增原则 P10“体验不倒退”。发现并修复:

问题 来源 修复 验证
“到底部”按钮的平滑滚动动画消失,直接跳到底部 fdf7d83 消息窗口化:贴底跟随、ResizeObserver、锚定修正都会瞬间写入 scrollTop,打断平滑滚动;点击时还立即切换窗口导致可见行被卸载 滑动期间暂停所有滚动写入,只随视口重新窗口化;到达底部、scrollend 或 1.5 秒兜底时再贴底;用户滚轮、按键或点击会接管滚动 真实 Electron 逐帧采样,要求有中间帧(旧代码中间帧为 0),连跑 3 次通过;新增单元测试,旧代码失败
消息滚出可见区后丢失状态:待回答问题里已选的选项和输入的内容、展开的工具详情和推理、Mermaid 重新绘制并显示“加载中” fdf7d83 有待回答问题或待审批的行、用户交互过的最近 8 行、选区两端所在的行保持挂载;Mermaid 按“主题 + 源码”缓存渲染结果(最多 64 个) 单元测试,旧代码失败
跨多条消息选择文本时,选区起点被卸载;复制时只拿到已挂载的文字 fdf7d83 选区两端所在的行保持挂载;复制时,未挂载的消息用原文补全 单元测试
侧栏在上方插入、删除、置顶或 5 秒重排时,可见的会话行跳动 663aa85,窗口化关闭了原生滚动锚定 会话 id 变化时,在绘制前按第一条可见行恢复位置;列表在顶部时不锚定 单元测试,旧代码失败
笔记、文件树、知识库文档表格的锚定修正可能和浏览器原生锚定冲突 b2eb86a 窗口化状态下关闭 overflow-anchor 单元测试
终端打字回显多等一帧;点击“清屏”前到达的输出在清屏之后才显示 f4458af 空闲时立即写入,只合并同一帧内后续到达的输出;清屏前先写完待写入的输出 单元测试
远程 Runtime 流式输出最多 50 ms 一批,显示一顿一顿 d859cbf 合并间隔改为 16 ms;写库成功后立即转发,仍然是先写库再转发 单元测试
流式输出时从侧栏复制或导出会话,可能缺最后不超过 250 ms 的文字 PERF-13 流式缓冲 复制、导出前先把待同步的流式文字刷进状态 单元测试,旧代码失败

检查后没有问题:CSS 动画和过渡没有被删改;其余 scrollIntoView 的行为和原来一致;标题跑马灯、侧栏 5 秒重排的悬停冻结、重命名和菜单行保持挂载;输入框自动增高;Markdown 和 KaTeX 渲染结果。快速大跨度跳转是否出现空白帧也做了实测:在 perf:app 滚动场景中加了检查,CPU 降速 4 倍时 10 次跳转中空白帧为 0,所以没有改动。

已知限制(不是倒退,原来也没有):读屏软件的浏览模式访问不到未挂载的消息;应用本身没有页内查找功能。

修复记录:文件树窗口化最初以 .assistant-sidebar__body 为滚动容器,但真实布局中滚动的是停靠的 .workspace-files__files-view,真实 Electron 测试发现后改为优先使用后者。

PERF-14 状态

  • 优先级 / 状态: P2 / 进行中(消息时间线和侧栏已窗口化;页内查找等限制未解决)
  • 实施条件: PERF-13 完成会话与消息迁移,行组件 props 已稳定。
  • 范围: 消息时间线和会话侧栏优先;保留滚动位置、跳转、展开状态和“加载更早消息”语义。 替换新面板分两步渲染(09c415b);侧栏 5 秒重排状态随侧栏迁出 App。
  • 验收: 5,000 条消息和 1,000 个会话场景下 DOM 节点数有界,滚动与切换指标改善。

PERF-15 存储接口异步化

  • 优先级 / 状态: P1 / 进行中(热点读走 worker;高频写降为 NORMAL、checkpoint 移出 Main、启动恢复按索引,见 7.2.2;接口异步化未开始)
  • 范围: 在原进程内把 AssistantDatabase、KnowledgeDatabase 等对外接口改为异步, 调用方全部 await;会话列表只返回摘要,搜索下推到 SQL,活动记录改为增量追加。 远程 Runtime 实时事件改用已有的 appendRemoteTaskEventsBatch 批量写入,不再逐条同步事务。 Story Graph 检索先补测耗时,扫描规模设上限,匹配尽量下推到 SQL。
  • 验收: 行为与测试不变;PERF-11 Main event-loop 延迟不变差。

PERF-16 数据进程

  • 优先级 / 状态: P1 / 待开始
  • 实施条件: PERF-15 完成,调用方已全部异步。
  • 范围: 新增独占 SQLite 的 utilityProcess,承接会话、知识库、向量检索、Story Graph (含 Agent 主动调用的检索)、远程 Runtime 事件持久化、活动记录及文档解析;Main 只转发; 保证同一时刻只有一个写入方。吸收 PERF-06 的范围。
  • 验收: 大知识库检索、会话列表和文档导入期间 Main event-loop 延迟达到批次目标;取消、 崩溃恢复和迁移语义不回退。

PERF-17 架构守护

  • 优先级 / 状态: P2 / 待开始
  • 范围: lint 禁止 src/main 使用同步文件 API 和在数据进程外引用 DatabaseSync;禁止 组件直接订阅 window.goodbuddy.*.on*;限制 App.tsx 规模;PERF-11 关键指标设置回归阈值。
  • 验收: 违反规则的改动在 lint 或基准阶段失败。

8. 体验与可访问性

UX-01 统一 Ask/Execute 文案

  • 优先级 / 状态: P1 / 已完成
  • 范围:
    • 删除“Execute 高风险操作仍需审批”的冲突说明。
    • 统一表达为使用当前账号完整权限执行,调用记录到活动。
    • 同步定时任务、Composer、Runtime、帮助文案及中英文资源。
  • 验收: 所有入口对 Ask/Execute 的描述与真实 Runtime 契约一致。

UX-02 Runtime 显式状态

  • 优先级 / 状态: P1 / 已完成
  • 范围: 顶栏以状态点和 Runtime/模型名称紧凑表达状态:就绪为绿色,连接中为黄色, 不可用为灰色;不重复显示状态文字,并通过 aria-describedby 暴露完整原因。
  • 验收: 三种状态均不重复显示状态文字;发送不可用原因仍由发送控件的可访问说明提供。

UX-03 等待审批实时通知

  • 优先级 / 状态: P1 / 已完成
  • 范围: 审批卡首次出现时用 polite live region 播报等待状态和工具名;不抢焦点。
  • 验收: 屏幕阅读器能发现新审批,右侧栏数量变化也有可访问状态。

UX-04 长任务阶段播报

  • 优先级 / 状态: P1 / 已完成
  • 范围: 低频阶段使用 role="status",失败终态使用 role="alert",不逐字播报正文。
  • 验收: 准备、等待、完成和失败可被辅助技术理解,流式输出不会形成播报风暴。

UX-05 Composer 错误关联

  • 优先级 / 状态: P1 / 已完成
  • 范围: 附件和上下文错误使用稳定 ID、role="alert"、aria-describedby 和适当的 aria-invalid。
  • 验收: 键盘和屏幕阅读器用户能知道发送失败原因及对应操作区域。

UX-06 设置离开确认焦点

  • 优先级 / 状态: P1 / 已完成
  • 范围: 确认出现时滚动到可见区域并聚焦“继续编辑”,或改用共享确认对话框。
  • 验收: 在长设置页底部切换分类、关闭设置和应用导航时均不会出现不可见确认。

UX-07 SSH Host 读取失败状态

  • 优先级 / 状态: P1 / 已完成
  • 范围: 读取失败与真实空列表分开,失败状态提供重新读取。
  • 验收: 暂时读取失败不会诱导用户重复添加 Host,重试成功后恢复原列表。

UX-08 工作区预览重试

  • 优先级 / 状态: P2 / 已完成
  • 范围: 加载使用 role="status" 和 aria-busy;失败保留路径并提供重新读取。
  • 验收: 网络抖动后无需返回目录重新查找文件即可恢复预览。

UX-09 最近对话空状态

  • 优先级 / 状态: P2 / 已完成
  • 范围: 区分首次为空、搜索无结果和加载失败,分别提供新建、清除搜索和重试。
  • 验收: 三种状态文案和操作互不混用。

UX-10 共享加载状态语义

  • 优先级 / 状态: P2 / 已完成
  • 范围: 为共享 EmptyState 增加 loading 变体,并迁移知识库、智能心跳和 SSH Host。
  • 验收: 加载状态统一输出 role="status"、aria-live="polite" 和 aria-busy="true"。

UX-11 更新检查错误反馈

  • 优先级 / 状态: P1 / 已完成
  • 范围:
    • 启动检查失败进入去重后的全局错误通知。
    • 保留来源、网络和有界 HTTP 错误上下文。
    • 打开下载页失败也显示通知。
  • 验收: 不再以空 catch 静默丢失用户可处理的更新错误。

UX-12 更新源与自动检查解耦

  • 优先级 / 状态: P2 / 已完成
  • 范围:
    • 关闭启动检查后仍可选择 GitHub 或镜像。
    • 手动检查和下载继续使用所选来源。
    • 同步修订 UI-DESIGN.md 当前相反的规则。
  • 验收: “关闭自动检查、切换镜像、手动检查”流程通过测试。

9. 诊断与质量

OPS-01 桌面端有界诊断日志

  • 优先级 / 状态: P1 / 已完成
  • 范围: 轮转记录最近启动和 Runtime 失败的时间、组件、阶段和稳定错误码。
  • 限制: 不记录 Prompt、凭据、文档正文或 Provider 原始响应。
  • 验收: 重启后仍可定位最近一次人为制造的数据库或 Runtime 启动失败。

OPS-02 远端 Agent 有界诊断日志

  • 优先级 / 状态: P1 / 已完成
  • 范围: Detached Agent 保存有界轮转诊断,覆盖 Runtime 启动、退出、连接和恢复阶段。
  • 验收: 人为制造远端失败后可导出根因阶段;关闭和轮转不会遗留无界文件。
  • 限制: 不记录 accepted Prompt、模型密钥或完整模型配置。

OPS-03 反馈可选附加诊断

  • 优先级 / 状态: P1 / 已完成
  • 范围: 用户主动勾选后才附加诊断,提交前说明类别,保留现有截图和草稿重试。
  • 验收: 默认反馈不附加诊断;勾选后只包含允许的有界记录。
  • 依赖: OPS-01;本批明确不自动读取或附加 OPS-02 的远端 Agent 记录, 远端记录通过 Agent 固定 CLI 独立导出。

QA-01 真实 Runtime E2E

  • 优先级 / 状态: P1 / 待开始
  • 范围:
    • 直连模型一次有界真实请求。
    • Bundled OpenCode 启动并执行文件任务。
    • Continue 启动并完成一次任务。
    • MCP 工具写入生产路径。
  • 验收: 受保护发布候选任务运行通过,并记录精确模型调用次数和费用边界。

QA-02 本地兼容 Provider E2E

  • 优先级 / 状态: P1 / 待开始
  • 目标: 常规 CI 无需公网或真实模型凭据也能覆盖打包 Runtime 启动和工具路径。
  • 范围: 提供最小本地 OpenAI 兼容测试 Provider,覆盖流式响应和工具调用。
  • 验收: SDK、Runtime 二进制、Provider 适配或工具写入回归会使常规 CI 失败。

QA-03 Windows ARM64 覆盖纠正

  • 优先级 / 状态: P1 / 待开始
  • 范围:
    • 文档明确 Windows ARM64 当前为交叉打包。
    • 在真实设备验证 Electron、node-pty、Koffi、Canvas、OCR 和 Runtime 加载。
    • 有可用 ARM64 Runner 后再声明原生 CI。
  • 验收: 文档和发布声明不再把文件架构验证描述为原生运行验证。
  • 外部依赖: 真实 Windows ARM64 设备或 Runner。

10. 每批完成标准

一个事项只有同时满足以下条件才可标记为“已完成”:

  1. 正常 UI 操作到达生产实现并产生预期结果,不以 mock、注入 fake 或禁用入口代替。

  2. 已覆盖本事项声明的可达失败场景,用户输入和可恢复上下文不会被静默丢弃。

  3. 新增或更新聚焦测试;功能变化后运行:

    npm test
    npm run typecheck
    npm run lint
    
  4. 生产构建或发布脚本变化后运行 npm run build。

  5. 改动涉及 GoodBuddy Agent 或 Desktop-to-Agent 生产路径时,按 AGENTS.md 在共享 Linux x64 Test Host 验证当前源码或候选包;Host 不可达时明确记录阻塞,不能标记完成。

  6. 检查并更新受影响的 PRD、架构、设计、功能矩阵、构建和运维文档。

  7. 在批次记录中填写实际验证命令、结果、剩余限制和对应提交。

11. 批次记录模板

开始一个批次时,在本节末尾复制以下模板:

### 批次 N:标题

- 状态:进行中
- 范围:ITEM-01、ITEM-02
- 明确不包含:
- 开始日期:
- 完成日期:
- 外部依赖:

#### 验收结果

- [ ] 正常生产路径
- [ ] 声明的失败与恢复路径
- [ ] 聚焦测试
- [ ] npm test
- [ ] npm run typecheck
- [ ] npm run lint
- [ ] npm run build(如适用)
- [ ] 真实 Host 或目标平台验证(如适用)
- [ ] 相关文档更新

#### 结果与剩余限制

- 提交:
- 验证:
- 剩余限制:

批次 1A:体验一致性与 P1 诊断

  • 状态:已完成
  • 范围:UX-01 至 UX-12、OPS-01 至 OPS-03
  • 明确不包含:REL-*、FUN-*、PERF-*、QA-*,以及初始 Renderer Bundle 优化
  • 开始日期:2026-08-31
  • 完成日期:2026-08-31
  • 外部依赖:无

验收结果

  • 正常生产路径
  • 声明的失败与恢复路径
  • 聚焦测试
  • npm test
  • npm run typecheck
  • npm run lint
  • npm run build
  • 共享 Linux x64 Host 的当前源码 Agent 候选验证
  • 相关文档更新

结果与剩余限制

  • 提交:尚未提交
  • 验证:306 个测试文件、3322 项测试通过,49 项按既有条件跳过;Node/Web TypeScript、 完整 ESLint、生产构建和 Renderer Bundle 预算均通过。共享 Linux x64 Host 使用隔离 installation 完成 bootstrap、attach、断开重连、controller resume、Runtime 进程启动、 CLI 诊断导出、重启读取、权限和诊断写失败隔离;真实文本模型调用 0 次。
  • 剩余限制:真实 Host 本次没有通过 Agent-owned model bridge 执行 Prompt,因此未在该 Host 上覆盖 Prompt 期间的 runtime.starting/runtime.started 诊断;三文件真实轮转仍由 聚焦测试覆盖。正式发布、功能增强、性能基线和 QA 路线均不属于本批。