收口 MNote P0 P1 P2 审查尾项

- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
This commit is contained in:
lix-2026
2026-06-01 09:29:12 +08:00
parent 49a0545148
commit 1882db7681
143 changed files with 29810 additions and 3228 deletions
@@ -0,0 +1,69 @@
# 7-45 ONLYOFFICE live bridge session 隔离风险
## 状态
- 状态:done
- Owner07-ai / OnlyOffice live bridge / mnote-web
- 发现时间:2026-05-31
## 现象
ONLYOFFICE live bridge session 当前是进程内全局状态。工具调用未显式传 `onlyofficeSessionId` / `bridgeSessionId` 时,会 fallback 到最近活跃 session;同一文档的 bridge session id 又由 `docKey` 派生,重复打开同文档或多用户/多 tab 打开不同 Office 文档时,存在命令落到错误 Office tab 或 token 覆盖的风险。
## 证据
- `rust/crates/mnote-web/src/routes/onlyoffice.rs` 生成 `bridgeSessionId = "mnote-oo-" + fileState.docKey`
- `rust/crates/mnote-web/src/routes/onlyoffice_bridge.rs``register_session``session_id` 覆盖 `token``document_id``asset_id``last_seen_millis`
- `rust/crates/mnote-web/src/routes/onlyoffice_bridge.rs``current_session_info()` 直接取全局 `last_seen_millis` 最大值。
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs``resolve_session_id()` 在未传 session id 时 fallback 到 `current_session_info()`
## 影响
- Page AI 或 Hermes tool 可能在多 Office tab 场景写错目标文档。
- 同一文档重复打开可能互相覆盖 bridge token,使旧 tab 的插件长轮询或结果回传失效。
- 多用户共享同一 mnote-web 进程时,全局最近 session 可能跨用户泄漏目标选择。
## 最小复现建议
1. 打开两个不同 Office 文档,或同一文档两个 tab。
2. 不传 `onlyofficeSessionId` 调用 `mnote.onlyoffice.session.current` 与一个写工具,例如 `mnote.onlyoffice.document.insert_text`
3. 观察返回 session 是否只由最近活跃 tab 决定。
4. 同一文档双 tab 复测 token 覆盖后,旧 tab `commands/next``results` 是否返回 401 / 无结果。
## 修复建议
- bridge session id 加入 browser tab 级随机后缀,不只使用 `docKey`
- Page AI run payload 必须携带当前 resource tab 的 explicit `onlyofficeSessionId`;写工具禁止默认 fallback 写入。
- `current_session` 只能作为只读诊断工具,不能作为写工具默认目标。
- session state 至少按 actor/session/document/resource 维度过滤。
## 本轮进展
- 2026-05-31
- `onlyoffice.rs` 已把 `bridgeSessionId` 从仅基于 `docKey` 改为 `docKey + bridgeSessionSalt`,避免同文档多 tab 共用同一个 bridge session id。
- `onlyoffice_live.rs` 已禁止读/写 action fallback 到全局最近 session;读写工具必须显式传 `onlyofficeSessionId` / `bridgeSessionId`
- `onlyoffice.rs` 的本地 callback 已绑定 bridge `sessionId` + `token`,未认证本地写回返回 401 并保持原文件不变。
- 已补 Rust 定点测试覆盖缺 explicit session 的读/写工具拒绝,以及未认证 local callback 拒绝。
- 已补 `scripts/task515-onlyoffice-live-scope-http-smoke.js` 覆盖 HTTP 层缺 explicit session 返回 400。
- 已补并通过 `scripts/task516-onlyoffice-bridge-multisession-browser-smoke.js`,在真实浏览器中验证 A/B 两个 bridge session 的 token 校验、command queue 和 result 回收互不串台,错误 token 返回 401。
- 2026-06-01
- 已补并通过 `scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js`,在真实 ONLYOFFICE iframe / DocumentServer 下打开同一 docx 两个 tab 和另一个 docx,验证同文档双 tab 共享 `docKey` 但使用不同 `bridgeSessionId`,三个插件 session 均能注册并各自回收 `selection.get` command。
- `task518` 同时覆盖显式传 B session 但 scope=A 时返回 403,以及 scope=B 授权 dry-run 返回 200,证明真实 iframe session 能被工具层按显式 session 和 resource scope 约束。
- Page AI open editors snapshot / target package 已开始保留 Office `onlyofficeSessionId` / `bridgeSessionId`,服务端 `agentTargetPackage``aiAccessScope.allowedResourceIds` 已能保留 Office asset、session 和 `resource:onlyoffice:{documentId}:{assetId}` 候选;新增 Rust 定点测试 `hermes_client_run_body_preserves_onlyoffice_target_scope` 覆盖该合同。
- ONLYOFFICE iframe 在设置 `__MNOTE_ONLYOFFICE_DEBUG__` 后会向父窗口发送 `mnote:onlyoffice-bridge-ready`resource tab runtime 收到后事件驱动刷新 open editors snapshotPage AI 发送前也会拒绝没有 `onlyofficeSessionId` 的 Office target,避免 bridge 未就绪时发起 run。
- `task518` 已扩展并通过非 dry-run 写入落点验证:通过 `mnote.onlyoffice.document.insert_text` 向 Office B 写入唯一 marker,再通过真实 iframe bridge `document.export` 导出 A/B 内容,断言 B 包含 marker 且 A 不包含;截图保存到 `tmp/task518-onlyoffice-real-iframe-session-scope-smoke/screenshots/office-a-after-write.png``office-b-after-write.png`
- 已补并通过 `scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`:在真实文档页内打开 Office resource tab,等待 iframe bridge ready 后通过 Page AI target picker 选择 Office target,断言 `editorTarget``targetPackage` 均冻结真实 `onlyofficeSessionId` / `bridgeSessionId`,并断言无 `consoleErrors` / `networkFailures` / `httpErrors`
当前 P0 安全阻断已覆盖“工具不能隐式读写全局最近 session”“callback 不能未认证写本地文件”“bridge session/token/queue 在浏览器 HTTP 层隔离”“真实 iframe 插件 session 显式授权边界”“Page AI run scope 不再被服务端压回当前页面 id”“Office bridge 未就绪时 Page AI 发送前拒绝”“真实 iframe 非 dry-run 写入不串台”和“真实 Page AI UI 从 Office iframe 目标取到 live `bridgeSessionId` 并冻结进 run payload”。
## 验收
- [x] Rust 单测覆盖页面生成同 docKey 双 session 随机 session id。
- [x] Rust 单测覆盖读/写工具缺 explicit session id 时拒绝。
- [x] Rust 单测覆盖本地 callback 缺 bridge session/token 时拒绝。
- [x] Browser smoke 覆盖两个 bridge session 的 token、command queue 与 result 隔离。
- [x] Browser smoke 覆盖两个真实 Office iframe tab 下插件注册、sessionId 隔离和 command 回收。
- [x] Rust 单测覆盖 Page AI Office target package / aiAccessScope 保留 session 与 resource scope。
- [x] 前端事件驱动刷新覆盖 ONLYOFFICE bridge ready 消息,发送前拒绝缺 session 的 Office target。
- [x] Browser smoke 覆盖两个真实 Office iframe tab 下非 dry-run 写入目标不串台。
- [x] Browser smoke 覆盖 Page AI target picker 从真实 Office iframe target 读取 live `onlyofficeSessionId` 并写入 run payload。
@@ -0,0 +1,66 @@
# 7-46 ONLYOFFICE live tool 缺少 resource scope 绑定
## 状态
- 状态:done
- Owner07-ai / Hermes tools / OnlyOffice live bridge
- 发现时间:2026-05-31
## 现象
ONLYOFFICE live 写工具当前主要检查 `idempotencyKey``dryRun``aiAccessScope.permissionLevel` 和 command context 写权限,但没有把 bridge session 的 `documentId` / `assetId``aiAccessScope.allowedResourceIds` 绑定。调用方如果显式传入另一个已注册 Office session,存在越过当前 target resource scope 写入非授权资源的风险。
## 证据
- `rust/crates/mnote-web/src/routes/hermes_tools.rs` 注册了多组 `mnote.onlyoffice.*` 读写工具。
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs``run_write_action()` 调用 `ensure_write_authorized()` 后立即解析 session 并执行 bridge command。
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs``resolve_session_id()` 只解析显式 session 或全局 current session,没有验证该 session 对应的 resource 是否在 `allowedResourceIds` 内。
- 对照 `rust/crates/mnote-web/src/hermes_tools/resource.rs`mindmap/resource 工具已有 `ensure_resource_scope_allowed()` 校验 `allowedResourceIds` / `objectIdentity`
## 影响
- Page AI 当前 target 是资源 A 时,模型或恶意调用可传入资源 B 的 `bridgeSessionId`,尝试写入 B。
- 写入审计会显示工具有写权限,但缺少“写的是哪个 resource、是否被 allowedResourceIds 授权”的闭环。
- 多 tab session fallback 与本 bug 叠加时,错误写入更难被用户发现。
## 最小复现建议
1. 构造 `aiAccessScope.allowedResourceIds = [A]`
2. 注册两个 ONLYOFFICE bridge sessionA 和 B。
3. 调用 `mnote.onlyoffice.sheet.set_value``mnote.onlyoffice.document.insert_text`,显式传 B 的 `bridgeSessionId`
4. 期望:返回 403;当前风险:只要通用写权限通过就可能执行。
## 修复建议
- `BridgeSessionInfo` 暴露稳定 `documentId``assetId``objectIdentity`
- ONLYOFFICE live 工具增加与 resource 工具等价的 scope 校验。
- 写工具返回 receipt 时包含 `resourceKind=office``documentId``assetId``onlyofficeSessionId` 与 permission decision。
- dry-run 也必须执行 scope 校验,不能只返回 wouldWrite。
## 本轮进展
- 2026-05-31
- `onlyoffice_live.rs` 已在读/写 action 执行前校验 explicit session 对应的 `sessionId` / `documentId` / `assetId` / `resource:office:{documentId}:{assetId}` 是否包含在 `aiAccessScope.allowedResourceIds`
- 缺失 `aiAccessScope` 或空 `allowedResourceIds` 现在返回 403,不再兼容放行;dry-run 同样执行该 scope 校验。
- `manifest.rs` 已为 OnlyOffice live 工具声明 `aiAccessScope.allowedResourceIds``onlyofficeSessionId` / `bridgeSessionId``anyOf` 合同。
- 已补 Rust 定点测试覆盖 `allowedResourceIds=[asset_a]` 时禁止写入 session `asset_b``allowedResourceIds=[asset_allowed]` 时允许生成 dry-run plan,缺 scope 时返回 403,以及 manifest 合同。
- 已补并通过 `scripts/task515-onlyoffice-live-scope-http-smoke.js`HTTP 层覆盖缺 explicit session 400、scope 不匹配 403、缺 scope 403、授权 scope dry-run 200。
- 2026-06-01
- resource scope candidate 已同时接受 `resource:office:{documentId}:{assetId}` 与 FileTree / resource 对象侧使用的 `resource:onlyoffice:{documentId}:{assetId}`,避免真实 Page AI target 使用 OnlyOffice object identity 时被误拒。
- 已补并通过 `scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js`:真实 ONLYOFFICE iframe / DocumentServer 下,显式传入 Office B 的 `bridgeSessionId``aiAccessScope.allowedResourceIds=[Office A]` 时返回 403;授权 B 的 `assetId` / `resource:onlyoffice:{documentId}:{assetId}` 时 dry-run 返回 200。
- `document-resource-tab-runtime.js` / `sidebar-page-ai-runtime.js` / `hermes_client.rs` 已补最小 target scope 链路:Office resource target 可把 `onlyofficeSessionId` 写入 target package,服务端 sanitize 不再丢弃 `primaryTargetId``targets[]``assetId``onlyofficeSessionId`,本地 run instructions 的 `aiAccessScope.allowedResourceIds` 会包含 Office asset、session 和 `resource:onlyoffice:{documentId}:{assetId}`。新增 `hermes_client_run_body_preserves_onlyoffice_target_scope` 证明服务端不再把 Office target scope 降级为当前页面 id。
- 本地 agent instructions 已明确要求从 `agentRunEnvelope.targetPackage.onlyofficeSessionId` 或对应 target 取值传给 `mnote.onlyoffice.*` 工具,不允许 fallback 到最近活跃 Office session。
- `task518` 已扩展并通过授权 B session 的非 dry-run 写入验证:写入后 `document.export` 证明 B 包含唯一 marker,A 不包含,补齐真实 iframe 层写入落点证据。
- 已补并通过 `scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`:真实 Page AI UI 选择 Office resource target 后,run payload 中 `editorTarget.onlyofficeSessionId``targetPackage.onlyofficeSessionId``targetPackage.targets[0].onlyofficeSessionId` 均等于 iframe live bridge session`allowedFiles` 只包含选中 Office 文件路径,且无 buffer-state 404 / console error。
当前 `task518` 已覆盖真实 iframe session + 工具层 resource scope + 非 dry-run 写入落点,`task523` 补齐真实 Page AI UI target picker 到 live Office session 的 run payload 绑定;组合后覆盖“UI 只授权选中 Office target,工具层拒绝未授权 session”的端到端安全边界。
## 验收
- [x] Rust 单测覆盖 allowedResourceIds 不包含 session resource 时写工具返回 403。
- [x] Rust 单测覆盖 allowedResourceIds 包含 `assetId``objectIdentity` 时允许执行。
- [x] Rust 单测覆盖缺失 / 空 resource scope 时拒绝。
- [x] HTTP smoke 覆盖工具层 session/scope 边界。
- [x] Browser smoke 覆盖真实 ONLYOFFICE iframe session 的工具层 A/B resource scope 边界。
- [x] Rust 单测覆盖 Page AI Office target package 生成的 aiAccessScope 包含 Office asset/session/object identity。
- [x] Browser smoke 覆盖真实 Page AI UI 选择 Office target 后只把该 Office 的 live session 与 relativePath 冻结进 run payload;工具层 A/B session 越权拒写由 `task518` 覆盖。
@@ -0,0 +1,43 @@
# 7-47 local-first Page AI 写入入口口径漂移
## 状态
- 状态:done
- Owner07-ai / Page AI / design governance
- 发现时间:2026-05-31
## 现象
`design/10-review/done/11-current-full-architecture-review-v1.md` 保留了“`mnote.doc.markdown_edit` 已是简单正文编辑主路径”的历史结论。但当前 AGENTS、ARCHITECTURE、CURRENT_ARCHITECTURE 与 `7-18` 都已经把 local-first 普通 Markdown 编辑主路径改为:定位真实 `.md` 文件,Hermes/Reasonix 在 allowed roots 内使用自身 patch/diff/文件编辑能力写入,再由 watcher / BufferStore / Page Aggregate 同步。旧 review 结论容易误导后续 worker 继续扩 `mnote.doc.markdown_edit`
## 证据
- `design/10-review/done/11-current-full-architecture-review-v1.md` 写明 `mnote.doc.markdown_edit` 是简单正文编辑主路径。
- `AGENTS.md``ARCHITECTURE.md` 当前口径明确:`mnote.doc.markdown_edit` 只作为 cloud / remote agent / compat fallback。
- `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 明确要求 Agent Target Resolver、allowed roots/files、dirty guard 和 agent 原生 patch/diff 主路径。
## 影响
- 后续 Page AI 任务可能绕开 `7-18` 的 target package / allowed roots / dirty conflict 模型。
- AI 写入审计、文件版本冲突和 watcher 同步难以统一。
- `mnote.page.save` / `mnote.doc.markdown_edit` 可能被继续当成 local-first 精确编辑入口扩张。
## 修复建议
-`11-current-full-architecture-review-v1.md` 补充“历史快照,local-first 主路径已被 `7-18` 覆盖”的说明,或在 `7-18` 中添加覆盖旧 review 的显式引用。
- Page AI 新任务默认从 `7-18` 拆 checklist,不从 `11` 的旧 tool 主路径拆实现。
- 对 Hermes tool guidance 增加断言:local-first 普通 Markdown 编辑优先文件引用和 patch/diff,工具写入只作为 fallback。
## 本轮进展
- 2026-05-31:已在 `design/10-review/done/11-current-full-architecture-review-v1.md` 顶部补充历史快照说明,明确当前 local-first 普通 Markdown 编辑主路径以 `AGENTS.md``CURRENT_ARCHITECTURE.md``ARCHITECTURE.md``7-18` 为准。
- 2026-06-01:已补 Page AI run 的 `mnote.agent_target_package.v1` 输入和后端 sanitize / allowedFiles 派生,避免 local-first agent 只拿到目录级 `allowedRoots` 而缺少冻结目标文件。该进展只覆盖运行时输入合同,target chip / picker、真实 agent 写入回收和 dirty conflict 仍按 `7-18` 后续阶段推进。
- 2026-06-01:已检查 Hermes client runtime guidance。local-first 分支已经明确“普通 Markdown 编辑优先使用 agent 原生 patch/diff 写入真实文件”;remote/cloud/compat 分支中 `mnote_doc_markdown_edit` 的描述已补上“远端 / cloud / compat”限定,测试名同步改为 `hermes_client_run_guidance_prefers_markdown_edit_only_for_remote_compat_plain_body_edits`
- 2026-06-01:已给 `design/10-review/done/08``09``10` 补充历史快照说明,避免旧 review 结论继续覆盖 `7-18` 当前口径。
## 验收
- [x] `rg -n "markdown_edit.*主路径|简单正文编辑主路径|应优先调用 mnote_doc_markdown_edit|mnote_doc_markdown_edit for plain" design AGENTS.md ARCHITECTURE.md CURRENT_ARCHITECTURE.md rust/crates/mnote-web/src/routes/hermes_client.rs -g '*.md' -g '*.rs'` 的剩余命中均属于历史快照说明、已退役/降级口径、old 目录或 remote/cloud/compat 分支。
- [x] Page AI task prompt / skill guidance 中明确区分 local-first 主路径与 cloud/remote/compat fallback。
- [x] `cargo test -p mnote-web hermes_client_run_guidance_prefers_markdown_edit_only_for_remote_compat_plain_body_edits -- --test-threads=1`
@@ -0,0 +1,29 @@
# 7-48 Mindmap create_from_outline envelope 单测缺少 embed 边界
## 状态
- 状态:done
- Owner07-ai / Hermes tools / mindmap resource
- 发现时间:2026-05-31
- 修复时间:2026-05-31
## 现象
`cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib hermes_tools_mindmap -- --test-threads=1``hermes_tools_mindmap_create_from_outline_writes_default_envelope` 失败,返回:
```text
mnote_resource_page_not_found: 找不到要绑定 mindmap 的本地 Markdown 页面
```
## 根因
该用例只验证 `mnote.mindmap.create_from_outline` 生成默认 `.mindmap.json` envelope,但 payload 未显式设置 `embedIntoPage`。当前工具合同中 `embedIntoPage` 默认是 `true`,而测试 fixture 没有创建 `README.md`,导致资源文件写入后进入页面 embed 阶段并失败。
## 修复
- 在纯 envelope 写入用例里显式设置 `embedIntoPage: false`
- 保留另一个 `hermes_tools_mindmap_create_from_outline_can_embed_into_local_markdown_page` 用例继续覆盖显式 `embedIntoPage=true` 的页面绑定行为。
## 验证
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib hermes_tools_mindmap -- --test-threads=1`
@@ -0,0 +1,51 @@
# 7-50 ONLYOFFICE session.current 诊断工具缺少 resource scope 过滤
## 状态
- 状态:done
- Owner07-ai / OnlyOffice live bridge / Hermes tools
- 发现时间:2026-06-01
## 现象
`mnote.onlyoffice.session.current` 是诊断工具,不会执行读写动作;但它在未传 `onlyofficeSessionId` / `bridgeSessionId` 时仍会 fallback 到进程内最近活跃 OnlyOffice bridge session,并返回 `sessionId``documentId``assetId``fileType`、pending command/result 计数等元数据。
这与读写工具已经要求 explicit session + `aiAccessScope.allowedResourceIds` 的收口方向不完全一致。若 Page AI 当前 target=A,但进程内最近活跃 Office session 属于 resource=B,模型可能通过 `session.current` 看到 B 的 session 元数据。
## 证据
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs``session_current()` 允许缺 session id 时使用 `current_session_info()`
- `rust/crates/mnote-web/src/routes/onlyoffice_bridge.rs``current_session_info()` 返回全局最近活跃 session。
- Reasonix 只读复核 `reasonix-2026-05-31T16-38-36-472Z-63f3a41c` 判断:这不是写绕过,但属于低级信息泄漏风险;建议 `session_current` 也加入 scope 校验或过滤返回字段。
## 影响
- 不会直接写入或读取文档正文,因此风险低于 `7-45` / `7-46`
- 可能暴露非当前 target resource 的 Office session 元数据,给后续工具调用或模型选择目标带来混淆。
- 与 OnlyOffice live bridge 的最小权限口径不一致。
## 修复建议
- `session_current` 若传 explicit session id,应校验该 session 对应的 `sessionId` / `documentId` / `assetId` / `resource:office:{documentId}:{assetId}` 是否在 `aiAccessScope.allowedResourceIds` 中。
- `session_current` 若未传 explicit session id,不应返回全局最近活跃 session;可以改为返回 `mnote_onlyoffice_session_explicit_required`,或只在 debug/admin 边界允许。
- 如果保留诊断 fallback,至少过滤 `documentId` / `assetId` 等跨 resource 元数据,并在 manifest 标注只用于诊断。
## 本轮处理
- 2026-06-01`mnote.onlyoffice.session.current` 已改为复用 `resolve_explicit_session_id()``ensure_onlyoffice_resource_scope_allowed()`
- 已删除 `onlyoffice_bridge::current_session_info()` 全局最近 session fallback,避免诊断工具继续返回非当前 target 的 session 元数据。
- `manifest.rs``mnote.onlyoffice.session.current` 已复用 OnlyOffice live 工具 schema,要求 `aiAccessScope.allowedResourceIds`,并通过 `anyOf` 要求 `onlyofficeSessionId``bridgeSessionId`
- `scripts/task515-onlyoffice-live-scope-http-smoke.js` 已扩展覆盖 `session.current` 缺 explicit session 返回 400、scope 不匹配返回 403、授权 scope 返回 session 元数据。
## 验收
- [x] Rust 单测覆盖 target=A 的 `aiAccessScope.allowedResourceIds` 不能通过 `session.current` 获取 resource=B 的 session 元数据。
- [x] 缺 explicit session id 的 `session.current` 不再返回全局最近 session。
- [x] `task515` HTTP smoke 覆盖 `session.current` scope 边界。
验证:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib hermes_tools_onlyoffice_session_current -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib hermes_tools_manifest_describes_onlyoffice_live_scope -- --test-threads=1`
- `node --check scripts/task515-onlyoffice-live-scope-http-smoke.js`
- `node scripts/task515-onlyoffice-live-scope-http-smoke.js`
@@ -0,0 +1,57 @@
# 7-51 mindmap apply_ops 结构化写入与可见性缺口
## 状态
- 状态:done,工具层最小结构化写入、刷新后 embedded mindmap id 保真、真实 mindmap resource tab Page AI target 已修复;更丰富 `apply_ops` 指令面扩展另拆 follow-up,不阻塞本 bug 归档
- Owner07-ai / Hermes tools / mindmap resource
- 发现时间:2026-06-01
## 现象
`mnote.mindmap.apply_ops` 的非 dry-run 路径曾不执行结构化写入,而是在完成资源路径和写权限校验后返回 `mnote_resource_native_patch_required`,提示 agent 使用原生 patch 编辑授权文件。这与 `7-42` 第二批目标“把 `apply_ops` 从提示 agent patch 文件收口到结构化写入”不一致。
2026-06-01 本轮已完成工具层最小修复:`dryRun=false` 可执行 `updateText` / `updateNode``insertChild` / `addChild``deleteNode`,写入后返回 `revision``changedFiles``markdownSummary`,并保留 `view`、未知顶层字段和未知 node 字段。当前缺口转为浏览器可见性和 Page AI resource target 集成。
## 证据
- `rust/crates/mnote-web/src/hermes_tools/resource.rs``mindmap_apply_ops()` 已实现最小结构化写入。
- `rust/crates/mnote-web/src/routes/hermes_tools.rs` 已有 `hermes_tools_mindmap_apply_ops_writes_and_preserves_envelope_fields`,覆盖非 dry-run 写入、`view` 和未知字段保留。
- `rust/crates/mnote-web/src/routes/hermes_tools.rs` 已有 `hermes_tools_mindmap_apply_ops_rejects_stale_revision`,覆盖 `expectedRevision` 不匹配拒写。
- `rust/crates/mnote-web/src/routes/hermes_tools.rs` 已有 `hermes_tools_mindmap_apply_ops_shared_read_is_forbidden`,说明 shared/read-only scope 拒写已经有单测证据,不是当前主要缺口。
- `task455``task524``task525` 已覆盖 embedded mindmap 刷新、FileTree / Resource Tab 可见性和真实 Page AI mindmap target/contextRefs。
## 影响
- Agent 已可通过结构化 tool 安全执行最小增删改 mindmap node,不再只能回退到文件 patch。
- `view`、未知字段保留和 revision conflict 已由工具层单测覆盖。
- Page AI mindmap 第二批的最小可用链路已覆盖:tool 结构化写入、embedded mindmap 刷新、FileTree / Resource Tab 可见性,以及真实 Page AI mindmap target/contextRefs。后续缺口转为真实编辑需求下的更丰富 `apply_ops` 指令面。
## 修复建议
- 按真实编辑需求继续扩展 `apply_ops`,例如 `moveNode``insertSiblingAfter``setHyperlink``setRefs``appendNote``patchView`
## 验收
- [x] Rust 单测覆盖 `apply_ops` 非 dry-run 结构化写入。
- [x] Rust 单测覆盖 `view` 和未知字段保留。
- [x] Rust 单测覆盖 revision mismatch 返回 conflict。
- [x] Rust 单测覆盖 shared/read-only scope 下写工具拒绝。
- [x] Browser smoke 覆盖 mindmap 资源在 File Tree / Resource Tab 可见。
- [x] Browser smoke 覆盖真实 mindmap resource tab 注入 Page AI `active_editor` contextRef / `targetPackage`
- [x] `task455-local-folder-mindmap-clean-smoke.js` 刷新后 `mindmapId` 保持新建资源文件名,不退化为 `"mindmap"`
## 本轮验证
- `cargo test -p mnote-web hermes_tools_mindmap_apply_ops -- --test-threads=1`
- `cargo test -p mnote-web mindmap -- --test-threads=1`
- 2026-06-01 复跑失败:`MNOTE_UI_BASE_URL=http://127.0.0.1:3301 node scripts/task455-local-folder-mindmap-clean-smoke.js`
- 失败点:`刷新后 mindmapId 应保持不变`
- 证据:`tmp/task455-local-folder-mindmap-clean-smoke/result.json`
- 关键现象:刷新前 `mindmapId=mindmap-647356316`,刷新后 DOM `mindmapId=mindmap`FileTree 仍有 `local-file:CleanPage/mindmap-647356316.json` 行。
- 2026-06-01 修复后复跑通过:`MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task455-local-folder-mindmap-clean-smoke.js`
- 修复点:`blockDocument.blocks[].attrs` 保留 `mindmapId/sourcePath/rootNodeId`,浏览器 `document-tiptap-conversion-runtime.js` 消费 blockDocument 时读取这些 attrs。
- 补充验证:`cargo test -p bridge-runtime page_aggregate_block_document_preserves_mindmap_projection_attrs -- --test-threads=1``cargo test -p mnote-web local_markdown_generated_mindmap_id_roundtrips_as_mindmap_block -- --test-threads=1`
- 回归验证:`MNOTE_UI_BASE_URL=http://127.0.0.1:3301 node scripts/task524-workspace-object-identity-matrix-smoke.js`
- 2026-06-01 新增并通过:`MNOTE_UI_BASE_URL=http://127.0.0.1:3301 node scripts/task525-page-ai-mindmap-resource-target-smoke.js`
- 覆盖真实 local-folder mindmap resource tab 打开后,Page AI run payload 中 `contextRefs.active_editor.resourceKind=mindmap`
- 覆盖 `targetPackage.primaryTargetId``targetPackage.currentFile``allowedFiles` 指向 `Page/map.mindmap.json`
@@ -0,0 +1,52 @@
# 7-52 Page AI target picker 与 resource target 仍未闭环
## 状态
- 状态:done
- Owner07-ai / Page AI sidebar runtime / Agent Target Resolver
- 发现时间:2026-06-01
## 现象
Page AI 已经有 agent selector、contextRefs popover、`targetPackage``allowedFiles` 基础输入。2026-06-01 已补上 page target、mindmap resource target、OnlyOffice resource target 与 raw local resource target 的 composer chip / picker / payload 冻结闭环;mindmap / raw file `active_editor` contextRef 均已携带 `targetId` / `objectIdentity` / `resourceKind` / `assetId` / `relativePath`。跨 workspace 多 target 的产品化确认继续由 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 跟踪,不再由本文阻塞 target picker 收口。
## 证据
- `rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js` 已有 `currentPageAiOpenEditorsSnapshot()``currentPageAiEditorTarget()`、target chip / picker 和 `pageAiBuildAgentTargetPackage()`
- `scripts/task502-page-ai-agent-selector-context-smoke.js` 已覆盖 page target button / popover / chip,并通过 mock `resourceEditors` 覆盖 mindmap target 选择,断言 run payload 的 `targetPackage.primaryTargetId``targets[]``resourceKind=mindmap``relativePath=maps/Task502.mindmap.json``policy.writeRequiresExplicitTarget=true`;同时断言 `active_editor` contextRef 携带 mindmap `targetId` / `objectIdentity` / `resourceKind` / `assetId` / `relativePath`
- 2026-06-01 验证命令:`MNOTE_UI_BASE_URL=http://127.0.0.1:3301 node scripts/task502-page-ai-agent-selector-context-smoke.js`,截图:`tmp/task502-page-ai-agent-selector-context-smoke/01-agent-selector-context.png`
- `scripts/task453-local-folder-page-ai-changed-files-smoke.js` 覆盖 target snapshot 冻结、跨 workspace 阻断、dirty buffer 阻断,但不是 target picker UI。
- `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 仍把 target chip/picker、跨 workspace 多选确认、真实 agent 写入回收列为未完成。
- OnlyOffice 工具层 session/scope 已有 `task515` - `task518` 覆盖;`task523-page-ai-onlyoffice-real-target-session-smoke.js` 已补真实 Page AI target 到 `onlyofficeSessionId` / `bridgeSessionId` 的 UI 绑定。
- `mnote-mindmap` skill 已注册,mindmap target 的 `active_editor` contextRef 已携带资源身份;`task525-page-ai-mindmap-resource-target-smoke.js` 已补真实 mindmap resource tab target / contextRefs 复测。
- `scripts/task520-page-ai-raw-resource-target-smoke.js` 已补真实 raw local resource tab:打开 `Page/notes.txt` 后 Page AI target chip 指向 `notes.txt`run payload 冻结 `targetPackage.primaryTargetId=resource:file:{rootUri}:Page/notes.txt``allowedFiles=["Page/notes.txt"]`,并断言 `objectIdentity` 不再退化成 `[object Object]`
## 影响
- Page AI UI 已能清楚告诉用户当前 page / mindmap / OnlyOffice target。
- Page AI target picker 已覆盖 page / mindmap / OnlyOffice / raw file 四类当前 P1 目标,不再退化为“当前页”语义。
- 真实 agent 写入回收、跨 workspace 多 target 和 mindmap resource tab 自动注入的更大产品化项继续由 `7-18` / `7-42` / `7-51` 跟踪。
## 下一步建议
- 若继续扩多目标选择,新增跨 workspace 多 target 确认 smoke,并归入 `7-18`
- 若继续扩 mindmap 多目标或更细粒度 contextRefs,另拆 `7-42` / `7-51` follow-up,不再由本文阻塞 target picker 收口。
## 验收
- [x] Page AI composer 可见 page target chip。
- [x] target picker 可在当前 page 与 mindmap resource tab 间切换。
- [x] run payload 冻结 page `targetPackage.targets[0]`
- [x] run payload 冻结 mindmap resource `targetPackage.targets[0]`
- [x] OnlyOffice target 不会把 A 页授权误用于 B session。
- [x] mindmap resource tab 可作为 `resourceKind=mindmap` target。
- [x] mindmap `active_editor` contextRef 携带 `targetId` / `objectIdentity` / `resourceKind` / `assetId`
- [x] raw file target 可从真实 resource tab 候选选择并冻结到 run payload。
- [x] 真实 mindmap resource tab target / contextRefs 已由 `task525` 覆盖。
## 本轮验证
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `node --check scripts/task520-page-ai-raw-resource-target-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 node scripts/task520-page-ai-raw-resource-target-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task502-page-ai-agent-selector-context-smoke.js`
@@ -0,0 +1,134 @@
# 7-53 Page AI runtime 已触发继续拆分阈值
## 状态
- 状态:done
- Owner07-ai / Page AI sidebar runtime
- 发现时间:2026-06-01
## 现象
`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js` 曾约 3458 行,已经超过 `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md` 中设定的 2500 行继续拆分阈值。文件内仍混合 UI render、run orchestration 等多类职责。
同时,`sidebar-tree-runtime.js` 已不再初始化 `pageUiState.pageAi*` 状态,Page AI 默认值由 `sidebar-page-ai-runtime.js``ensurePageAiStateFacade()` 集中兜底;但 tree runtime 仍传入共享 `pageUiState` 对象并保留少量 Page AI 代理函数,后续子模块拆分仍未闭环。
## 证据
- `sidebar-page-ai-runtime.js` 曾约 3458 行;2026-06-01 render helper 续补后降至 2498 行。
- `sidebar-page-ai-markdown-runtime.js` 已承接 Page AI Markdown / conversation 文本渲染 helper。
- `sidebar-page-ai-profile-runtime.js` 已承接 provider/profile/history filter/usage helper。
- `sidebar-page-ai-permission-runtime.js` 已承接 ACP permission message/dialog/resolve helper。
- `sidebar-page-ai-session-runtime.js` 已承接 session storage、backend session list/detail/search/resume/delete 和 runtime event replay helper。
- `sidebar-page-ai-skill-runtime.js` 已承接 skill source、skill preference、Hermes builtin 隐藏和 Reasonix memory preference helper。
- `sidebar-page-ai-target-runtime.js` 已承接 OpenEditorsSnapshot target 派生、WorkspacePath、run target snapshot、contextRefs、agentTargetPackage 和 target writable guard helper。
- `sidebar-page-ai-runtime.js` 包含 `function ensurePageAiStateFacade`,并集中初始化 `pageAiAcpRuntime: 'reasonix'` 等 Page AI 默认状态。
- `sidebar-tree-runtime.js` 不再包含 `pageAiAcpRuntime: 'reasonix'``pageAi*:` 默认字段,只保留 `open-page-ai` 主壳入口和代理函数。
- `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md` 第 3 节仍将以下项列为未完成:
-`sidebar-page-ai-runtime.js` 超过 2500 行,再按 render / conversation / run orchestration 继续拆成子模块。
## 影响
- Page AI 行为继续占用 tree runtime 委托面,tree/filetree 和 AI session owner 边界仍混杂。
- 后续新增 target picker、OnlyOffice target、mindmap contextRefs 时,容易继续堆入单一大文件。
- 浏览器事件委托散落在 tree runtime 与 Page AI runtime 之间,增加回归风险。
## 下一步建议
- 若后续再次超过 2500 行,继续按 conversation / run orchestration 分子模块拆,降低 `sidebar-page-ai-runtime.js` 单文件职责。
- 继续补 Page AI smoke,覆盖停止、关闭、history/session、permission、profile/skill 切换等 owner 迁移风险面;停止/关闭尾项已拆到 `bugs/07-ai/process/7-56-page-ai-runtime-stop-close-smoke-gap-v1.md`
## 验收
- [x] `sidebar-tree-runtime.js` 不再包含 `[data-page-ai-action=...]` click/change 分发。
- [x] `sidebar-page-ai-runtime.js` 导出并安装 `installPageAiDelegates()`
- [x] `pageUiState.pageAi*` 从通用 sidebar state 下沉或有明确兼容 getter/setter。
- [x] Page AI smoke 覆盖打开 drawer、切换 tab、切换 agent和发送;停止/关闭完整断言已拆到 `bugs/07-ai/process/7-56-page-ai-runtime-stop-close-smoke-gap-v1.md`,不阻塞本文拆分阈值归档。
## 2026-06-01 第一刀验证
已完成第一刀:`sidebar-tree-runtime.js` 中 Page AI click/input/change/keydown 的大段 action 分发已迁到 `sidebar-page-ai-runtime.js``handlePageAiClick``handlePageAiKeyDown``handlePageAiInput``handlePageAiChange`,并由 `installPageAiDelegates()` 在 Page AI owner runtime 内安装事件监听。tree runtime 仅保留 `data-mnote-action="open-page-ai"` 主壳入口。
已通过验证:
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_agent_target_picker_contract_is_visible_and_serialized -- --test-threads=1`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task490-runtime-surfaces-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task502-page-ai-agent-selector-context-smoke.js`
2026-06-01 续补:
- `sidebar-page-ai-runtime.js` 新增并导出 `installPageAiDelegates()`,内部一次性安装 Page AI click/keydown/input/change delegate。
- `sidebar-tree-runtime.js` 不再调用 `sidebarPageAi.handlePageAi*`,只在自身监听注册后调用 `sidebarPageAi.installPageAiDelegates()`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证 drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 state facade 续补:
- `sidebar-page-ai-runtime.js` 新增 `ensurePageAiStateFacade()`,集中初始化 Page AI 默认状态并导出该 facade。
- `sidebar-tree-runtime.js``pageUiState` 初始对象删除 `pageAi*:` 默认字段,tree runtime 不再定义 Page AI 状态真相。
- Rust include/assert 已更新为:Page AI runtime 包含 `ensurePageAiStateFacade``pageAiAcpRuntime: 'reasonix'`tree runtime 不包含该默认字段。
2026-06-01 conversation helper 续补:
- 新增 `sidebar-page-ai-markdown-runtime.js`,抽出 `textFromUnknown``renderPageAiMarkdown` 和 inline Markdown 渲染 helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiMarkdownRuntime()`,保留现有 assistant message 渲染行为。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-markdown-runtime.js` 静态 runtime asset,并更新 runtime asset mount 测试。
2026-06-01 profile helper 续补:
- 新增 `sidebar-page-ai-profile-runtime.js`,抽出 provider/profile/chat-only profile/history filter/usage helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiProfileRuntime()` 并保留同名代理常量,降低调用点扰动。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-profile-runtime.js` 静态 runtime asset,并更新 runtime asset mount 测试。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证二级 import 后 drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 permission helper 续补:
- 新增 `sidebar-page-ai-permission-runtime.js`,抽出 ACP permission message、dialog show/hide 和 resolve-permission helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiPermissionRuntime()` 并保留同名代理常量,主 runtime 继续负责事件流持久化、会话同步和 conversation 渲染入口。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-permission-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 permission 二级 import 后 sidebar/Page AI 仍可加载和发送。
2026-06-01 session helper 续补:
- 新增 `sidebar-page-ai-session-runtime.js`,抽出 session storage、backend session list/detail/search/resume/delete、session message sync 和 backend runtime event replay helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiSessionRuntime()` 并保留同名代理常量,主 runtime 继续负责 Page AI UI render、run orchestration 和事件委托入口。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-session-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 session 二级 import 后 Page AI drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 skill helper 续补:
- 新增 `sidebar-page-ai-skill-runtime.js`,抽出 skill source、skill preference、Hermes builtin 隐藏和 Reasonix memory preference helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiSkillRuntime()` 并保留同名代理常量,主 runtime 继续负责 skill 异步加载、target/run orchestration 和 UI render。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-skill-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已通过 `node --check` 覆盖 Page AI 主 runtime、skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch``page_ai_agent_target_picker_contract_is_visible_and_serialized`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 skill 二级 import 后 Page AI drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 target helper 续补:
- 新增 `sidebar-page-ai-target-runtime.js`,抽出 OpenEditorsSnapshot target 派生、WorkspacePath、run target snapshot、contextRefs、agentTargetPackage 和 target writable guard helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiTargetRuntime()` 并保留同名代理常量,主 runtime 继续负责 target popover UI、事件分发和 run orchestration 顺序。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-target-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言;target picker 合同测试改为在 target helper 中断言 `primaryTargetId` / `targets` / `policy`
- 已通过 `node --check` 覆盖 Page AI 主 runtime、target/skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_uses_backend_acp_session_runtime_store``page_ai_agent_target_picker_contract_is_visible_and_serialized``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js``task520-page-ai-raw-resource-target-smoke.js``task525-page-ai-mindmap-resource-target-smoke.js`,验证新增 target 二级 import 后 Page AI drawer、agent/context/target/skills、raw resource target、mindmap target 和发送 payload 链路仍可用。
2026-06-01 render helper 续补:
- 新增 `sidebar-page-ai-render-runtime.js`,抽出 target/context display helper、agent/profile/model label、skill filter、drawer shell、controls render、suggestions render、conversation render 和 response humanize helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiRenderRuntime()`,保留同名转发函数,主 runtime 继续负责 state facade、agent/run orchestration、session/permission/skill/target runtime 接线与事件委托。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-render-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言,避免浏览器 ES module import 404。
- `sidebar-page-ai-runtime.js` 当前 2498 行,已低于 `7-38` 设定的 2500 行继续拆分阈值。
- 已通过 `node --check` 覆盖 Page AI 主 runtime、render/target/skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_uses_backend_acp_session_runtime_store``page_ai_agent_target_picker_contract_is_visible_and_serialized``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch`
最终验证:
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task490-runtime-surfaces-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task502-page-ai-agent-selector-context-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task504-page-ai-history-agent-filter-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task520-page-ai-raw-resource-target-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task525-page-ai-mindmap-resource-target-smoke.js`
本文已满足归档条件:`sidebar-page-ai-runtime.js` 当前 2498 行,低于 2500 行继续拆分阈值;render helper 二级 import、drawer controls、history/session filter、当前页 target、raw resource target、mindmap target 和发送 payload 链路均通过 smoke。
@@ -0,0 +1,33 @@
# 7-54 Page AI Office target 误查 Markdown buffer-state
## 状态
- 状态:done
- Owner07-ai / Page AI target runtime / OnlyOffice resource target
- 发现时间:2026-06-01
- 修复时间:2026-06-01
## 现象
真实 Page AI UI 选择 Office resource target 后,发送 run 前的 buffer guard 会按 Office 文件相对路径请求 `/api/documents/buffer-state`
```text
/api/documents/buffer-state?...&relativePath=Page/office-a.docx
```
该端点只服务 local Markdown 文档 bufferOffice target 会返回 404。虽然当前 404 没有阻断 run,但会污染 console / HTTP error 证据,并说明 Page AI 把 Office resource 当成 Markdown buffer 检查。
## 根因
`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js``fetchPageAiTargetBufferState()` 只判断 `sourceKind=local_folder`,没有区分 target `resourceKind`。Office target 已由 OnlyOffice bridge session、resource scope 和 tool 层权限保护,不应走 Markdown `BufferStore` 查询。
## 修复
- `fetchPageAiTargetBufferState()``office` / `only_office` / `onlyoffice` target 直接返回 `null`
- `scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js` 增加断言:真实 Page AI Office target run 过程中不得出现 `/api/documents/buffer-state` 404,并要求 `consoleErrors` / `networkFailures` / `httpErrors` 为空。
## 验收
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `node --check scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`
@@ -0,0 +1,32 @@
# 7-55 Page AI 当前页目标继承 stale workspacePath
## 状态
- 状态:done
- Owner07-ai / Page AI target runtime
- 发现时间:2026-06-01
- 修复时间:2026-06-01
## 现象
`task504-page-ai-history-agent-filter-smoke.js` 在模拟 stale `OpenEditorsSnapshot` 后,用户偏好已将 `active_editor` contextRef 关闭,只发送当前页上下文;但 `sendPageAiMessage()` 仍强制把 `scopedContext.editorTarget` 覆盖为 `currentPageAiEditorTarget()`,导致当前页请求被旧 active editor 的 `workspaceId` 拦截,前端显示:
`AI target 与当前 workspaceId 不一致,请重新选择当前工作区内的目标。`
## 根因
- `pageAiScopedPageContext()` 已按 contextRefs 计算 scoped target,但发送链路又覆盖为 `currentPageAiEditorTarget()`
- `currentPageAiPageEditorTarget()` 从 page editor snapshot 读取 `workspacePath` 时,没有把当前 `workspaceId/sourceKind/rootUri/relativePath/documentId` 写回,stale snapshot 会把旧 workspace 信息带进当前页目标。
## 修复
- `sendPageAiMessage()` 改用 `currentPageAiScopedEditorTarget()`,遵守 contextRefs 对 active editor 的开关。
- `currentPageAiPageEditorTarget()` 在复用 page editor snapshot 时显式覆盖当前 workspace path 字段,避免 stale snapshot 污染当前页 target。
## 验证
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.js`
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task504-page-ai-history-agent-filter-smoke.js`
验证结果:`task504` 通过,`capturedRuns[0].contextRefs` 只包含 `current_page``runTargetSnapshot.editorTarget.workspaceId` 为当前 `local-ws:mnote-e2e:task504`,未继续使用 stale workspaceId。
@@ -0,0 +1,49 @@
# 7-56 Page AI runtime stop / close smoke 缺口
## 状态
- 状态:done
- Owner07-ai / Page AI runtime
- 发现时间:2026-06-01
## 现象
`7-53` 已完成 Page AI runtime 拆分并把 `sidebar-page-ai-runtime.js` 降到 2500 行阈值以下,但停止运行与关闭抽屉的浏览器断言仍不完整。
## 证据
- `bugs/07-ai/done/7-53-page-ai-runtime-split-threshold-triggered-v1.md` 已记录拆分完成,但验收中仍保留 stop/close smoke 的 `[~]` 项。
- `scripts/task490-runtime-surfaces-smoke.js` 会打开并点击关闭 Page AI 抽屉,但缺少明确的 drawer hidden / closed state 断言。
- 现有 smoke 未覆盖 `data-page-ai-action="stop-run"` 到 abort API / terminal event 的端到端行为。
## 影响
- Page AI runtime 拆分后,基础打开/发送/target picker 已有保护,但 stop/close 这类常用交互仍可能在后续拆分中回退。
- `7-53` 虽然可作为拆分阈值 bug 归档,但停止/关闭行为应继续作为独立 P2 测试缺口跟踪。
## 下一步
1. 扩展现有 `task490` 或新增窄 smoke,断言关闭后 Page AI drawer 进入隐藏状态,且页面根状态同步清除。
2. 增加 stop-run smoke:启动可控流式 run,点击停止,断言 abort API 被调用并收到 terminal/aborted 状态。
3. 将验证命令补入 `scripts/TESTING_REFERENCE.md`
## 验收
- [x] 浏览器 smoke 明确断言 Page AI drawer close 后不可见。
- [x] 浏览器 smoke 覆盖 stop-run -> abort/terminal event。
- [x] `node --check` 与对应 smoke 通过。
## 2026-06-01 修复记录
- `scripts/task490-runtime-surfaces-smoke.js` 已扩展 Page AI close 断言:关闭后 drawer 必须 `hidden=true`,浮动 AI 按钮 `data-state=closed``aria-expanded=false`
- `task490` 已新增可控 mock run:发送后等待 run 进入 `running`,点击 `data-page-ai-action="stop-run"`,断言 abort API 被调用一次、请求 reason 为 `page_ai_user_stop`,并等待 UI 进入 `aborted` 状态。
- 修复 Page AI render helper 拆分漏注入:`sidebar-page-ai-render-runtime.js` 使用 `pageAiProviderLabel()` 渲染 provider card,但 `sidebar-page-ai-runtime.js` 未传入该 helper;已补 context 注入。
- 已通过:
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js`
- `node --check scripts/task490-runtime-surfaces-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task490-runtime-surfaces-smoke.js`
截图:
- `tmp/task490-runtime-surfaces-smoke/02b-page-ai-stopped.png`
@@ -0,0 +1,80 @@
# 7-57 OnlyOffice Plugin Bridge P2 产品化尾项
## 状态
- 状态:done
- Owner07-ai / ONLYOFFICE live bridge / Page AI target runtime
- 发现时间:2026-06-01
- 归档时间:2026-06-01
## 现象
OnlyOffice live bridge 的 P0 安全阻断已完成:显式 session、resource scope、真实 iframe 多 session 隔离、非 dry-run 写入落点和 Page AI target picker 到 live session 的绑定均已有 smoke 证据。但 `7-43` 仍保留若干 P2 产品化尾项,缺少独立 bug 落点。
## 证据
- `design/07-ai/process/7-43-onlyoffice-plugin-bridge-design-v1.md` 仍保留 P2 follow-upOffice 主文档加载噪音治理、第三批 recipe 逐项实测。`session/current``session/close` 与 session state `docKey/pageOrigin` 已在 2026-06-01 续补完成。
- 已有 smoke 证明安全主路径完成:
- `scripts/task515-onlyoffice-live-scope-http-smoke.js`
- `scripts/task516-onlyoffice-bridge-multisession-browser-smoke.js`
- `scripts/task517-onlyoffice-bridge-plugin-direct-smoke.js`
- `scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js`
- `scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`
## 影响
- P0 安全问题已经不再阻塞;P2 recipe 扩展已收窄为 `7-43` 的矩阵化后续,不再作为本文 bug 阻塞项。
- `7-43` 已区分已完成安全项和后续产品化项。
## 下一步
1. 记录并治理 Office 主文档加载噪音,例如 DocumentServer `errorCode=-18`、WebSocket / polling 失败等。(已完成)
2. 第三批 recipe 必须逐项实测后再暴露给 agent,不凭 API 名称直接开放。(已收窄为矩阵规则)
## 验收
- [x] `session/current``session/close` API 已实现并有 Rust route 测试。
- [x] session state 已补 `docKey` / `pageOrigin`,并由 plugin config / register session / list sessions smoke 证明透传。
- [x] Office 加载噪音有明确分类、截图或日志证据,并不误判为打开成功。
- [x] 每个已暴露 recipe 保留 Rust unit、browser direct smoke 和 tool API smoke 要求;未暴露第三批 recipe 已明确不对 agent 宣称可用。
- [x] 插件执行层已补 editorType guard`document.*` / `sheet.*` / `presentation.*` 不能跨 Word / Excel / PPT session 串用。
- [x] `design/07-ai/process/7-43...` 中 P0 已完成项与 P2 尾项拆分清晰。
## 2026-06-01 session lifecycle 续补记录
- `rust/crates/mnote-web/src/routes/onlyoffice_bridge.rs` 已新增 `current_session` / `close_session`,并把 `docKey` / `pageOrigin` 纳入 `BridgeSessionState``BridgeSessionPayload``BridgeSessionInfo` 和 plugin index state。
- `/onlyoffice` 页面会把当前 `fileState.docKey``location.origin` 传给 bridge plugin configmock plugin direct smoke 已验证 plugin 注册后 session 中存在 `docKey/pageOrigin`
- `scripts/task516-onlyoffice-bridge-multisession-browser-smoke.js` 已扩展 `session/current``session/close` 和关闭后 session 不再可取 command 的断言。
- 已通过:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1`
- `node scripts/task517-onlyoffice-bridge-plugin-direct-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3302 node scripts/task516-onlyoffice-bridge-multisession-browser-smoke.js`
## 2026-06-01 噪音分类与主文档失败断言
- `scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js` 已新增 `classifyOnlyOfficeSignals`,把 bridge plugin translation 404、ONLYOFFICE 内置插件噪音和主文档加载失败分开记录。
- smoke 会断言 MNote bridge session 注册与 command loop 可用,同时要求 `mainDocument.ready=true`
- `errorCode=-18` 不再被当作可忽略噪音;一旦在 console 或 editor error log 中出现,`task518` 会失败并把它归为主文档失败。
- `design/07-ai/process/7-43-onlyoffice-plugin-bridge-design-v1.md` 头部口径已声明 P0 session/scope 安全阻断完成,本文和 `7-43` 只继续跟踪 P2 recipe 扩展与产品化尾项。
本文继续保持 `process` 的剩余原因:
- 已归档。第三批 recipeWord 图片/修订/content controls/表格行列样式、Excel 筛选/工作表删除移动、PPT 图片/重排/主题布局/shape 样式位置、PDF/forms)仍需逐项实测后再开放给 agent,但这些是后续 `7-43` 矩阵项,不再作为本文 bug 阻塞。
## 2026-06-01 归档记录
Recipe 产品化矩阵:
| 类别 | 当前状态 | 归档口径 |
| --- | --- | --- |
| Word 基础读写 / 表格 / 评论 | exposed-with-tests | 已暴露,继续要求 Rust unit、browser direct smoke、tool API smoke。 |
| Excel sheets / range / format / dimensions / sort / chart | exposed-with-tests | 已暴露,Excel sort/chart 已纳入当前 capabilities。 |
| PPT slide texts / shapes / replace / delete / table / clear / shape | exposed-with-tests | 已暴露,PPT table/clear/shape 已纳入当前 capabilities。 |
| Word 图片 / 修订 / content controls / 表格增删行列和样式 | not-exposed | 仅作为候选,不向 agent 宣称可用。 |
| Excel 筛选 / 工作表删除移动 | not-exposed | 仅作为候选,不向 agent 宣称可用。 |
| PPT 图片 / slide 重排 / 主题布局 / shape 样式位置 | not-exposed | 仅作为候选,不向 agent 宣称可用。 |
| PDF / forms 字段读取与填写 | blocked-by-api | 需先确认 ONLYOFFICE 社区版 Plugin API 可行性。 |
验证:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1`
@@ -0,0 +1,57 @@
# 7-58 Mindmap P2 apply_ops 与真实 UI smoke 缺口
## 状态
- 状态:done
- Owner07-ai / mindmap resource skill / Page AI target runtime
- 发现时间:2026-06-01
## 现象
Mindmap skill 和资源 target 已有最小闭环:`fetch``create_from_outline``apply_ops` 基础工具、`mnote-mindmap` skill 注册、`task503``task525` smoke 已覆盖 API/target 绑定。但 P2 第二批仍缺 apply_ops 操作白名单文档化、长文本节点策略和真实 UI 交互 smoke。
## 证据
- `rust/crates/mnote-web/src/hermes_tools/resource.rs` 已有 mindmap `fetch/apply_ops/create_from_outline` 入口。
- `scripts/task503-mindmap-skill-capability-smoke.js` 覆盖 skill、create_from_outline、fetch、embed。
- `scripts/task525-page-ai-mindmap-resource-target-smoke.js` 覆盖 mindmap resource tab / Page AI targetPackage。
- 当前缺口未在独立 bugs/process 中跟踪;历史相关 bug `7-48/7-51/7-52` 已作为各自窄问题归档。
## 影响
- agent 可以调用 mindmap 工具,但不清楚 `apply_ops` 的稳定操作白名单和限制。
- 长文本节点可能破坏 mindmap 渲染或 agent 输出可读性。
- 现有 smoke 偏 API/target 合同,未覆盖用户真实 UI 交互,例如节点编辑、拖拽、导出等。
## 下一步
1. 已文档化 `apply_ops` 支持的操作白名单、payload schema、dry-run/revision 规则和失败形态。
2. 已为关键 ops 增加 Rust 单测,覆盖成功、别名、delete、dry-run、revision mismatch、越权/只读拒绝、unsupported op、invalid ops payload、禁止删除 root。
3. 已在 skill prompt 中明确长文本节点压缩策略:短语化、避免长段落,拆成 child nodes。
4. 已用真实 mindmap UI smoke 覆盖打开、插入、渲染、样式抽屉默认状态、slash 菜单层级和 resize 基础交互。
## 验收
- [x] `apply_ops` 白名单与限制写入 design / skill。
- [x] `hermes_tools_mindmap_apply_ops_*` targeted tests 覆盖核心操作。
- [x] 长文本节点策略有测试或明确降级说明。
- [x] 至少一条真实 mindmap UI browser smoke 通过并登记到 `scripts/TESTING_REFERENCE.md`
## 2026-06-01 修复记录
- `skills/mnote-mindmap/SKILL.md` 已新增 `Current apply_ops contract`,明确支持 `updateText/updateNode``insertChild/addChild``deleteNode`,记录 `nodeId/id``text/title``parentId/nodeId` 等 payload schema、未知 op 拒绝、禁止删除 root、长文本短语化/拆子节点、`dryRun` 与 revision checks。
- `rust/crates/mnote-web/src/routes/hermes_tools.rs` 已补齐 apply_ops targeted tests
- `hermes_tools_mindmap_apply_ops_accepts_common_aliases`
- `hermes_tools_mindmap_apply_ops_deletes_child_node`
- `hermes_tools_mindmap_apply_ops_dry_run_returns_diff_without_writing`
- `hermes_tools_mindmap_apply_ops_rejects_invalid_ops_payload`
- `hermes_tools_mindmap_apply_ops_rejects_root_delete`
- `hermes_tools_mindmap_apply_ops_rejects_unsupported_op`
- 已通过:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_mindmap_apply_ops -- --test-threads=1`
- `node --check scripts/task455-local-folder-mindmap-clean-smoke.js`
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3303 node scripts/task455-local-folder-mindmap-clean-smoke.js`
真实 UI smoke 结果:
- `tmp/task455-local-folder-mindmap-clean-smoke/result.json`