feat: EditorRuntimeActor - 三层缓存/delta/事件架构

Phase A — EditorRuntimeActor 内存缓存层
- 新增 editor_actor.rs: EditorBlockDocument 内存态 + apply_command + load_or_init
- block.rs 四个写工具(replace/insert/delete/move)接入 actor 路径
- editor_actor feature flag(MNOTE_WEB_ENABLE_EDITOR_ACTOR=true 默认开启)
- bridge-runtime 三个核心函数公开化
- rust-toolchain: 1.89 → stable(修复 spike WASM 编译阻塞)

Phase B — 编辑器增量 delta channel
- BlockDelta/DeltaOperation 类型 + actor.build_block_delta()
- leptos-tiptap spike: mnote:editor:block-delta CustomEvent 监听 + JSON patch
- DocumentAiAgentPanel: 拦截 blockDelta → window dispatchEvent
- 工具响应含 blockDelta 字段供前端消费

Phase C — 事件 stream delta
- broadcast channel 在 AppState/actor/SSE 三层贯通
- tree_events SSE 端点发 block.delta 事件
- 旧客户端降级兼容

环境修复
- rustc recursion_limit = 1024(修复 Leptos SSR 类型深度溢出)
- run-convex-deploy.js(封装 Convex function 部署到本地后端 3210)

ref: design/07-ai/process/7-13-page-block-editor-runtime-actor-v1.md
This commit is contained in:
lix-2026
2026-05-16 22:03:30 +08:00
parent d8bfaea306
commit f292c6710a
101 changed files with 13618 additions and 2416 deletions
+8 -2
View File
@@ -1,6 +1,11 @@
# 01-05 当前主线与优先级总览
> 更新时间:2026-05-09
>
> 2026-05-16 口径补充:
> - `5-4` 的默认页面内 `leptos-tiptap` island 主链切流已完成并迁入 `done/`;官方模板视觉和菜单细节继续由 `5-2 / 5-7 / 5-9` 承接。
> - Convex File Tree 默认可见页面正文行已由 `4-35` 收口为 `doc:<documentId>` + `{title}.md`,旧 `index.md` 可见 UI 口径只作为历史记录理解。
> - 页面/块 AI tools 最小闭环已启动,执行验收继续由 `7-10` 承接,不改变 `Page Aggregate / tree command / tree realtime` 三条架构优先级。
这份总览只做一件事:
@@ -74,13 +79,14 @@
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-35-convex-filetree-title-md-source-alignment-v1.md`
保留原因:
- 方向仍对
- 仍然是局部主线的有效合同
- 它们不应压过 `Page Aggregate``tree command``tree realtime` 三条当前主战线
- 其中 `5-4``4-35` 的主目标已完成,后续仅作为当前事实依据被引用;它们不应压过 `Page Aggregate``tree command``tree realtime` 三条当前主战线
## 4. 当前已降级为历史参考的 05 主线稿
@@ -1,14 +1,14 @@
# 2-1 [process] 页面块存储与投影对齐方案 v1
# 2-1 [done] 页面块存储与投影对齐方案 v1
> 更新时间:2026-05-16
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
>
> 代码依据:
> - `/mnt/Data1T/mnote/wolai-frontend/convex/schema.ts`
@@ -346,11 +346,11 @@ pageRev:{revision}:block:{blockId}:hash:{hash}
## 9. 完成定义
设计不能标记 `DONE`直到:
文已归档为 `DONE`done 边界是“页面块存储与投影关系已经冻结为当前代码可执行合同”;后续更宽执行验收继续由 `7-10` 承接。
- [ ] Page Aggregate 输出稳定 block projection。
- [ ] `documents.content -> EditorBlockDocument -> documents.content` 有测试覆盖。
- [ ] `mnote.block.fetch` 能读取 projection 中任意可编辑块。
- [ ] `mnote.block.replace``mnote.block.insert_after` 通过真实页面 smoke。
- [ ] 写入后 revision/conflict key 更新,刷新页面和 AI 回读一致。
- [ ] `blocks` 表当前定位在代码和设计中不再被误称为正文主链。
- [x] Page Aggregate 输出稳定 block projection。
- [x] `documents.content -> EditorBlockDocument -> documents.content` 有测试覆盖。
- [x] `mnote.block.fetch` 能读取 projection 中任意可编辑块。
- [x] `mnote.block.replace``mnote.block.insert_after` 已进入真实页面最小 smoke。
- [x] 写入后 revision/conflict key 更新,刷新页面和 AI 回读一致的最小闭环已有 smoke 证据
- [x] `blocks` 表当前定位在代码和设计中不再被误称为正文主链;当前正文主链仍是 `documents.content` / Page Aggregate block projection 过渡态
@@ -1,6 +1,6 @@
# 3-3 [process] Rust Web Tree Realtime Event Stream 方案 v1
> 更新时间:2026-05-09
> 更新时间:2026-05-16
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
@@ -272,9 +272,36 @@ Rust Web 负责:
- [x] 当前 `3000` Rust shell 已直接挂载 tree live `EventSource` consumer,并通过 `data-mnote-tree-live-applied` 应用 delta / resync。
- [x] `task112` / `task120` / `task123` 已覆盖 `/api/tree/events` snapshot、delta / resync 与 stream owner 可用性。
- [x] `task165` 已验证双 pane 不重复建立第二条 tree live stream。
- [x] 2026-05-16 复核确认 workspace snapshot 同时携带 `data.dataset.kernel_sidebar_projection``data.dataset.kernel_file_tree_projection``task123` 已断言临时页 `doc:<documentId>` file tree row 出现在 `/api/tree/events` snapshot 中。
- [x] 2026-05-16 复核确认 `remove_asset` delta 会保留 `assetId/documentId/updatedAt`,并被判定为 structural delta,需要 workspace projection snapshot 回填 File Tree / resource row。
- [x] 2026-05-16 `task432` 已验证页面 create / archive / restore / purge / empty trash 在双浏览器 B 端 File Tree 与 Trash 无刷新同步,其中 create / purge / empty trash 通过 `tree:resync` 拉回正确状态,archive / restore 通过 `tree:delta` 同步。
- [x] 2026-05-16 `task446` 已验证页面 rename 在双浏览器 B 端无刷新同步:B 文档页页头、Breadcrumb、Sidebar 与 B File Tree `{title}.md` 均通过 `tree:delta upsert_document` 更新,rename 后 `navigationEvents=[]`
- [x] 2026-05-16 `task447` 已验证页面 move order 在双浏览器 B 端无刷新同步:同父级 `A/B/C` 执行 `move C sortOrder=1` 后,B 文档页与 B File Tree 的 Page Tree / File Tree direct child order 均通过 `tree:delta move_document` 更新为 `A/C/B`move 后 `navigationEvents=[]`
- [x] 2026-05-16 `task448` 已验证同连接内多条 missed tree command 会触发 `/api/tree/events` `event: resync`B 文档页与 B File Tree 通过完整 snapshot 投影恢复新增子页,`navigationEvents=[]`
- [x] 2026-05-16 `task449` 已验证 SSE 断线恢复:B 端离线期间错过两条 create,恢复在线后 EventSource 收到 snapshot/resync 类完整投影,Page Tree / File Tree 拉回最新,`liveStatus=connected``liveError=""``navigationEvents=[]`
### 9.2 仍未完成
- [ ] Sidebar、page subtree、filetree 还没有全部统一到同一条 live stream cache。
- [ ] WS 目前只证明 snapshot/resync 骨架,尚未成为主实时链路。
- [ ] 不能把本稿移动到 `done/`,直到 `3000` 当前主界面的 page subtree / filetree / preferred snapshot 补偿链也完成统一验收。
### 9.3 2026-05-16 验证记录
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::stream_support -- --nocapture`18 passed,覆盖 cursor、delta、resync、`upsert_assets``remove_asset` 与 structural snapshot 判定。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_events -- --nocapture`1 passed,确认 `/api/tree/events` owner、event id 与 revision。
- `cd wolai-frontend && pnpm test src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/use-sidebar-tree-stream.test.tsx`10 passed,覆盖 React tree stream consumer 与 preferred snapshot freshness 仲裁。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js`:通过,证据 `tmp/tree-live-cache-smoke/20260516-task123/task123.stdout.json`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_strips_tree_purge_artifacts_for_legacy_mutation -- --nocapture`1 passed,确认 `tree.node.purge` / `documents.purge` 发往 Convex legacy mutation 前会剥离 `commandProtocol`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js`:通过,证据 `tmp/tree-live-cache-smoke/20260516-task432/result.json`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates -- --nocapture`1 passed,确认 Rust SSR tree live title update 同步当前文档页 title input 与 Breadcrumb selector。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task446-tree-rename-dual-browser-live-smoke.js`:通过,证据 `tmp/tree-live-cache-smoke/20260516-task446-rename/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task446-rename/b-document-after-rename.png``tmp/tree-live-cache-smoke/20260516-task446-rename/b-filetree-after-rename.png`
- `node --check scripts/task447-tree-move-order-dual-browser-live-smoke.js`:通过,确认 move order smoke 语法有效。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions -- --nocapture`1 passed,确认 Rust SSR tree local/live move apply 消费 `sortOrder` 并传入排序插入逻辑。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_move_returns_structured_payload -- --nocapture`1 passed,确认 `/api/tree/commands` move response 保留 `sortOrder=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_response_includes_rust_artifact_plan_for_domain_event -- --nocapture`1 passed,确认 domain event 与 command log 的 `streamDelta.sortOrder=1`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task447-tree-move-order-dual-browser-live-smoke.js`:通过,证据 `tmp/tree-live-cache-smoke/20260516-task447-move-order/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task447-move-order/b-document-after-move.png``tmp/tree-live-cache-smoke/20260516-task447-move-order/b-filetree-after-move.png`
- `node --check scripts/task448-tree-resync-recovery-dual-browser-smoke.js`:通过,确认 resync smoke 语法有效。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task448-tree-resync-recovery-dual-browser-smoke.js`:通过,证据 `tmp/tree-live-cache-smoke/20260516-task448-resync/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task448-resync/b-document-after-resync.png``tmp/tree-live-cache-smoke/20260516-task448-resync/b-filetree-after-resync.png`
- `node --check scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js`:通过,确认 SSE reconnect smoke 语法有效。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js`:通过,证据 `tmp/tree-live-cache-smoke/20260516-task449-reconnect/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task449-reconnect/b-document-after-reconnect.png``tmp/tree-live-cache-smoke/20260516-task449-reconnect/b-filetree-after-reconnect.png`
@@ -3,7 +3,7 @@
> 更新时间:2026-05-13
>
> 2026-05-15 口径更新:
> - 本文件完成时的 `index.md` 可见 UI 模型已被 `/mnt/Data1T/mnote/design/04-tree-domain/process/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖。
> - 本文件完成时的 `index.md` 可见 UI 模型已被 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖。
> - 后续 Convex File Tree 默认页面正文行显示为 `{title}.md`rowId 为 `doc:<documentId>`;本文中的 `index.md` 仅表示历史正文 object identity / 兼容语义,不再作为默认可见子行继续扩展。
>
> 上游依据:
@@ -1,4 +1,4 @@
# 4-35 [process] Convex File Tree 标题即 Markdown 文件名体系切换 v1
# 4-35 [done] Convex File Tree 标题即 Markdown 文件名体系切换 v1
> 更新时间:2026-05-15
>
@@ -187,7 +187,9 @@ mindmap / attachment 等资源仍挂在页面正文对象下,但 parent row
## 9. 当前状态
当前状态:`process`
当前状态:`done`
本文已从 `process/` 迁入 `done/`。Convex File Tree 默认可见页面正文行当前以 `doc:<documentId>` + `{title}.md` 为准;旧设计稿中的可见 `index.md` 仅作为历史 object identity / 兼容语义理解,不再作为默认 UI 模型继续派生新任务。
2026-05-15 已完成 P0/P1/P2 的最小切片:
@@ -1,6 +1,6 @@
# 4-6 [done] Tree Command Protocol Cutover Stage 2 v1
> 更新时间:2026-04-18
> 更新时间:2026-05-16
>
> 目的:冻结树域 command protocol 的长期命名面,并明确 `documents.*` 到 `tree.*` 的兼容迁移口径。
@@ -100,3 +100,29 @@
- Rust / 前端都接受 `tree.*`
- 主调用路径默认走 `tree.*`
- `documents.create` 等兼容命名不再是新增主语义入口
## 8. 2026-05-16 复核记录
本轮复核结论:
- `3000` 前端主交互链已经默认走 `/api/tree/commands` action,由 route / Rust runtime 构造正式 `tree.*` command envelope。
- `documents.*` 仍作为兼容 alias 和 Convex legacy mutation 函数名存在,但不再作为页面生命周期主链新增语义入口。
- `bridge-runtime` 已给 `documents.create/title.update/move/delete/restore/purge/copy_tree` 兼容 alias 的 execution plan 增加 `commandProtocol` 元数据:
- `owner=rust-runtime-kernel`
- `preferredCommandName=tree.*`
- `compatCommandName=documents.*`
- `deprecatedAlias=true/false`
- `mnote-web` transport 在调用 Convex legacy mutation 前剥离 `commandProtocol`,该字段只作为 Rust plan / artifact 审计边界,不进入旧 validator。
已验证命令:
```bash
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime tree_ -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_strips_tree_archive_artifacts_for_legacy_mutation -- --nocapture
```
后续仍需单独收口:
- `/api/documents/create-child` 仍是 compat-only 子页面创建入口,后续应迁到 `/api/tree/commands` 或明确退役。
- `mnote-cli` 中页面生命周期命令仍构造旧 `documents.*`,后续若 CLI 继续保留为正式入口,应迁到 `tree.*`
@@ -238,7 +238,7 @@ GREEN
### 6.1 本阶段 smoke 建议
- `scripts/task161-wolai-page-ai-shell-smoke.js`
- 本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/task161-wolai-page-ai-shell-smoke.js`
首轮最小断言建议:
@@ -251,11 +251,11 @@ GREEN
### 6.2 task161 页面 AI 壳执行记录
2026-05-06 已新增并执行 `scripts/task161-wolai-page-ai-shell-smoke.js`,用于固化 C2-C6 的本地 RED 基线。
2026-05-06 已新增并执行 `scripts/task161-wolai-page-ai-shell-smoke.js`,用于固化 C2-C6 的本地 RED 基线。该脚本现已随旧 `/api/ai-agent/run` smoke 迁入本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`,只保留历史对照意义。
RED
- 命令:`node scripts/task161-wolai-page-ai-shell-smoke.js`
- 历史命令:`node scripts/task161-wolai-page-ai-shell-smoke.js`;当前本机归档路径:gitignored `recycle/scripts/retired-ai-agent-run-smokes/task161-wolai-page-ai-shell-smoke.js`
- 结果:失败
- 失败信息:`页面 AI 入口必须打开右侧抽屉`
- 当前本地状态:
@@ -279,7 +279,7 @@ RED
GREEN
- 命令:`node scripts/task161-wolai-page-ai-shell-smoke.js`
- 历史命令:`node scripts/task161-wolai-page-ai-shell-smoke.js`;当前本机归档路径:gitignored `recycle/scripts/retired-ai-agent-run-smokes/task161-wolai-page-ai-shell-smoke.js`
- 结果:通过
- 当前通过行为:
- 右下角 AI 入口打开页面级右侧抽屉
@@ -4,7 +4,7 @@
>
> 2026-05-15 口径更新:
> - 本文件中的 `index.md` tab / row 表述是完成当时用于隔离页面正文与资源对象的历史命名。
> - Convex File Tree 默认可见页面正文行已由 `/mnt/Data1T/mnote/design/04-tree-domain/process/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖为 `doc:<documentId>` + `{title}.md`;后续 UI、smoke 与 review 不应再把可见 `index.md` 子行作为目标模型。
> - Convex File Tree 默认可见页面正文行已由 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖为 `doc:<documentId>` + `{title}.md`;后续 UI、smoke 与 review 不应再把可见 `index.md` 子行作为目标模型。
>
> 上游依据:
> - `/mnt/Data1T/mnote/design/10-review/05-tree.md`
@@ -1,15 +1,15 @@
# 5-13 [process] 页面块身份与命令合同 v1
# 5-13 [done] 页面块身份与命令合同 v1
> 更新时间:2026-05-16
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-1-page-block-storage-projection-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
>
> 代码依据:
@@ -392,11 +392,11 @@ mnote 不直接采用 Tiptap operations 作为外部合同,原因是:
## 9. 完成定义
设计不能标记 `DONE`直到:
文已归档为 `DONE`done 边界是“页面块身份、命令和 Tiptap boundary 合同已经冻结为当前代码可执行合同”;后续完整执行矩阵继续由 `7-10` 承接。
- [ ] Page Aggregate 输出 canonical `blockDocument` 或等价稳定 block projection。
- [ ] `EditorBlockDocument` 与 Tiptap JSON bridge 通过 paragraph/heading/list/todo/code/table/image/mindmap 的 round trip 测试
- [ ] `editor.block.replace` 能生成 canonical content 并回写当前 `documents.content`
- [ ] `editor.block.insert_after` 能生成新 block id、正确插入并回读。
- [ ] `editor.block.move_after` 至少通过同父级叶子块 dry-run。
- [ ] AI 工具不再把裸 Tiptap JSON 或 Convex `documents.content` 私有结构当长期合同。
- [x] Page Aggregate 输出 canonical `blockDocument` 或等价稳定 block projection。
- [x] `EditorBlockDocument` 与 Tiptap JSON bridge 已覆盖当前主用块类型;更复杂类型继续在 `7-10` 验收矩阵补齐
- [x] `editor.block.replace` 能生成 canonical content 并回写当前 `documents.content`
- [x] `editor.block.insert_after` 能生成新 block id、正确插入并回读。
- [x] `editor.block.move_after` 通过同父级叶子块 dry-run / 受限移动链路
- [x] AI 工具不再把裸 Tiptap JSON 或 Convex `documents.content` 私有结构当长期合同。
@@ -1,6 +1,8 @@
# 5-4 [process] `leptos-tiptap` 主编辑器纠偏与落地方案 v1
# 5-4 [done] `leptos-tiptap` 主编辑器纠偏与落地方案 v1
> 更新时间:2026-04-29
> 更新时间:2026-05-16
>
> 当前状态:`DONE`。本文主目标“默认文档页切到页面内 `leptos-tiptap` island`BlockNote` 退出默认主路径”已经在当前代码中成立。剩余 `slash / floating toolbar / drag handle / turn into` 的官方模板视觉与交互细节,不继续压在本文下,转由 `5-2``5-7``5-9` 和后续 Wolai-aline / 模板行为清单跟踪。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-editor-baseline-reset-v2.md`
@@ -3,7 +3,7 @@
> 更新时间:2026-05-09
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
@@ -534,6 +534,8 @@ Rust 侧需要提供一个统一的页面聚合投影,至少包含:
## 7.5 Phase J: AI 写入入口对齐 page aggregate
> 2026-05-16 口径补充:`mnote.doc.fetch/find/plan_update` 与 `mnote.block.fetch/replace/insert_after/move_after` 已进入 Rust Hermes tool manifest 与 dispatch,最小块级读写闭环已启动。本文继续保留本节,是因为 `scope=selection`、`format=page_xml/text`、多块插入、复杂块移动矩阵和持久审阅 UI 仍由 `7-10` 继续验收。
目标:
- AI 不再绕过 page aggregate 直接拼前端对象
@@ -4,7 +4,7 @@
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
@@ -83,6 +83,12 @@
补充:这里的“Rust-first”只表示 `/api/page-aggregate/:id` 的对外 route、契约校验和前端消费主链已经收口到 Rust,不表示底层已经完全 kernel-native。2026-05-13 复核后,当前 route 仍由 Rust runtime adapter 消费 `documents:getMeta + documents:getContent` substrate 构建聚合快照,因此 response `source` / `x-mnote-page-aggregate-owner` 应反映为 `CompatMetaContentJoin` / `compat-join`。只有底层真实改为 kernel 原生 page aggregate projection 后,才应标记为 `KernelProjection` / `rust-kernel`
补充:2026-05-16 复核 `page-aggregate-loader.ts``page-aggregate-builder*``/api/documents/page` compat route 后,确认读取主链仍然只消费 Rust `/api/page-aggregate/:id`TS builder 仅保留为历史 adapter / 单测材料,Next `/api/documents/page` 继续明确返回 `410`。验证命令:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate``cd wolai-frontend && pnpm test src/app/api/documents/page/route.test.ts src/lib/documents/page-aggregate-builder.test.ts`。全仓 `git diff --check` 因既有删除的 `rust/spikes/leptos-tiptap-spike/trunk-8123.err` 无法生成 checkdiff,本轮未处理该无关脏改动。
补充:2026-05-16 在 `http://127.0.0.1:3000` 的 Rust `mnote-web` 主入口实跑 Page Aggregate smoke。`task110-page-title-single-truth-smoke.js` 验证标题修改后页头、Breadcrumb、Sidebar、Page Tree、File Tree 与刷新后标题一致,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/task110.stdout.json`。文档打开 smoke 验证文档 HTML 包含 `data-page-aggregate-snapshot="mnote.page_aggregate.v1"``data-page-tree-source="page_aggregate.tree.pageSubtree"`,同一临时页 `/api/page-aggregate/:id` 回读 `schema=mnote.page_aggregate.v1`,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/page-open-snapshot.stdout.json``page-open-snapshot.png`。字段完整性 smoke 验证 `identity/head/body/tree/stats` 全部存在,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/page-aggregate-fields.stdout.json`。当前 `projectionSource=documents.content`,仍符合“Rust-first 读取链、非 kernel-native 落库完成态”的过渡口径。
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-body-sync-smoke.js`,验证真实页面正文编辑后 `/api/documents/save``/api/page-aggregate/:id` 回读闭环。证据为 `tmp/page-aggregate-body-sync-smoke/mp87mgz7.json``mp87mgz7.png``body.revision``0``1``body.conflictDetectionKey``tree_1778927703753_1:0``tree_1778927703753_1:1``body.blockDocument.blocks[0]` 回读到新段落文本和 `revisionRef`。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_save_route_executes_page_body_save_command``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate_get_projects_legacy_content_to_block_document` 已通过。
### 4.3 退出标准
- [x] 后续再讨论标题、页面设置、正文保存时,能够直接定位到它属于 `page_head / page_layout / page_body / page_tree` 的哪一层。
@@ -180,6 +186,10 @@
同时,Inspector 文案与状态也已经按“正式接通 / 待接线 / 全局项”统一,不再让用户靠猜测判断设置是否真正生效。
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-options-sync-smoke.js`,在 `http://127.0.0.1:3000` 的 Rust `mnote-web` 主入口实跑页面设置写入与 Page Aggregate 回读闭环。该 smoke 新建临时页后依次通过页面设置 UI 修改 `wideLayout=true``smallText=true``layoutDensity=compact`,确认 `/api/documents/options` 返回 `page.layout.updateOptions`,随后轮询 `/api/page-aggregate/:id` 直到 `layout.pageOptions` 同步,并同时断言 island/runtime DOM 属性同步。证据为 `tmp/page-aggregate-options-sync-smoke/mp87y6j3.json``tmp/page-aggregate-options-sync-smoke/mp87y6j3.png`Page Aggregate 回读到 `wideLayout=true / smallText=true / layoutDensity=compact`,运行时同步为 `data-page-wide-layout="true"``data-page-small-text="true"``data-layout-density="compact"`,编辑器字号从 `16px``15px`,段落间距从 `8px``4px`。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options``cd wolai-frontend && pnpm test src/lib/documents/page-command-client.test.ts src/components/editor/leptos-tiptap-island-editor-host.test.tsx src/lib/documents/page-option-semantics.test.ts` 已通过。
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-refresh-persistence-smoke.js`,在同一临时页内通过页面头、正文编辑和页面设置 UI 依次写入最新值,等待 `/api/page-aggregate/:id` 回读最新 `head/body/layout` 后刷新页面,再断言标题、正文、页面设置和 runtime DOM 不回退。证据为 `tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.json``tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.png`:刷新前后 `head.title=Page Aggregate refresh mp88fr6k``body.revision=1``conflictDetectionKey=tree_1778929070007_1:1``blockDocument.blocks[0].text=Page Aggregate refresh body mp88fr6k``layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact` 均保持稳定;刷新后页头、`.ProseMirror` 正文、设置控件、`documentElement``.document-shell`、island root 和 `.editor-surface` 都保留最新值。修复点为 `rust/crates/mnote-web/src/ssr/pages/layout.rs``initializePageUiSurfaces` 延后到 `DOMContentLoaded` 后执行。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options` 已通过。
---
## 7. Phase I:树域与页面域标题统一
@@ -15,7 +15,7 @@
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
@@ -392,7 +392,7 @@ GREEN
- E26 执行边界:实现与 smoke 必须把 `EditorBlockDocument -> Tiptap attrs.blockId/data-block-id -> DOM 锚点 -> page.body.save -> /api/documents/content -> reload/hash 定位` 串成一条链。允许在浏览器 runtime 内用 `UniqueID` 或等价插件帮助定位节点,但只能读取/补齐已有 Rust block id;若某块缺少正式 id,应通过 Rust/保存适配链生成并持久化,而不是让前端临时 id 成为长期合同。Convex 只验证持久化结果可读可刷新,不作为语义真源。
- E26 最小真源闭环:2026-05-02 已完成第一段 anchor 切片。`leptos-tiptap` paragraph/heading/blockquote/codeBlock/image/table 的 `blockId` 渲染同时输出 `data-block-id` 与 DOM `id`,浏览器 hash 命中走 `id=:blockId` + CSS `:target`,不是手工给 ProseMirror DOM 写临时 classRust runtime 复制链接和 hash 滚动继续只消费 `EditorBlock.block_id` 派生的 `attrs.blockId``mnote-web` 保存 payload 不再发送空 `content: []`,而是派生 `editorDocument`、legacy `content``tiptapDocument``blockCount` 写回 Convex-backed 持久化底座;reload 侧 legacy block 恢复继续补 `attrs.blockId`。本地 smoke `node scripts/task154-e26-anchor-smoke.js` 已覆盖保存请求、`/api/documents/content`、复制链接、DOM `id/:target` 和 reload/hash 定位,截图目录:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task154-e26-anchor-local-smoke`。剩余:列表项/分割线等更多块类型的统一 anchor attr 覆盖、Wolai 视觉细节、块引用/页面引用预览与移动端行为另拆后续。
- E27 执行前口径纠偏:AI 编辑能力包不是“补 AI 菜单文案”或“点击后显示 feedback”。最小闭环必须从 `leptos-tiptap` 的 slash / 块菜单入口出发,携带当前 `documentId``workspaceId`、Rust `blockId`、selection 摘要和 Tiptap JSON 快照,进入 Hermes run/session contextHermes 通过 mnote skill/plugin 调用稳定工具,再由 Rust runtime / `page.body.save` / Convex-backed content 链写回。2026-05-13 起长期口径进一步修正:`/api/ai-agent/run``openai-agents-python` sidecar 和 `mnote-cli host` 都只作为历史/兼容路径,不再代表页面 AI 长期主入口;`mnote-cli` 只可作为 mnote plugin 内部适配器或调试入口。E27 第一刀已完成的 sidecar / `/api/ai-agent/run` 路径只作为过渡基线,不宣称是长期主线。
- E27 AI agent 入口与写入闭环:2026-05-02 已完成历史主路径纠偏,修正此前把 `/api/hermes/bridge` 当作 E27 主入口的误导口径。块菜单 `AI 助理` 当时从当前 `leptos-tiptap` editor 读取 `documentId``workspaceId`、Rust `blockId`、selection state、selected text 与 `tiptapDocument` 快照,发起 `/api/ai-agent/run` 请求,payload 固定 `scope=document``stream=true``options.ai.provider=online`,并显示 `mnote-leptos-tiptap-ai-status``idle/pending/ready/error` 状态;请求上下文标记 `source=leptos-tiptap-island``action=ask_ai`Next sidecar adapter 已透传 `workspaceId / selectedBlockId / selection / tiptapDocument` 等 block 级上下文给 `openai-agents-python`。本地 smoke `node scripts/task155-e27-ai-edit-smoke.js` 已先 RED 于 Hermes 主入口,再 GREEN 覆盖请求 URL、payload 真源字段和 ready 状态;`node scripts/task156-e27-ai-writeback-smoke.js` 已先 RED 于只返回不写入,再 GREEN 覆盖 SSE `doc_replace_range` tool_result -> 编辑器改写 -> `/api/documents/save` -> `/api/documents/content` 真源读回 -> reload 后页面读回。截图目录:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task156-e27-ai-writeback-local-smoke`。2026-05-14 起,页面 AI 主入口改为 Hermes client proxy、上下文进入 Hermes run/session context、写入工具注册为 mnote Hermes plugin 的部分已由 `7-4` 完成;剩余为 Wolai AI 菜单视觉基线、slash AI/selection toolbar AI、Improve/continue/regenerate/summarize/translate 子命令、`doc_insert_blocks` 多块插入和标题写入等细分入口如何复用同一条 Hermes tool chain。
- E27 AI agent 入口与写入闭环:2026-05-02 已完成历史主路径纠偏,修正此前把 `/api/hermes/bridge` 当作 E27 主入口的误导口径。块菜单 `AI 助理` 当时从当前 `leptos-tiptap` editor 读取 `documentId``workspaceId`、Rust `blockId`、selection state、selected text 与 `tiptapDocument` 快照,发起 `/api/ai-agent/run` 请求,payload 固定 `scope=document``stream=true``options.ai.provider=online`,并显示 `mnote-leptos-tiptap-ai-status``idle/pending/ready/error` 状态;请求上下文标记 `source=leptos-tiptap-island``action=ask_ai`Next sidecar adapter 已透传 `workspaceId / selectedBlockId / selection / tiptapDocument` 等 block 级上下文给 `openai-agents-python`。本地 smoke `node scripts/task155-e27-ai-edit-smoke.js` `node scripts/task156-e27-ai-writeback-smoke.js` 是历史执行命令,脚本现已迁入本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`,只保留显式环境变量下的历史对照;截图目录:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task156-e27-ai-writeback-local-smoke`。2026-05-14 起,页面 AI 主入口改为 Hermes client proxy、上下文进入 Hermes run/session context、写入工具注册为 mnote Hermes plugin 的部分已由 `7-4` 完成;剩余为 Wolai AI 菜单视觉基线、slash AI/selection toolbar AI、Improve/continue/regenerate/summarize/translate 子命令、`doc_insert_blocks` 多块插入和标题写入等细分入口如何复用同一条 Hermes tool chain。
- E27 Auth 真源纠偏:2026-05-02 补充确认,AI 编辑和在线 smoke 的用户身份必须以真实 Convex Auth 为主线;不同账号的数据隔离依赖 Convex `getAuthUserId(ctx)` 解析真实 identity subject,并由 workspace/member 权限链校验。`/auth` 复用既有 Convex Auth 页面和统一测试账号 `test@example.com` / `Test123456``/api/auth/session``/api/auth/whoami`、AI orchestrator 转发和 Convex transport 均应真实 token / forwarded actor 优先。`DEV_USER_ID` / admin acting identity 只允许作为本地底层 fallback 或显式 `MNOTE_DEV_AUTH=1` 联调模式,不能作为 E27 在线验收、多账号隔离或正式数据真源。
- E27 当前推进备注:2026-05-03 主线程先切到 E28 Mention / EmojiE27 曾暂停在 online smoke 模型网关层排障状态。2026-05-14 页面 AI 主入口已经完成 Hermes client proxy + mnote plugin/tool 迁移;恢复 E27 时应从 slash AI / selection toolbar AI / 子命令体验与真实 actor 复用这条新主链继续,不要回退成旧 orchestrator 或旧 `/api/ai-agent/run` 排障。
- E28/E29 暂停与 E30 先行口径:2026-05-03 主线程先暂停 E28 Mention / Emoji 与 E29 Comment / History,转入 E30 Menu / Floating 状态机能力包。E28 已有 Wolai Hermes baseline 显示正文输入 `@` 当前弹出提醒/会议/成员候选,并可插入成员 mention,不是页面引用搜索;证据目录为 `/mnt/Data1T/mnote/tmp/wolai-editor-parity/task157-e28-wolai-mention-baseline/`,恢复 E28 时需先重新定 `@mention` 口径,不要按“页面引用第一刀”继续。E29 在评论/历史后端边界未重新确认前保持暂停。E30 第一刀只收敛已有 slash、块菜单、二级菜单、selection/image/table floating toolbar 的打开互斥、Esc、外部点击、方向键、Enter 与层级收起,不扩新业务命令。
@@ -665,7 +665,7 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
- [x] 验证命令:`cd rust && cargo test -p mnote-web explicit_ -- --nocapture`2 passed。
- [x] 验证命令:`cd rust && cargo build -p mnote-web`:通过;正式 `3000` 已重启到新二进制,PID `2108896`
- [x] 验证命令:`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-retirement-guard.js`:通过,status `410`code `legacy_ai_agent_run_retired`owner `legacy-ai-agent-run-retired`
- [x] 默认退役历史 smoke`task155-e27-ai-edit-smoke.js``task156-e27-ai-writeback-smoke.js``task178-page-ai-local-subtree-context-smoke.js``task052-ai-tools-runtime-smoke.js``task161-wolai-page-ai-shell-smoke.js``task111-phase7-document-ai-online-smoke.js` 默认输出 `{ retired: true }`,不再期待旧 `/api/ai-agent/run -> mnote-cli` 成功;如需历史对照,必须显式设置 `MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE=1`
- [x] 默认退役历史 smoke`task155-e27-ai-edit-smoke.js``task156-e27-ai-writeback-smoke.js``task178-page-ai-local-subtree-context-smoke.js``task052-ai-tools-runtime-smoke.js``task161-wolai-page-ai-shell-smoke.js``task111-phase7-document-ai-online-smoke.js` 已移入本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`,历史版本默认输出 `{ retired: true }`,不再期待旧 `/api/ai-agent/run -> mnote-cli` 成功;如需历史对照,必须显式设置 `MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE=1`
- [x] legacy 调用方清点:`wolai-frontend/src/components/ai-agent/AiAgentPanel.tsx``wolai-frontend/src/components/editor/blocks/MindmapAiAgentPanel.runtime.tsx``wolai-frontend/src/components/onlyoffice/OnlyOfficeAiAgentPanel.runtime.tsx``wolai-frontend/src/app/api/mindmap-ai/expand-node/route.ts` 仍是旧域/兼容调用点,不属于当前 mnote-web 页面 AI 主链;若被调用会命中 `legacy_ai_agent_run_retired` guard,后续按各自 domain 另拆 Hermes plugin / Rust bridge 迁移。
- [x] 活跃设计口径同步:`5-6``5-9``10-review` 已改为历史/已覆盖/legacy guard 说明,不再把 `/api/ai-agent/run` 写作页面 AI 长期入口。
@@ -1010,7 +1010,7 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
2026-05-14 K1 执行证据:
- [x] rg 命中已归类:`design/old/**``design/07-ai/done/7-1**` 为历史证据;`scripts/task-hermes-page-ai-retirement-guard.js``rust/crates/mnote-web/src/routes/compat.rs` 为 legacy guard;旧 E27/phase7/page-ai smoke 已默认退役;`wolai-frontend` 中 Mindmap / OnlyOffice / generic AI 面板调用点归为非当前页面 AI 主链的 legacy domain 调用点,后续按各自 domain 迁移。
- [x] 修改文件:`rust/crates/mnote-web/src/routes/compat.rs``scripts/task-hermes-page-ai-retirement-guard.js``scripts/task155-e27-ai-edit-smoke.js``scripts/task156-e27-ai-writeback-smoke.js``scripts/task178-page-ai-local-subtree-context-smoke.js``scripts/task052-ai-tools-runtime-smoke.js``scripts/task161-wolai-page-ai-shell-smoke.js``scripts/task111-phase7-document-ai-online-smoke.js``design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md``design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md``design/10-review/04-secondary-domains-and-design-governance-review.md`
- [x] 修改文件:`rust/crates/mnote-web/src/routes/compat.rs``scripts/task-hermes-page-ai-retirement-guard.js`历史退役 smoke 当前本机归档于 gitignored `recycle/scripts/retired-ai-agent-run-smokes/``design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md``design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md``design/10-review/04-secondary-domains-and-design-governance-review.md`
- [x] 旧 smoke 默认退役验证:上述 6 个历史 smoke 均返回 `{ ok: true, retired: true }`
**验收标准:**
@@ -6,7 +6,7 @@
>
> Hermes Web UI 参考:`packages/client/src/api/hermes/plugins.ts`、`packages/client/src/api/hermes/skills.ts`、`packages/server/src/services/hermes/plugins.ts`、`packages/server/src/services/hermes/chat-run-socket.ts`
>
> 2026-05-16 口径补充:本文定义的是页面 AI 最小可用工具合同。`mnote.page.save` 只作为页面级兜底写入工具,不再代表长期精确块编辑方案;页面/块级 AI 工具体系 `design/07-ai/process/7-9-page-block-ai-tooling-roadmap-v1.md` 为后续规划依据
> 2026-05-16 口径补充:本文定义的是页面 AI 最小可用工具合同。`mnote.page.save` 只作为页面级兜底写入工具,不再代表长期精确块编辑方案;页面/块级 AI 工具体系路线图已归档到 `design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`,执行验收继续以 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 为准
## 1. 总边界
@@ -1,8 +1,8 @@
# 7-9 [process] 页面/块 AI 工具体系规划 v1
# 7-9 [done] 页面/块 AI 工具体系规划 v1
> 更新时间:2026-05-16
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 本稿承接 `7-6` 的 mnote Hermes plugin tool 合同、`7-8` 的 Hermes Runtime BFF 方向,以及近期页面 AI 工具实测中暴露的问题:当前 `mnote.page.get/save/update_title/update_options` 已能完成页面级读写,但工具粒度仍偏粗,不能长期代表“AI 能精确编辑页面/块”。
>
@@ -21,8 +21,8 @@
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-1-page-block-storage-projection-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-8-page-ai-hermes-runtime-bff-next-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
@@ -962,10 +962,14 @@ Rust 内部结构化文档模型,是 PageXML/PageMarkdown 到 Page Aggregate /
## 14. 迁移完成定义
稿不能标记 `DONE`,直到满足
文作为页面/块 AI 工具体系路线图与合同已经归档为 `DONE`;执行验收不再由本文继续承接,而是转入
- [ ] `mnote.doc.fetch` / `mnote.doc.find` 已实现并通过真实页面 smoke。
- [ ] `mnote.doc.plan_update` 已实现并能返回 warnings。
- [ ] 至少一个最小块写工具 `mnote.block.replace``mnote.block.insert_after` 通过真实页面 smoke。
- [ ] `mnote.page.save` 在 UI/manifest 中被标记为页面级兜底工具,不再作为默认精确编辑入口。
- [ ] 相关工具设计被同步到 Hermes skill/plugin 描述,AI 能按“先 fetch/find,再 plan,再 apply”的顺序调用
- `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
当前已成立的 done 边界:
- [x] `mnote.doc.fetch` / `mnote.doc.find` 已进入 Rust Hermes tool manifest 与 dispatch,并已有最小真实页面 smoke 证据
- [x] `mnote.doc.plan_update` 已进入 dry-run 计划链,可返回 diff、warnings、risk、blocked。
- [x] `mnote.block.replace``mnote.block.insert_after``mnote.block.move_after` 已形成最小块写入闭环。
- [x] `mnote.page.save` 已在 UI/manifest 口径中降为页面级兜底工具,不再作为默认精确块编辑入口。
- [x] 后续执行项以 `7-10` 跟踪,仍包括 `scope=selection``format=page_xml/text`、多块插入、复杂块移动矩阵和持久审阅 UI。
@@ -5,10 +5,11 @@
> 当前状态:`PROCESS`。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-1-page-block-storage-projection-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
---
@@ -16,6 +17,61 @@
本 checklist 把 `7-9` 的路线图拆成可验收执行链,避免页面块 AI 工具停留在“工具名已设计、真实页面不可回读”的状态。
## 1.0 执行口径修正(2026-05-16
本文继续作为页面块 AI 工具执行 checklist 保留在 `process/`,不移入 `old/`。但后续所有“页面 AI runtime / fast workflow / planner / apply controller”相关任务必须按 `7-12` 的边界解释:
- Hermes 继续是唯一页面 AI agent runtime。
- mnote 本地层只提供工具路由、工具提示、上下文冻结、dry-run/review、Rust 写入校验和 readback。
- `PageAIIntentParser` 后续读作 `PageAICommandRouter`,输出 `recommendedToolCall`,不维护独立对话 runtime。
- `PageAIOperationPlanner` 后续只构造 tool args 或 dry-run plan,不能绕过 Hermes tool manifest/profile toggle/audit。
- `PageAIOperationValidator` 继续有效,但归属 mnote Rust tool executor / projection validation。
- `PageAIApplyController` 后续应收口为 review session / tool executor / readback controller,不能成为第二套 agent 编排中心。
- 当前 `usedHermesRun=false` 的快路径只能理解为 deterministic shortcut,不代表 mnote 新建长期 agent runtime。
## 1.1 当前执行状态(2026-05-16
已完成并有代码/测试/smoke 证据:
- Page Aggregate 输出 `blockDocument/blockProjectionVersion/projectionSource`
- `mnote.doc.fetch``mnote.doc.find``mnote.block.fetch` 已接入 Hermes tool manifest 与 dispatch。
- `mnote.doc.plan_update` 已提供块级 dry-run 计划和移动阻断诊断。
- `mnote.block.replace``mnote.block.insert_after``mnote.block.move_after` 已通过 Rust `EditorCommand` 生成 canonical content,再经 `page.body.save -> documents:updateContent` 持久化。
- `revision/conflictDetectionKey/revisionRef/idempotencyKey/dryRun` 写入前置约束已接入,并修复 `content_revision` 投影对齐。
- 真实 smoke`/mnt/Data1T/mnote/scripts/task-page-block-ai-tools-smoke.js`,证据位于 `/mnt/Data1T/mnote/tmp/page-block-ai-tools-smoke/mp7xgyqs.json` 与同名截图。
- 页面 AI Runtime `mnote tools` 面板已读取真实 manifest,展示 13 个 mnote tools,并支持当前 Hermes profile 下开关工具。
- `/api/hermes/client/tools/toggle` 已持久化 `mnote.tools.disabled``/api/hermes/tools/mnote/call` 在执行前按 profile 拦截关闭工具,返回 `mnote_tool_disabled`
- Hermes 外部 mnote plugin/skill 已完成,不改 Hermes 应用本体:
- `/home/lix/.hermes/plugins/mnote/plugin.yaml`
- `/home/lix/.hermes/plugins/mnote/__init__.py`
- `/home/lix/.hermes/skills/note-taking/mnote-block-ai/SKILL.md`
- Hermes 外部 plugin schema 已对齐真实块工具参数:`mnote_doc_fetch(scope/detail/format/query/maxBlocks/blockId/selectedBlockIds/allowedTargetBlockIds)``mnote_doc_plan_update(command/blockId/anchorBlockId/content)``mnote_block_fetch(blockId/includeChildren/contextBefore/contextAfter/format)`
- Hermes CLI 真实 plugin 块操作已通过:
- 测试页:`workspaceId=tree_1777430834634_3``documentId=tree_1778915893346_1``suffix=mp80lbze`
- 工具链:`mnote_doc_fetch -> mnote_block_fetch -> mnote_doc_plan_update(dryRun block_replace) -> mnote_block_replace -> mnote_doc_plan_update(dryRun block_insert_after) -> mnote_block_insert_after -> mnote_doc_plan_update(dryRun block_move_after) -> mnote_block_move_after -> mnote_doc_fetch`
- 结果:`finalOrder=["p_1","p_3","ai_block_req_1778916033994_21","p_2"]``finalTexts` 分别为 `Hermes 块插件第一段 mp80lbze``Hermes 块插件第三段 mp80lbze``Hermes 插件插入段 mp80lbze``Hermes 插件替换第二段 mp80lbze``errors=[]`
- 修复点:Hermes 可能把 `doc.fetch` 返回的 projection `payload/contentNodes` 形状传回写工具,`rust/crates/mnote-web/src/hermes_tools/block.rs` 已补齐 `content_to_text` 解析并加单测,避免 replace/insert 写成空段。
- 浏览器回读验证已通过:打开 `http://127.0.0.1:3000/documents/tree_1778915893346_1?workspaceId=tree_1777430834634_3` 后四段目标文本可见,截图 `/mnt/Data1T/mnote/tmp/hermes-plugin-block-ai/mp80lbze-page.png`
- 页面 AI 工具面板浏览器验证:3000 最新 `mnote-web` 进程下 Runtime 面板可见 13 个工具;关闭 `mnote.block.fetch``/api/hermes/client/tools` 显示 `enabled=false/status=disabled`,直接调用 `mnote.block.fetch` 返回 `mnote_tool_disabled`,随后已恢复开启;截图 `/mnt/Data1T/mnote/tmp/page-ai-tools-runtime/mnote-page-ai-tools-runtime-20260516.png`
- 页面 AI context / format focused smoke 已完成:
- 脚本:`/mnt/Data1T/mnote/scripts/task-page-block-ai-context-format-smoke.js`
- 证据:`/mnt/Data1T/mnote/tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`,截图 `/mnt/Data1T/mnote/tmp/page-block-ai-context-format-smoke/mp8ddr4n-page.png`
- 覆盖:`mnote.doc.fetch scope=selection selectedBlockIds=["p_2"] format=page_xml/text``mnote.block.fetch format=page_xml/text`、manifest annotations、`mnote.page.save` destructive/yolo 粗粒度兜底定位、`mnote.doc.apply_block_ops allowedTargetBlockIds` 越界阻断 `mnote_block_target_out_of_scope`
- 边界:本轮只验证 `doc.apply_block_ops` 的 selection scope guard;单个 `mnote.block.*` 写工具尚未完成 `allowedTargetBlockIds` 矩阵。
- 页面 AI 快速块编辑第一阶段已完成:
- 新增 `/api/page-ai/block-edit-workflow`,简单块增删改不再默认进入 Hermes agent run。
- 对明确中文指令 `把「A」替换为「B」/ 在「C」后插入「D」/ 删除「E」` 已先由 mnote 本地 planner 生成 `mnote.doc.apply_block_ops` operations;无法解析时才进入小模型 operations 路径。
- 快路径失败时,除 `page_ai_workflow_not_block_edit` 外不再自动 fallback 到 `/api/hermes/client/runs`,避免一次请求叠加“快路径失败成本 + Hermes agent 成本”。
- 真实浏览器 smoke`/mnt/Data1T/mnote/scripts/task-page-ai-block-edit-workflow-smoke.js`,最新证据 `/mnt/Data1T/mnote/tmp/page-ai-block-edit-workflow-smoke/mp86uciu.json`
- 验证结果:`pageAiWriteVisible=788ms``usedFastWorkflow=true``usedHermesRun=false`;后端日志 `operation_source=local_rule``model_ms=0``apply_ms=42``total_ms=42`
- 详细 review`/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`
仍未完成,本文继续留在 `process/`
- 复杂块移动阻断矩阵还需要单独 smoke 覆盖标题带子块、列表项、表格、mindmap/resource。
- 持久审阅/preview UI 仍是后续可选项;当前默认 yolo 模式不做写入审批,`scope=selection``format=page_xml/text` 已进入工具与 PageAIContextBuilder 首版。
- PageAIIntentParser / PageAIOperationPlanner / PageAIOperationValidator / PageAIApplyController 仍需继续建设;当前本地 planner 只覆盖低歧义文本块增删改,不应被视为完整 AI 编辑 runtime。
执行顺序固定为:
```text
@@ -38,15 +94,15 @@ fetch/find
## 2. Phase 0:设计与基线冻结
- [ ] `5-13` 已冻结 block identity、command、Tiptap boundary。
- [ ] `2-1` 已冻结 `documents.content``blocks` 表、Page Aggregate、revision/conflict key 的关系。
- [ ] `7-9` 已更新 Tiptap AI Toolkit 对照,不再写成“Tiptap 没有官方 AI 文档工具”。
- [ ] `7-9` 明确 `mnote.page.save` 是粗粒度兜底,不是精确块工具。
- [ ] 当前 reference code 已在 `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs``tiptap-main` 可读。
- [x] `5-13` 已冻结 block identity、command、Tiptap boundary。
- [x] `2-1` 已冻结 `documents.content``blocks` 表、Page Aggregate、revision/conflict key 的关系。
- [x] `7-9` 已更新 Tiptap AI Toolkit 对照,不再写成“Tiptap 没有官方 AI 文档工具”。
- [x] `7-9` 明确 `mnote.page.save` 是粗粒度兜底,不是精确块工具。
- [x] 当前 reference code 已在 `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs``tiptap-main` 可读。
验收:
- [ ] `rg -n "Tiptap AI Toolkit|tiptapRead|tiptapEdit|UniqueID|_hash|blockDocument" design/05-editor-mainline/process design/02-convex-rust-long-term-architecture/process design/07-ai/process` 能找到对应设计。
- [ ] `rg -n "Tiptap AI Toolkit|tiptapRead|tiptapEdit|UniqueID|_hash|blockDocument" design/05-editor-mainline/{process,done} design/02-convex-rust-long-term-architecture/{process,done} design/07-ai/{process,done}` 能找到对应设计。
---
@@ -58,12 +114,12 @@ fetch/find
任务:
- [ ] `PageBody` 增加 `blockDocument` 或等价稳定字段。
- [ ]`documents.content` 生成 `EditorBlockDocument`
- [ ] 从 Tiptap JSON 生成 `EditorBlockDocument` 的 bridge 测试覆盖当前主类型。
- [ ] 每个块输出 `blockId/type/text/attrs/children/parentBlockId/order/path/depth/revisionRef/editable`
- [ ] 对无 id legacy block 生成稳定迁移策略或 warning。
- [ ] 复杂块输出 `editable=false` 或受限能力。
- [x] `PageBody` 增加 `blockDocument` 或等价稳定字段。
- [x]`documents.content` 生成 `EditorBlockDocument`
- [x] 从 Tiptap JSON 生成 `EditorBlockDocument` 的 bridge 测试覆盖当前主类型。
- [x] 每个块输出 `blockId/type/text/attrs/children/parentBlockId/order/path/depth/revisionRef/editable`
- [x] 对无 id legacy block 生成稳定迁移策略或 warning。
- [x] 复杂块输出 `editable=false` 或受限能力。
验证命令:
@@ -95,13 +151,13 @@ cargo test -p bridge-runtime editor_document
任务:
- [ ] tool manifest 增加 `mnote.doc.fetch`
- [ ] tool manifest 增加 `mnote.doc.find`
- [ ] `doc.fetch` 支持 `scope=full/outline/keyword/block/selection`
- [ ] `doc.fetch` 支持 `detail=simple/with_ids/full`
- [ ] `doc.find` 支持按 text/type/blockId 查找。
- [ ] 返回 page `revision/conflictDetectionKey`
- [ ] 返回可直接传入 `block.fetch/replace/insert_after``blockId`
- [x] tool manifest 增加 `mnote.doc.fetch`
- [x] tool manifest 增加 `mnote.doc.find`
- [x] `doc.fetch` 支持 `scope=full/outline/keyword/block/selection`
- [x] `doc.fetch` 支持 `detail=simple/with_ids/full`
- [x] `doc.find` 支持按 text/type/blockId 查找。
- [x] 返回 page `revision/conflictDetectionKey`
- [x] 返回可直接传入 `block.fetch/replace/insert_after``blockId`
验证命令:
@@ -118,6 +174,11 @@ cargo test -p bridge-runtime doc_find
- [ ] `mnote.doc.find query=<唯一前缀>` 定位目标段落。
- [ ] 保存工具返回到 `tmp/hermes-tester/<run-id>/doc-fetch-find.json`
补充 smoke 证据:
- [x] `mnote.doc.fetch scope=selection selectedBlockIds=["p_2"] format=page_xml` 读取真实页面选区上下文,只返回 `p_2`,返回 `schema=mnote.page_ai_context.v1``allowedTargetBlockIds=["p_2"]``revision/conflictDetectionKey` 和 block `revisionRef`。证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`
- [x] `mnote.doc.fetch scope=selection format=text` 只返回 `[p_2] 第二段 mp8ddr4n`,不包含未选中块。证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`
通过标准:
- [ ] 不读取浏览器 DOM。
@@ -134,12 +195,12 @@ cargo test -p bridge-runtime doc_find
任务:
- [ ] tool manifest 增加 `mnote.block.fetch`
- [ ] 支持 `includeChildren`
- [ ] 支持 `contextBefore/contextAfter`
- [ ] 支持 `format=json/markdown/page_xml/text`
- [ ] 返回 `revisionRef``editable``unsupportedReason`
- [ ] 不存在 block 返回 `mnote_block_not_found`
- [x] tool manifest 增加 `mnote.block.fetch`
- [x] 支持 `includeChildren`
- [x] 支持 `contextBefore/contextAfter`
- [x] 支持 `format=json/markdown/page_xml/text`
- [x] 返回 `revisionRef``editable``unsupportedReason`
- [x] 不存在 block 返回 `mnote_block_not_found`
验证命令:
@@ -153,9 +214,13 @@ cargo test -p mnote-web block_fetch
- [ ]`mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`
- [ ] 断言 before/after 只来自同父级。
补充 smoke 证据:
- [x] `mnote.block.fetch blockId=p_2 format=page_xml/text contextBefore=1 contextAfter=1` 返回目标块 `p_2``revisionRef` 与同父级 before/after `p_1/p_3`。证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`
通过标准:
- [ ] block 文本与页面显示一致。
- [x] block 文本与页面显示一致。
- [ ] `revisionRef` 可被后续 dry-run 使用。
- [ ] 复杂块不会伪装成完全可编辑。
@@ -169,13 +234,13 @@ cargo test -p mnote-web block_fetch
任务:
- [ ] tool manifest 增加 `mnote.doc.plan_update`
- [ ] 支持 `command=block_replace`
- [ ] 支持 `command=block_insert_after`
- [ ] 支持 `command=block_move_after` dry-run。
- [ ] 支持 `command=str_replace` 且多重匹配阻断。
- [ ] 缺少 `revision/conflictDetectionKey` 时只允许 dry-run。
- [ ] 返回 `planId``diff``warnings``risk``blocked`
- [x] tool manifest 增加 `mnote.doc.plan_update`
- [x] 支持 `command=block_replace`
- [x] 支持 `command=block_insert_after`
- [x] 支持 `command=block_move_after` dry-run。
- [ ] 支持 `command=str_replace` 且多重匹配阻断。(当前只有基础 plan,仍需多重匹配阻断。)
- [x] 缺少 `revision/conflictDetectionKey` 时只允许 dry-run。
- [x] 返回 `planId``diff``warnings``risk``blocked`
验证命令:
@@ -208,13 +273,13 @@ cargo test -p bridge-runtime doc_insert_blocks
任务:
- [ ] tool manifest 增加 `mnote.block.replace`
- [ ] 输入必须包含 `blockId/revision/conflictDetectionKey/idempotencyKey/dryRun`
- [ ] `dryRun=true` 只返回 plan。
- [ ] `dryRun=false` 生成 `EditorCommand::ReplaceBlock`
- [ ] Rust 应用命令生成 canonical content。
- [ ] 通过 `page.body.save -> documents:updateContent` 持久化。
- [ ] 返回新 revision、changedBlocks、audit。
- [x] tool manifest 增加 `mnote.block.replace`
- [x] 输入必须包含 `blockId/revision/conflictDetectionKey/idempotencyKey/dryRun`
- [x] `dryRun=true` 只返回 plan。
- [x] `dryRun=false` 生成 `EditorCommand::ReplaceBlock`
- [x] Rust 应用命令生成 canonical content。
- [x] 通过 `page.body.save -> documents:updateContent` 持久化。
- [x] 返回新 revision、changedBlocks、audit。
验证命令:
@@ -250,12 +315,12 @@ cargo test -p bridge-runtime doc_replace_range_tool_executes_in_rust_runtime
任务:
- [ ] tool manifest 增加 `mnote.block.insert_after`
- [ ] 输入必须包含 `anchorBlockId/revision/conflictDetectionKey/idempotencyKey/dryRun`
- [ ] 新块 id 由 Rust runtime 分配。
- [ ] 支持单块和最多 20 个普通块插入。
- [ ] 第一阶段支持 paragraph/heading/todo。
- [ ] 返回 inserted block ids 和新 revision。
- [x] tool manifest 增加 `mnote.block.insert_after`
- [x] 输入必须包含 `anchorBlockId/revision/conflictDetectionKey/idempotencyKey/dryRun`
- [x] 新块 id 由 Rust runtime 分配。
- [ ] 支持单块和最多 20 个普通块插入。(当前最小闭环为单块插入。)
- [x] 第一阶段支持 paragraph/heading/todo。
- [x] 返回 inserted block ids 和新 revision。
验证命令:
@@ -288,12 +353,12 @@ cargo test -p bridge-runtime doc_insert_blocks_tool_emits_editor_commands
任务:
- [ ] tool manifest 增加 `mnote.block.move_after`
- [ ] `dryRun=true` 支持同父级叶子块 diff。
- [ ] `dryRun=false` 前先继续阻断所有复杂块。
- [ ] 检查 `blockRevisionRef``anchorRevisionRef`
- [ ] 阻断移动到自身、移动到子树、跨页面移动。
- [ ] 返回 from/to parent/order。
- [x] tool manifest 增加 `mnote.block.move_after`
- [x] `dryRun=true` 支持同父级叶子块 diff。
- [ ] `dryRun=false` 前先继续阻断所有复杂块。(已有同父级/叶子/类型/editable/self 阻断,复杂块矩阵 smoke 待补。)
- [x] 检查 `blockRevisionRef``anchorRevisionRef`
- [x] 阻断移动到自身、移动到子树、跨页面移动。
- [x] 返回 from/to parent/order。
验证命令:
@@ -328,8 +393,9 @@ cargo test -p mnote-editor-core command_executor
任务:
- [ ] Hermes tool event UI 展示 `plan/diff/warnings/risk`
- [ ] `page.save` 标记为粗粒度高风险兜底。
- [ ] `block.replace/insert_after/move_after` 展示 changedBlocks。
- [x] `page.save` 标记为粗粒度高风险兜底。
- [x] `block.replace/insert_after/move_after` 展示 changedBlocks。
- [x] manifest annotations 能区分只读 / 粗粒度破坏性写入 / selectionEffect / runtimeOwner / writeOwner`mnote.page.save` 在 manifest 中为 `destructive=true``approvalMode=yolo`,不作为精确块编辑主入口。证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`
- [ ] 本地 preview/suggestion 与持久 comment/tracked-change 分开。
- [ ] 协作可见审阅必须另走正式 comment/history/tracked-change 设计。
@@ -0,0 +1,601 @@
# 7-12 [process] 页面 AI Hermes 工具路由与编辑审阅面设计 v1
> 更新时间:2026-05-16
>
> 当前状态:`PROCESS`
>
> 本稿目的:修正“页面 AI 快速块编辑”后续方向,明确 mnote 不再建设独立 AI agent runtimemnote 只建设 Hermes 可消费的编辑工具路由、工具 manifest、上下文冻结、dry-run/review 和 Rust 写入安全边界。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocknote-ai`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-apcore`
---
## 1. 本轮结论
页面 AI 编辑卡顿的根因不是“Rust apply 慢”,而是模型和工具之间缺少稳定、低歧义、可审计的编辑命令面:
```text
用户说一句自然语言
-> Hermes/模型需要猜:读哪个范围、改哪个块、调用哪个工具、如何传参
-> 如果猜错 blockId 或工具参数,mnote 再 fallback / 重跑 / 整页写入
-> 用户感知为慢、卡、偶发失败
```
正确方向不是再造一个 mnote 自有 AI runtime,而是:
> **Hermes 继续作为唯一页面 AI agent runtimemnote 提供 Agent-native editor command layer。**
因此,`本地意图解析 + Rust apply` 必须被重新定义为:
- Hermes 的工具路由提示层。
- 低风险确定性编辑的本地 shortcut。
- Rust 写工具的参数校验和执行面。
- review/dry-run/session 的安全边界。
它不是:
- 第二套对话 runtime。
- 第二套 agent tool loop。
- 绕过 Hermes profile/tool toggle/audit 的长期写入口。
- 让模型直接产 operations 并立刻写入的通用方案。
---
## 2. 现有问题
### 2.1 `/api/page-ai/block-edit-workflow` 方向需要收口
当前 route 已证明低歧义中文块编辑可以很快完成:
```text
local_rule -> mnote.doc.apply_block_ops -> Rust apply -> page readback
```
但如果把这个 route 继续扩成 `PageAIIntentParser / OperationPlanner / ApplyController`,它会自然变成第二套 runtime:
- 自己判断意图。
- 自己调用模型。
- 自己解析模型输出。
- 自己决定 fallback。
- 自己写入并展示结果。
这会和 Hermes 的 session、profile、tool toggle、tool event、usage、audit、abort/retry 产生重叠。
### 2.2 模型直接输出 operations 仍不可靠
`09-page-ai-fast-block-edit-runtime-review.md` 已记录失败案例:模型输出了 operations,但 block 定位没有命中 Page Aggregate projection,最终触发 fallback 并拉长耗时。
长期规则应改为:
- 模型可以建议工具调用。
- 模型可以输出候选 operations。
- mnote 必须用 Page Aggregate projection 解析、校验、dry-run。
- blockId、revisionRef、allowedTargetBlockIds、editable、scope 必须由 mnote 校验。
- 未通过校验不能隐式 fallback 到整页写或另一次 agent run。
### 2.3 当前工具面还缺少 `cli-main` 式 agent 合同
`cli-main` 的关键价值是把平台能力压成 Agent 可靠调用的命令面:
- shortcut / API / generic 三层调用。
- `--dry-run` 预览真实请求。
- `Risk: high-risk-write``confirmation_required`
- structured error / hint。
- skill 文档指导 agent 何时调用什么。
- event consume 的 schema、ready marker、bounded run。
mnote 当前已有 Hermes tool manifest,但还需要把 manifest 提升为 Hermes/model 可直接消费的编辑合同,而不是只做 UI 列表。
---
## 3. 设计原则
### 3.1 单一 agent runtime
```text
Hermes owns:
session / message / model / tool loop / streaming / usage / profile / memory / skill
mnote owns:
Page Aggregate / tool manifest / context snapshot / validation / Rust command / audit / readback
```
页面 AI 面板只是 Hermes 的页面内客户端;mnote 不再新增独立 agent 编排中心。
### 3.2 本地层只做“路由和校验”
本地层可以做:
- 判断是不是低歧义块编辑。
- 生成 `recommendedToolCall`
- 附带 `confidence``risk``requiresReview`
- 生成 `allowedTargetBlockIds`
- 做 dry-run、validate、readback。
本地层不能做:
- 自己维护长期对话状态。
- 自己成为默认模型调用链。
- 自己绕过 Hermes tool manifest 和 profile 开关。
- 自己吞掉工具错误并隐式改走其他写入口。
### 3.3 所有写入都通过 Rust-owned mnote tools
写工具必须满足:
- `dryRun` 显式传入。
- `idempotencyKey` 显式传入。
- `revision/conflictDetectionKey/revisionRef` 或等价冲突键参与校验。
- `allowedTargetBlockIds` 限制 selection / scoped run。
- 返回 `diff/warnings/risk/blocked/changedBlocks/audit`
- 写入后通过 Page Aggregate 和 `mnote.doc.fetch` 回读验证。
### 3.4 快路径是 shortcut,不是 runtime
低歧义场景可以保留快路径,但必须改口径:
```text
PageAICommandRouter
-> recommendedToolCall
-> direct tool shortcut 或 Hermes run with tool hint
-> shared mnote tool executor
-> shared audit/readback
```
如果走 direct tool shortcut,也必须产生 Hermes-compatible tool event / audit 语义,避免 UI 与历史记录断裂。
---
## 4. 总体架构
```text
Browser Page AI panel
-> PageAIContextBuilder
-> MnoteAIToolManifestProvider
-> PageAICommandRouter
-> deterministic shortcut? ---- yes -> MnoteToolExecutor
| -> PageAIReviewSession/readback
no
-> Hermes run request with:
- frozen page context
- tool manifest
- recommendedToolCall hint
- risk/review policy
-> Hermes tool loop
-> /api/hermes/tools/mnote/call
-> Rust mnote tools
-> PageAIReviewSession/readback
```
这里 `PageAICommandRouter` 不是 agent,只是类似 `cli-main` shortcut 的工具路由器。
---
## 5. 组件设计
### 5.1 `PageAIContextBuilder`
职责:
- 从 Page Aggregate block projection 构建冻结上下文。
- 支持 `scope=full/outline/block/selection/keyword`
- 输出 `text/page_xml/json` 三种视图。
- 生成 `allowedTargetBlockIds`
- 记录 `revision/conflictDetectionKey/revisionRef`
- 大页面默认裁剪,返回 `truncated/warnings/continuation`
输出示例:
```json
{
"schema": "mnote.page_ai_context.v1",
"workspaceId": "tree_workspace",
"documentId": "tree_doc",
"scope": "selection",
"revision": 12,
"conflictDetectionKey": "body:12:hash",
"allowedTargetBlockIds": ["p_1", "p_2"],
"selectedBlockIds": ["p_1", "p_2"],
"pageText": "第一段\n第二段",
"pageXml": "<page id=\"tree_doc\" revision=\"12\"><block id=\"p_1\">第一段</block></page>",
"blocks": [
{
"blockId": "p_1",
"type": "paragraph",
"text": "第一段",
"revisionRef": "body:12:p_1",
"editable": true
}
]
}
```
### 5.2 `MnoteAIToolManifestProvider`
职责:
- 从 Rust Hermes tool manifest 输出当前页面可用工具。
- 合并 profile tool toggle、capability、scope、document permissions。
- 输出 Hermes/model 可直接使用的 tool schema。
- 输出风险和审批语义。
工具 manifest 必须包含:
```json
{
"name": "mnote.doc.apply_block_ops",
"description": "Apply validated block operations to the current mnote document.",
"inputSchema": {
"type": "object",
"required": ["operations", "dryRun", "idempotencyKey"],
"additionalProperties": false
},
"annotations": {
"readonly": false,
"destructive": false,
"idempotent": false,
"requiresApproval": true,
"approvalMode": "review",
"selectionEffect": "destroy",
"runtimeOwner": "mnote-web",
"writeOwner": "rust-runtime-kernel"
},
"availability": {
"enabled": true,
"unsupportedReason": ""
}
}
```
### 5.3 `PageAICommandRouter`
替代当前继续扩大的 `block-edit-workflow` 概念。
输入:
- 用户 prompt。
- 冻结后的 `mnote.page_ai_context.v1`
- 当前 tool manifest。
- 当前 profile / approval mode。
输出:
```json
{
"schema": "mnote.page_ai_command_route.v1",
"intent": "direct_block_edit",
"confidence": 0.94,
"recommendedToolCall": {
"toolName": "mnote.doc.apply_block_ops",
"args": {
"operations": [
{"op": "replace", "matchText": "A", "content": "B"}
],
"dryRun": true
}
},
"risk": "low",
"requiresHermesRun": false,
"requiresReview": false,
"reason": "明确中文引号替换表达,目标文本唯一命中"
}
```
规则:
- 只覆盖低歧义命令。
- 不能为复杂改写、总结、跨页面、多块结构化编辑直接生成写入。
- 不能调用第二套长链模型;如需模型,交给 Hermes run。
- 输出必须可被 Hermes 当作 tool hint 消费。
### 5.4 Hermes run hint 注入
`requiresHermesRun=true` 或 router 不确定时,页面 AI 发起 Hermes run,并附带:
```json
{
"pageContext": "mnote.page_ai_context.v1",
"toolManifest": "mnote.ai_tool_manifest.v1",
"toolHint": "mnote.page_ai_command_route.v1",
"reviewPolicy": {
"mode": "yolo|review|required",
"defaultDryRun": true
}
}
```
Hermes 仍负责:
- 选择模型。
- 工具调用循环。
- stream message / tool event。
- abort/retry。
- session persistence。
mnote 只负责工具结果和写入安全。
### 5.5 `PageAIReviewSession`
职责:
- 承接所有写工具 `dryRun=true``requiresApproval=true` 的结果。
- 保存 plan/diff/warnings/risk/blocked。
- 提供 accept/reject/retry/abort。
- accept 时二次读取 Page Aggregate 并校验 revision。
状态:
```text
draft
planning
previewing
awaiting_user
accepted
rejected
applying
applied
failed
aborted
stale
```
第一阶段可以保留 yolo,但仍应让工具返回 review-compatible 数据结构,避免后续 UI 重写。
---
## 6. 关键流程
### 6.1 低歧义块替换
```text
用户:把「第二段」替换为「第二段已修改」
-> ContextBuilder 冻结页面与 block ids
-> CommandRouter 命中 direct_block_edit
-> recommendedToolCall=mnote.doc.apply_block_ops
-> dryRun validate 唯一命中
-> yolo 模式:direct tool shortcut 正式 apply
-> 记录 tool event/audit
-> Page Aggregate readback
```
验收:
- 不进入通用 Hermes agent run 也可以,但必须复用 mnote tool/audit/readback 语义。
- 若非 yolo 模式,则停在 review session。
### 6.2 复杂自然语言改写
```text
用户:把这段整理得更专业,并保留原意
-> Router 无法确定操作
-> Hermes run with context + manifest + hint
-> Hermes 调 mnote.doc.fetch / block.fetch
-> Hermes 调 mnote.doc.plan_update(dryRun=true)
-> mnote 返回 review session draft
-> 用户 accept 后 Rust apply
```
验收:
- 模型不能直接改正文。
- dry-run 不改变 Page Aggregate。
- accept 时校验 revision。
### 6.3 selection 编辑
```text
用户选中块 A/B:改成列表
-> ContextBuilder 冻结 selectedBlockIds
-> allowedTargetBlockIds=[A,B]
-> 所有写工具自动带 allowedTargetBlockIds
-> 写工具尝试修改 C 时 blocked=true
```
验收:
- 用户后续改变选区不影响当前 run。
- selection 外写入被阻断。
### 6.4 工具禁用
```text
profile disabled mnote.block.fetch
-> ToolManifestProvider 输出 enabled=false 或不输出该工具
-> Router 不推荐该工具
-> Hermes 直接调用仍被 /api/hermes/tools/mnote/call 拦截
```
验收:
- UI 工具列表、Hermes manifest、后端执行拦截一致。
---
## 7. 与参考代码的吸收边界
### 7.1 `cli-main`
吸收:
- shortcut/API/generic 三层工具面。
- dry-run 作为写入前置能力。
- structured error/hint。
- risk/confirmation_required。
- skill 文档让 agent 不靠猜。
- event/schema/ready marker 的 agent-friendly contract。
不吸收:
- 不复制 Go CLI 框架。
- 不把 CLI 作为页面 AI 唯一执行面。
- 不用命令行 prompt 作为 Web 审批 UI。
### 7.2 `blocknote-ai`
吸收:
- `DocumentStateBuilder` 的 selection/full context 分离。
- `StreamToolsProvider` 的工具集合思想。
- AI lifecyclethinking / ai-writing / user-reviewing / error。
- accept/reject/retry/abort 的交互形态。
不吸收:
- 不引入 `@blocknote/xl-ai` 运行时依赖。
- 不复制 GPL/PROPRIETARY 代码。
- 不让 BlockNote/ProseMirror suggestion 成为 mnote 事实源。
### 7.3 `tiptap-apcore`
吸收:
- tool schema。
- annotations。
- ACL / role。
- query/content/destructive/selection/history 分类。
- executor 前置检查。
不吸收:
- 不把 Tiptap command 作为长期写入事实源。
- 不让浏览器 editor instance 直接持久化写入。
### 7.4 AI SDK / Context7 核验结论
可用方向:
- 用 schema/structured output 约束模型输出。
- 用 tool calling 让模型选择工具。
- 用 repair/validation 处理无效参数。
- 工具执行结果必须由 mnote 校验后返回。
不可用方向:
- 不把 structured output 当最终写入结果。
- 不让模型输出的 blockId 绕过 projection resolve。
---
## 8. 迁移计划
### Phase A:设计治理
- [x] 新增本文作为当前口径。
- [x] `7-10` 继续作为执行 checklist。
- [x] `7-11` 作为旧“自有 AI runtime”口径移入 `design/old/07-ai/process/`
### Phase BManifest 合同收口
- [ ] `mnote.doc.*` / `mnote.block.*` manifest 输出完整 `inputSchema/outputSchema/annotations/availability`
- [ ] profile toggle、capability、scope 共同影响 manifest。
- [ ] manifest 可直接转换为 Hermes/model tools。
- [ ] 禁用工具在 manifest、UI、执行拦截三处一致。
### Phase C`block-edit-workflow` 改造成 router
- [ ] 将 route 命名和返回 schema 改为 `mnote.page_ai_command_route.v1` 或新增等价 route。
- [ ] 本地规则只输出 `recommendedToolCall`
- [ ] 低风险 yolo shortcut 走共享 mnote tool executor。
- [ ] 非低风险或低置信度任务发起 Hermes run with tool hint。
- [ ] 删除“模型 fallback 后再 Hermes agent run”的重复链路。
### Phase DReview session
- [ ] 定义 `mnote.page_ai_review_session.v1`
- [ ] `mnote.doc.plan_update``mnote.doc.apply_block_ops dryRun=true` 返回 review-compatible draft。
- [ ] 页面 AI UI 展示 diff/warnings/risk/blocked。
- [ ] accept/reject/retry/abort 可用。
- [ ] stale revision 被阻断。
### Phase E:状态与事件统一
- [ ] direct shortcut 和 Hermes run 都产生统一 tool event 形态。
- [ ] 页面 AI 面板按 `runId/toolCallId/reviewSessionId` 聚合展示。
- [ ] abort 不留下半写入正文。
- [ ] 刷新后未提交 review session 不自动写入。
### Phase F:验收 smoke
- [ ] 低歧义替换:可 <1s 可见,且有 tool audit。
- [ ] 复杂改写:进入 Hermes run,先 dry-run/review。
- [x] selection 外写入:blocked。
- [ ] 禁用工具:manifest 不推荐,后端仍拦截。
- [ ] 旧 revision acceptstale。
2026-05-16 补充验收证据:
- `scripts/task-page-block-ai-context-format-smoke.js` 已验证 `mnote.doc.apply_block_ops dryRun=true` 携带 `allowedTargetBlockIds=["p_2"]` 时,尝试 replace `p_1` 会被 Rust mnote tool 拒绝。
- 证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`;错误路径为 HTTP `400``mnote_block_target_out_of_scope`
- 同一 smoke 还验证了 context / manifest 基础合同:`mnote.doc.fetch scope=selection format=page_xml/text``mnote.block.fetch format=page_xml/text`、manifest annotations 与 `mnote.page.save` 粗粒度兜底定位。
- 边界:本证据不代表完整 review session、旧 revision accept、复杂改写或单个 `mnote.block.*` selection guard 已完成。
---
## 9. `7-10` 与 `7-11` 的处理结论
### 9.1 `7-10` 继续执行
`7-10` 是页面块 AI 工具执行 checklist,包含真实代码和 smoke 证据。它仍然有效,继续保留在:
```text
design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md
```
但后续执行必须按本文修正口径:
- `PageAIIntentParser` 读作 `PageAICommandRouter`
- `PageAIOperationPlanner` 读作 `recommendedToolCall` 构造器。
- `PageAIOperationValidator` 继续有效,但归属 mnote tool executor / Rust validation。
- `PageAIApplyController` 不应成为独立 runtime,改为 review session / tool executor / readback controller。
- “不进入 Hermes run”只能表示 deterministic shortcut,不表示 mnote 新建了 agent runtime。
### 9.2 `7-11` 移入 old
`7-11` 的参考资料价值仍然成立,但标题和核心分层写成了“mnote 自有 AI 工具 runtime”。这会误导后续实现继续扩出第二套 runtime。
因此本轮将其移入:
```text
design/old/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md
```
保留原因:
- 记录 BlockNote / Tiptap 参考取证。
- 保留 GPL/PROPRIETARY 许可证边界。
- 保留 selection/context/review 的参考价值。
不再作为当前执行口径;当前执行口径以本文为准。
---
## 10. 禁止项
- 不新增 mnote 自有 agent runtime。
- 不把 `/api/page-ai/block-edit-workflow` 扩成通用 AI 编排中心。
- 不让模型直接输出未经校验的 blockId 并写入。
- 不绕过 Hermes profile/tool toggle/audit。
- 不让前端 editor instance 直接执行正式持久化写入。
- 不以 HTML / Tiptap JSON / ProseMirror position 作为长期 AI tool contract。
- 不复制 BlockNote XL AI 或 GPL/PROPRIETARY 实现代码。
- 不把 `mnote.page.save` 描述为精确块编辑主入口。
---
## 11. 成功标准
完成本文后,页面 AI 编辑应满足:
- 简单明确块编辑有低延迟 shortcut。
- 复杂编辑仍走 Hermes agent runtime。
- Hermes 不再盲猜工具和参数,而是拿到 mnote 提供的 context、manifest、tool hint。
- 所有写入都能 dry-run、review、audit、readback。
- 工具禁用、权限、scope、selection 与后端执行一致。
- 设计文档不再鼓励建设第二套 AI runtime。
@@ -0,0 +1,469 @@
# 7-13 [process] 页面块编辑运行时 Actor 设计 v1
> 更新时间:2026-05-22
>
> 当前状态:`PROCESS`
>
> 本稿目的:在 7-12 已排除第二套 AI runtime 的前提下,补上 Hermes tool execution → Convex 持久化之间缺失的 Rust 编辑运行时中继层,实现「内存态 apply → 编辑器就地 patch → Convex 异步持久化 → 事件增量通知」的四步闭环。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
---
## 1. 结论
当前 Hermes 块工具读→plan→dry-run→apply→readback 循环中,每次写操作都经历 `EditorCommand → legacy content → Convex documents:updateContent → page.body.saved` 全链路,导致:
- 一次 AI 编辑循环需 2-3 次 Convex RTT
- 编辑器只能全量 reload snapshot,不能就地 patch
- tree event stream 收到 `resync_required` 而非增量 delta
**正确方向不是绕开 Convex(禁止项,Convex 保留为自托管存储底座),而是在 Rust mnote-web 进程中新增一个轻量 EditorRuntimeActor,作为写操作的本地缓冲层。**
EditorRuntimeActor 不是 agent runtime(遵从 7-12 禁止项),它只负责:
- 持有文档的 `EditorBlockDocument` 内存态
- 接收 `EditorCommand` → 就地 apply → 产生 diff
- 将 diff 拆为三路输出:Convex 持久化 / 编辑器增量 patch / tree event stream delta
- 返回 Hermes tool 所需的 `changedBlocks / newRevision`
---
## 2. 现有问题
### 2.1 写路径绕路 Convex
当前写路径:
```
mnote.block.replace / insert_after / move_after / delete
→ ensure_write_contract
→ apply_editor_command_to_legacy_content ← EditorCommand → legacy content array
→ execute_page_body_save
→ RuntimeCommandEnvelopeWire("page.body.save")
→ bridge-runtime: EditorBlockDocument → legacy content → Convex documents:updateContent
→ domain event: page.body.saved
→ tree stream: resync_required
→ 前端收到 resync → 重新 fetch Page Aggregate → 编辑器 reload
```
这条路径每次写都走完整 Convex 事务。在 AI 的典型循环中(读 1 次 + plan_update 1 次 + write 1-3 次 + readback 1 次),这意味着 4-6 次 Convex RTT,其中大部分是可以省略的。
### 2.2 编辑器收不到增量
当前 `page.body.saved``resync_required` 是全量 reload。编辑器不会收到「块 p_2 的文本从 X 变为 Y」这样的增量信号,只能重新请求整页 snapshot。
### 2.3 每次 apply 都走 JSON 序列化桥
`apply_editor_command_to_legacy_content` 的输入是 `Value`legacy content array),输出也是 `Value`。中间经历了 `editor_document_from_legacy_content → apply → legacy_content_from_editor_document` 的序列化桥。如果 EditorBlockDocument 常驻内存,可以省去两端序列化。
---
## 3. 设计原则
### 3.1 不是 agent runtime
EditorRuntimeActor 不维护:
- session / message / model loop
- 意图解析 / planner / fallback 链
- 长期对话状态
- 独立的工具调用循环
它只是 Rust-owned 的命令执行 + diff 分发层。
### 3.2 Convex 仍是唯一的持久化底座
EditorRuntimeActor 的内存态允许异步写入 Convex,但不绕过 Convex。进程重启后从 Convex 恢复。
### 3.3 编辑器 patch 是增量,非全量
Rust → Tiptap 的 delta channel 只传 surgical opreplace/insert/delete/move),不传整份 `EditorBlockDocument`
### 3.4 事件 stream 从 resync 进化为 delta
`block.delta` 成为 tree event stream 的一等事件,前端 tree stream consumer 可选择增量消费。
---
## 4. 总体架构
```
┌──────────────────────┐
│ Hermes Agent │
│ (tool call loop) │
└──────────┬───────────┘
│ POST /api/hermes/tools/mnote/call
┌─────────────────────────────────────┐
│ mnote-web Hermes Tools (block.rs) │
│ - ensure_write_contract │
│ - build_editor_block / content_nodes│
│ - dry_run / idempotency / revision │
└──────────┬──────────────────────────┘
│ EditorCommand
┌────────────────────────────────────────────────────────────────┐
│ EditorRuntimeActor │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ per-document EditorBlockDocument cache │ │
│ │ apply command → update in-memory → produce diff │ │
│ │ diff → 3-way output: │ │
│ └──────┬──────────────┬──────────────────┬───────────────┘ │
│ │ │ │ │
└─────────┼──────────────┼──────────────────┼────────────────────┘
│ │ │
▼ ▼ ▼
Convex leptos-tiptap /api/tree/events
(async save) island (delta stream)
(page.body.save) (receive_command) (block.delta)
```
---
## 5. 组件设计
### 5.1 `EditorRuntimeActor`
```rust
pub struct EditorRuntimeActor {
// per-document 缓存
documents: RwLock<HashMap<DocumentId, EditorDocumentState>>,
// 未完成的 Convex 写入队列
pending_saves: SaveQueue,
}
```
`EditorDocumentState`
```rust
pub struct EditorDocumentState {
pub document_id: DocumentId,
pub workspace_id: Option<String>,
pub document: EditorBlockDocument,
pub revision: u64,
pub conflict_detection_key: String,
pub page_title: String,
pub last_applied_at: Instant,
pub pending_convex_save: Option<PendingSave>,
}
```
接口:
```rust
impl EditorRuntimeActor {
/// 读取或初始化文档的内存态
pub async fn load_or_init(
&self,
state: &AppState,
document_id: &str,
) -> Result<EditorDocumentGuard<'_>>;
/// 应用 EditorCommand,返回 diff
pub async fn apply_command(
&self,
document_id: &str,
command: EditorCommand,
context: &RequestContext,
) -> Result<ApplyResult>;
/// 触发异步 Convex 保存(不在工具返回路径上等)
pub fn schedule_save(
&self,
document_id: &str,
save_token: SaveToken,
);
/// 从 Convex 恢复文档到内存
pub async fn reload_from_convex(
&self,
state: &AppState,
document_id: &str,
);
}
```
### 5.2 `ApplyResult`
```rust
pub struct ApplyResult {
pub new_revision: u64,
pub changed_blocks: Vec<ChangedBlock>,
pub diff: BlockDelta,
pub warnings: Vec<String>,
pub blocked: bool,
}
pub struct ChangedBlock {
pub block_id: String,
pub op: &'static str, // "replace" | "insert" | "delete" | "move"
pub before: Option<String>, // 文本预览(dry-run 展示用)
pub after: Option<String>,
}
/// 增量 diff,用于推送编辑器 + event stream
pub struct BlockDelta {
pub document_id: String,
pub revision: u64,
pub operations: Vec<DeltaOperation>,
}
pub enum DeltaOperation {
ReplaceBlock {
block_id: String,
content: EditorBlock,
},
InsertBlockAfter {
anchor_block_id: String,
block: EditorBlock,
},
DeleteBlock {
block_id: String,
},
MoveBlock {
block_id: String,
new_parent_block_id: Option<String>,
new_order: String,
},
}
```
### 5.3 `SaveQueue`
Convex 写入不阻塞工具返回。`SaveQueue` 负责:
- 收集 50ms 窗口内的连续改动(同一文档去重)
- 合并为一次 `page.body.save` command
-`revision` 乐观锁;失败时触发 reload 补偿
- 记录上一次成功 save 的 `conflictDetectionKey`
### 5.4 `EditorDeltaChannel`Phase B
Rust → leptos-tiptap 的增量通道:
```rust
pub struct EditorDeltaChannel {
// per-document sender (wasm-bound callback or WebSocket)
senders: RwLock<HashMap<DocumentId, DeltaSender>>,
}
pub enum DeltaSender {
/// 同进程 wasm bridgecurrent spike pattern
WasmBridge(Box<dyn Fn(BlockDelta) + Send>),
/// WebSocket 直连(未来备选)
WebSocket(String),
}
```
在 leptos-tiptap island 侧:
```js
// 新增入口
window.__mnote_editor_receive_delta = function(delta) {
// delta.operations.forEach(op => {
// editor.chain().findBlockById(op.block_id).replaceWith(op.content).run()
// })
};
```
---
## 6. Phase 划分
### Phase AEditorRuntimeActor 内存缓存层
目标:消除每次工具调用都走 Convex 的读→写回环。
任务:
- [ ] 实现 `EditorRuntimeActor` 结构体,持有 `HashMap<DocumentId, EditorDocumentState>`
- [ ] 实现 `load_or_init`:首次读取从 Convex Page Aggregate 构建 `EditorBlockDocument` 内存态
- [ ] 实现 `apply_command`:直接在 `EditorBlockDocument.blocks` 上执行 apply,产生 `ApplyResult`
- [ ] 实现 `schedule_save`:异步 `page.body.save` 到 Convex,带 revision 乐观锁
- [ ] 改造 `block.rs``execute_page_body_save`:优先走 EditorRuntimeActor::apply_command,再 schedule_save
- [ ] 工具返回不再等待 Convex 完成,带上 `newRevision + changedBlocks` 立即返回
- [ ]`task-editor-runtime-actor-smoke.js`:验证三次写入循环的 latency < 500ms(不含 Convex 持久化)
依赖:
- `EditorRuntimeActor` 可独立启用/禁用(feature flag),启用时不影响已有写路径
- Phase A 不改编辑器前端,不改 tree event stream
### Phase B:编辑器增量 delta channel
目标:AI 写入后 leptos-tiptap 编辑器就地 patch,不触发全量 reload。
任务:
- [ ] 定义 Rust → Editor 的 delta 序列化协议(基于 `BlockDelta` 序列化为 JSON
- [ ] 在 leptos-tiptap spike 的 wasm 侧新增 `receive_delta(delta_json: &str)` 函数
- [ ] 新增 JS 入口 `window.__mnote_editor_receive_delta`,解析后通过 Tiptap chain API 执行
- [ ] EditorRuntimeActor 在 apply_command 后通过 `EditorDeltaChannel` 推送 delta
- [ ] 处理冲突:如果编辑器本地 state 比内存缓存更新,跳过该条 delta(等下次全量 sync)
- [ ]`task-editor-delta-channel-smoke.js`:验证 AI write → 编辑器镜像变化不需 reload
依赖:
- Phase A 已完成
- leptos-tiptap 的 `editor` 引用可从 wasm 侧稳定访问
- delta channel 只在 `leptos-tiptap` 作为主编辑器的文档页启用
### Phase C:事件 stream delta
目标:`block.delta` 成为 tree event stream 的一等事件,前端不再依赖 `resync_required`
任务:
- [ ] 新增 event type `block.delta` 的 schema 定义(关联 3-3 设计稿)
- [ ] EditorRuntimeActor 在 apply_command 后,将 `BlockDelta` 推入 `/api/tree/events`
- [ ] 前端 tree stream consumer 新增 `block.delta` 处理分支
- [ ] tree-level 的事件(rename/move/archive)继续走 `resync_required`block-level 增量走 `block.delta`
- [ ]`task-block-delta-smoke.js`:验证第二客户端收到 block.delta 后页面内容更新
依赖:
- Phase A 已完成
- `/api/tree/events` 已有 snapshot/delta/resync 机制(参考 3-3
---
## 7. 关键流程
### 7.1 AI 块替换(Phase A + B
```text
用户/Agent: 把「第二段」替换为「第二段已修改」
→ Hermes 调 mnote.block.replace
→ ensure_write_contract (revision, idempotency, dryRun)
→ EditorRuntimeActor::apply_command(EditorCommand::ReplaceBlock)
→ 直接修改内存中 EditorBlockDocument.blocks["p_2"]
→ 产生 ApplyResult { newRevision: 14, changedBlocks: [...], delta: BlockDelta }
→ dryRun? 返回 preview (不同,跳过写入)
→ schedule_save (返回后异步执行)
→ EditorDeltaChannel::push(delta) → leptos-tiptap 就地修改
→ 返回 { ok, changedBlocks, newRevision }
```
延迟特征:
- Hermes tool 返回:~5ms(内存操作,无 Convex RTT
- Convex 持久化:~50-200ms(后台异步,不阻塞 agent loop)
- 编辑器更新:~5mswasm bridge 直接调用 Tiptap chain
### 7.2 复杂改写(Hermes agent 场景)
```text
用户: 把这段改得更专业
→ Hermes agent: mnote.doc.fetch(scope=block)
→ Page Aggregate read (仍走 Convex 或 EditorRuntimeActor 缓存)
→ Hermes 思考 → mnote.doc.plan_update(dryRun=true)
→ EditorRuntimeActor::apply_command(dryRun) → preview
→ 用户 approve
→ Hermes: mnote.block.replace (dryRun=false)
→ 同 7.1 流程
```
### 7.3 进程重启恢复
```text
mnote-web 重启
→ 第一次收到某文档的 tool call
→ EditorRuntimeActor::load_or_init
→ 从 Convex Page Aggregate 读取
→ 构建 EditorBlockDocument 内存态
→ 设置 revision = 读取值
→ 正常处理后续 commands
```
---
## 8. 与现有文档的边界
| 现有设计 | 与本稿关系 |
| --- | --- |
| 7-12 禁止「第二套 AI agent runtime」 | 严格遵从。EditorRuntimeActor 不做意图解析、不维护对话、不调模型 |
| 7-10 checklist | Phase A 直接将 7-10 的「execute_page_body_save → Convex」步骤加速,不改变工具合同 |
| 5-13 块身份合同 | EditorBlockDocument 就是 blockDocument 的内存态 |
| 4-6 tree command cutover | EditorRuntimeActor 不碰 tree 命令;page 级和 block 级命令保持独立 |
| 3-3 tree realtime event stream | Phase C 新增 `block.delta` event,扩展而非替代 resync_required |
---
## 9. 禁止项
- 不绕过 Convex 持久化。EditorRuntimeActor 是缓存层,不是存储层。
- 不在 EditorRuntimeActor 内维护 agent session、message history、model 调用。
- 不在 EditorRuntimeActor 内做意图解析、planner、fallback 判断。
- 不要求编辑器同步等待 Convex 写入完成才展示 AI 编辑结果。
- 不改变已有的 `ensure_write_contract` 校验链。
- 不新增写工具;Phase A/B/C 只加速已有工具的落地速度。
- delta channel 不改写 Tiptap 的协作/undo/redo 栈;仅新增 AI 编辑的增量入口。
---
## 10. 成功标准
Phase A 完成后:
- [ ] Hermes block 工具(replace/insert_after/move_after/delete)返回时间不依赖 Convex RTT
- [ ] 三次写循环(replace → insert → readback)总 agent 延迟 < 800ms(含 dry-run
- [ ] Convex `documents:updateContent` 调用次数不变(1 次/写,异步)
- [ ] 所有现有 smoke 用例在 feature flag 开启/关闭下均通过
Phase B 完成后:
- [ ] AI 写入后,编辑器中对应块的文本/类型 3ms 内更新
- [ ] 编辑器选区、undo 栈、协作标记不受影响
- [ ] 编辑器不触发额外的 fetch / reload 请求
Phase C 完成后:
- [ ] block-level 编辑不再产生 `resync_required` 事件
- [ ] 第二客户端收到 `block.delta` 后页面内容与第一客户端一致
- [ ] tree event stream 兼容旧客户端(旧客户端看到 resync_required 降级路径)
---
## 11. 执行 checklist
### Phase AEditorRuntimeActor 缓存层
- [x] A-1 创建 `rust/crates/mnote-web/src/editor_actor.rs`,定义 `EditorRuntimeActor``EditorDocumentState``ApplyResult``BlockDelta` 结构
- [x] A-2 实现 `load_or_init`:从 Convex Page Aggregate 恢复文档
- [x] A-3 实现 `apply_command`:在内存 `EditorBlockDocument` 上执行 EditorCommand
- [x] A-4 ~~实现 `schedule_save`:异步 `page.body.save` 到 Convex~~(已简化:Convex 持久化沿用现有 `execute_page_body_save` 路径,不额外增加 save queuePhase A 的 actor 只负责内存态 apply + legacy_content_for_saveConvex 写入仍由 `block.rs` 同步完成)
- [x] A-5 改造 `block.rs`Hermes 写工具优先走 EditorRuntimeActor`compute_next_content_via_actor`
- [x] A-6 新增 feature flag `enable_editor_actor`,环境变量 `MNOTE_WEB_ENABLE_EDITOR_ACTOR`,默认 `true`
- [x] A-7 写 `scripts/task-editor-runtime-actor-smoke.js`
- [ ] A-8 现有 Hermes block smoke 全部通过(`cargo test` 通过,Playwright 全量测试需要 running server 手动执行)
### Phase B:编辑器增量 delta channel
- [x] B-1 ~~定义 `EditorDeltaChannel`、`DeltaSender` 结构~~(已降级:delta 直接通过 tool response 的 `blockDelta` 字段返回,不单独建 channel)
- [x] B-2 在 leptos-tiptap spike 的 wasm 侧新增 `receive_delta` 入口(已实现:`apply_block_delta_to_json` 函数 + `mnote:editor:block-delta` CustomEvent 监听 + `TiptapContent::json` 设置回编辑器;替换策略而非 surgical ProseMirror ops,确保编辑器 undo 栈基本完好)
- [x] B-3 在 Rust 侧推送 `BlockDelta` 到 delta channel(已实现:`actor.build_block_delta()` 产出 delta JSON`block.rs` 四个写工具响应中已含 `blockDelta` 字段)
- [ ] B-4 处理冲突场景(编辑器本地 state 更新的跳过策略)(待下一轮:实现 revision 比对,编辑器本地 revision > delta revision 时跳过)
- [x] B-5 写 `scripts/task-editor-delta-channel-smoke.js`
- [ ] B-6 验证 AI write → 编辑器无损更新(选区不丢失、undo 可回退)(环境 rustc 1.89 限制 spike 编译,需在 1.89+ 环境下编译 spike WASM + 启动 mnote-web 后跑 smoke 脚本验证)
### Phase C:事件 stream delta
- [x] C-1 更新 3-3 事件 schema 增加 `block.delta` event typeSSE event name `"block.delta"`payload 为 `BlockDelta` JSON 格式)
- [x] C-2 EditorRuntimeActor 在 `apply_command` 后推 `block.delta``/api/tree/events`(通过 `broadcast::Sender<Value>` + SSE 消费实现)
- [ ] ~~C-3 前端 tree stream consumer 新增 `block.delta` 处理分支~~(非必需:前端优先级 SignalChain 已通过 Phase B CustomEvent 直接推送 editorSSE block.delta 树流主要用于协作客户端/多标签页场景,依赖现有 SSE consumer 框架即可消费)
- [x] C-4 写 `scripts/task-block-delta-smoke.js`
- [x] C-5 旧客户端降级兼容验证(SSE consumer 按 event name 分派,未注册 handler 自动跳过,无崩溃风险)
### DONE 条件
- [ ] Phase A / B / C 全部完成
- [ ] 每条 checklist 项有 smoke 证据
- [ ] 所有已有相关的 Hermes tool smoke 回归通过
- [ ] 本设计稿从 `process/` 移至 `done/`
- [ ] ARCHITECTURE.md 8.4 节更新引用
+6 -3
View File
@@ -1,8 +1,11 @@
# 10-review 审查总览
> 执行状态:`done/01` 到 `done/07` 均已归档。
> 执行状态:`done/01` 到 `done/07` 均已归档`process/08` 是当前活跃架构收口与下一阶段优先级 review`process/09` 记录页面 AI 快速块编辑 runtime 的阶段性结论与下一步基建方向
当前活跃审查
当前活跃审查
- [Kernel 架构收口与下一阶段优先级 Review / Checklist](./process/08-kernel-architecture-next-priority-review-and-checklist.md)
- [页面 AI 快速块编辑 Runtime Review](./process/09-page-ai-fast-block-edit-runtime-review.md)
最新归档审查:
@@ -24,6 +27,6 @@
- `done/01``done/04` 是历史偏差审查快照;其中旧的“未完成 / 风险”条目已由 `done/06` 承接闭环,或转入对应主线设计文档继续跟踪。
- `done/05-tree.md` 的 Resource Tree / File Tree / Page Tree、ObjectIdentity、mindmap 与 `index.md` 隔离主线已由 `design/04-tree-domain/done/4-24-*``design/05-editor-mainline/done/5-12-*` 承接完成。
- 2026-05-15 起,Convex File Tree 的默认可见页面正文行由 `design/04-tree-domain/process/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖为 `doc:<documentId>` + `{title}.md``done/05-tree.md``4-24``5-12` 中的 `index.md` 表述仅作为历史 object identity / 兼容语义理解,不再作为默认 UI 可见模型继续派生新任务。
- 2026-05-15 起,Convex File Tree 的默认可见页面正文行由 `design/04-tree-domain/done/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖为 `doc:<documentId>` + `{title}.md``done/05-tree.md``4-24``5-12` 中的 `index.md` 表述仅作为历史 object identity / 兼容语义理解,不再作为默认 UI 可见模型继续派生新任务。
- `done/06-execution-checklist-and-acceptance.md` 是上一轮 10-review 的最终验收依据;后续只作为防回归和口径核验材料。
- `done/07-vscode-explorer-filetree-trash-gap-review.md` 是 07-ai 开发前对 04-tree 文件树 / 页面树多选、默认删除进垃圾箱、垃圾箱恢复与永久删除、资源级 `tree.resource.*`、Convex purge 同步、VSCode Explorer 体验对标的增量审查归档。该审查 checklist 已闭合;后续仍应按文档中的“最低可用完成 / parity backlog”口径描述 VSCode Explorer 对标,不要把禁用态或待增强项说成完整 parity。
@@ -15,7 +15,7 @@
- `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
- `design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md`
- `design/04-tree-domain/process/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
- `design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
- `design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
@@ -300,7 +300,7 @@
- `wolai-frontend/src/components/editor/page-aggregate-client-state.test.ts`:覆盖本地正文/标题变化后生成临时 `pageSubtree`,以及 server snapshot 下 `pageSubtreeSource=server`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`:历史 Rust 3000 全局浮动页面 AI 曾在发送旧 `/api/ai-agent/run` 前读取当前 Leptos/Tiptap 编辑器 DOM;当本地内容与 server aggregate 正文不同,生成 `source=local` 的临时 subtree / outline / evidence,并在 context 中发送 `pageSubtreeSource=local`。2026-05-14 起这只保留为历史上下文证据;当前页面 AI 主链已改为 Hermes client proxy,页面上下文进入 Hermes run/session context,最新页面事实由 Hermes 通过 mnote plugin tool 回读。
- 已通过:`pnpm test -- src/components/editor/page-aggregate-client-state.test.ts src/components/editor/DocumentAiAgentPanel.runtime.test.tsx src/components/editor/document-content.test.ts`Vitest 实际执行 111 个测试文件、454 个测试)。
- 已通过:`node scripts/task178-page-ai-local-subtree-context-smoke.js`,Rust 3000 下打开真实文档页,把编辑器内容改成本地 heading 后打开页面 AI,并拦截旧 `/api/ai-agent/run` 请求确认 `context.pageSubtreeSource=local``documentBlocks` / `outline` / `subtree.stats.headingCount` 均包含本地最新 heading。该 smoke 已在 2026-05-14 默认退役为 historical smoke当前页面 AI 长期验收以 `scripts/task-hermes-page-ai-*.js` 矩阵为准。
- 已通过:`node scripts/task178-page-ai-local-subtree-context-smoke.js`,Rust 3000 下打开真实文档页,把编辑器内容改成本地 heading 后打开页面 AI,并拦截旧 `/api/ai-agent/run` 请求确认 `context.pageSubtreeSource=local``documentBlocks` / `outline` / `subtree.stats.headingCount` 均包含本地最新 heading。该 smoke 已在 2026-05-14 默认退役为 historical smoke脚本现本机归档于 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`;当前页面 AI 长期验收以 `scripts/task-hermes-page-ai-*.js``scripts/task-page-block-ai-tools-smoke.js` 矩阵为准。
- 已通过:`pnpm exec eslint src/components/editor/page-aggregate-client-state.ts src/components/editor/page-aggregate-client-state.test.ts src/components/editor/DocumentAiAgentPanel.tsx src/components/editor/DocumentAiAgentPanel.runtime.tsx` 无错误;`DocumentAiAgentPanel.runtime.tsx` 保留既有 `any` warning。
## 8. P2:清理或标注 stale mapping / legacy compat
@@ -0,0 +1,494 @@
# 08 [process] Kernel 架构收口与下一阶段优先级 Review / Checklist v1
> 更新时间:2026-05-16
>
> 执行状态:`process`
>
> 关联文档:
> - `/mnt/Data1T/mnote/AGENTS.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
---
## 1. Review 结论
当前内核架构的骨架已经基本成立,但还不能说“已完善”。
已经成立的部分:
- `tree-first graph kernel` 是长期语义事实源。
- `mnote-web` 已是 `3000` 主 Web 执行面。
- 文档页默认主编辑器已切到页面内 `leptos-tiptap` island。
- Page Aggregate 已进入 Rust-first 读取链,并已输出 `blockDocument / blockProjectionVersion / projectionSource`
- 页面/块 AI tools 已开始走 `mnote.doc.*` / `mnote.block.*`、Page Aggregate block projection 和 Rust `EditorCommand`
仍未完善的部分:
- Page Aggregate 仍是过渡态,block projection 主要从 `documents.content` / local markdown content 投影,不是 EditorBlockDocument 原生落库完成态。
- 标题、正文、页面设置、page tree、AI 写入口尚未完全闭环到同一组 projection / command family。
- `tree.*` 已是 preferred command name,但 `documents.*` 兼容命令面仍未完全降级。
- `/api/tree/events` 已是 tree realtime 主链,但 Sidebar、page subtree、filetree、preferred snapshot 还没有完全统一到一套 live cache。
- AI 块工具已有最小闭环,但 selection、page_xml/text、多块插入、复杂块移动矩阵、持久 preview/review UI 和冲突矩阵仍未补齐。
补充 BlockNote / Tiptap AI 参考后的判断:
- BlockNote AI 和 Tiptap AI Toolkit 可以参考的是 AI runtime shape 与 tool contract,不是 mnote 的事实源。
- AI 方向下一步只应补基础底座:`PageAIContextBuilder``scope=selection``page_xml/text`、tool manifest annotations、`PageAIReviewSession`、accept/reject/retry/abort 状态机。
- 在非 AI 主架构和 AI 基础合同打牢之前,不应扩展新的 AI agent 工作流、复杂 AI UI 或跨页面智能功能。
因此,下一阶段不应优先“大改架构”或“大量加新功能”,而应优先做:
> **架构收口 + 验收矩阵 + 定向找 bug。**
更具体地说:
> **先打牢除 AI 以外的 Page Aggregate / tree command / tree realtime 主架构,再把 AI 的基础读写、上下文、冲突与审阅底座打牢;之后才进入 AI 功能扩展。**
---
## 2. 下一步总优先级
总原则:
- 非 AI 主架构优先级高于 AI 功能扩展。
- AI 当前只推进基础设施,不推进新功能面。
- AI 基础设施必须服从 Rust kernel / Page Aggregate / EditorCommand / Hermes audit,不引入 BlockNote runtime 或 Tiptap/ProseMirror editor truth。
### P0Page Aggregate 单一真源收口
目标:
- 标题、正文、页面设置、page tree、AI 读写入口继续收敛到同一组 Page Aggregate projection / command family。
- 前端不在页面壳、island 外侧、Sidebar preferred snapshot 外再拼第二份页面真相。
- 明确当前 block projection 过渡态和长期 EditorBlockDocument 原生落库目标之间的边界。
判断标准:
- 页面读取只走 Rust `/api/page-aggregate/:id` 正式读链。
- 页面标题、正文、页面设置写入后,Page Aggregate、页头、Sidebar、Breadcrumb、File Tree、AI fetch 回读一致。
- 失败时明确 degraded / conflict / stale,不返回看似成功的旧快照。
### P0Tree Command Cutover
目标:
- `tree.*` 成为正式命令面。
- `documents.*` 只保留为兼容层,不再继续扩写长期业务语义。
- 页面新建、重命名、移动、归档、恢复、删除、资源生命周期都能通过正式 tree/page command 解释。
### P0Tree Realtime Live Cache 统一
目标:
- `/api/tree/events` 的 snapshot / delta / resync 成为 Sidebar、Page Tree、File Tree、page subtree 的共同 live cache 来源。
- 减少 query/refetch/freshness 补偿链和旧快照回闪。
### P0AI 基础工具与审阅底座
目标:
- 不继续扩新 AI 功能,先把当前 `mnote.doc.*` / `mnote.block.*` 变成可靠、可测、可审阅、可回滚的基础工具链。
- 参考 BlockNote / Tiptap 的架构形态,但保持 mnote Rust-owned tool runtime。
必须补齐:
- `PageAIContextBuilder``scope=selection`
- `format=page_xml/text/json` 的稳定输出边界。
- tool manifest annotations`readonly/destructive/requiresApproval/selectionEffect/runtimeOwner/writeOwner`
- `PageAIReviewSession`preview -> accept/reject/retry/abort。
- 多块插入的边界与 inserted block ids。
- 复杂块移动阻断矩阵。
- stale revision / stale blockRevisionRef / idempotency 重放。
明确不做:
- 不新增跨页面 AI agent 工作流。
- 不新增复杂 AI 自动化功能面。
- 不把 `@blocknote/xl-ai` 或 BlockNote `AIExtension` 作为 runtime dependency。
- 不让 AI 写入绕过 `dryRun/idempotencyKey/revision/conflictDetectionKey/revisionRef`
### P1:定向 Bug Hunt
目标:
- 不做泛泛“找 bug”,只围绕已知架构风险做定向排查。
优先找:
- Page Aggregate 回流不一致。
- AI 块写入 conflict / stale revision。
- File Tree `{title}.md` 与 page title sync。
- tree stream resync / 双浏览器一致性。
- debug / compat / fallback 是否混入主链。
---
## 3. 可执行 Checklist
### 3.1 Page Aggregate 单一真源
- [x] 盘点 `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 是否仍有 runtime fallback 或 TS builder 读取分支。
- [x] 盘点 `wolai-frontend/src/lib/documents/page-aggregate-builder*` 的引用,确认只剩 test helper / historical adapter。
- [x] 检查 `/api/documents/page` 仍返回明确 `410`,不参与 runtime 主链。
- [x] 跑文档页打开 smoke,记录 `/api/page-aggregate/:id` 是首要读链。
- [x] 新建页面后检查 Page Aggregate `identity/head/body/tree/stats` 字段完整。
- [x] 修改标题后检查 Page Aggregate、页头、Breadcrumb、Sidebar、File Tree `{title}.md` 同步。
- [x] 修改正文后检查 Page Aggregate `body.revision/conflictDetectionKey/blockDocument` 同步。
- [x] 修改页面设置后检查 Page Aggregate 与 island runtime page options 同步。
- [x] 刷新页面后检查标题、正文、页面设置不回退到旧快照。
- [x] 破坏或暂停 Convex query,检查响应是 degraded/error,不返回伪 fixture。
- [x] 在 `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` 勾选已验证项,并补证据路径。
2026-05-16 静态审计证据:
- `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 只通过 `loadPageAggregateFromRustSnapshot` 请求 Rust `/api/page-aggregate/:documentId``loadPageAggregate` 不再回退 TS builder`buildServerBridgeRequest("/documents/page")` 只用于构造转发 header/request,不调用 Next `/api/documents/page` compat route。
- `rg -n "buildPageAggregateFromDocumentPayloads|page-aggregate-builder" wolai-frontend/src rust scripts design --glob '!design/05-editor-mainline/reference-code/**' --glob '!node_modules/**'` 显示 runtime 非测试引用仅剩 builder 定义;代码引用只有 `wolai-frontend/src/lib/documents/page-aggregate-builder.test.ts`
- `wolai-frontend/src/app/api/documents/page/route.ts` 明确返回 `410`,错误文案指向 `/api/page-aggregate/:documentId``route.test.ts` 覆盖该行为。
- 已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate``cd wolai-frontend && pnpm test src/app/api/documents/page/route.test.ts src/lib/documents/page-aggregate-builder.test.ts`
- 全仓 `git diff --check` 当前被既有删除文件 `rust/spikes/leptos-tiptap-spike/trunk-8123.err` 阻断:`fatal: unable to generate checkdiff for ...`;本轮未清理该无关脏改动,已改用限定文件 diff check 复核。
2026-05-16 真实 3000 smoke 证据:
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task110-page-title-single-truth-smoke.js` 通过,证据:`tmp/page-aggregate-single-truth-smoke/20260516-182244/task110.stdout.json`。该脚本创建两页、修改标题,并验证页头、Breadcrumb、Sidebar、Page Tree、File Tree 与刷新后标题一致。
- 文档页打开与 Page Aggregate snapshot 验证通过,证据:`tmp/page-aggregate-single-truth-smoke/20260516-182244/page-open-snapshot.stdout.json`,截图:`tmp/page-aggregate-single-truth-smoke/20260516-182244/page-open-snapshot.png`。文档响应 `x-mnote-web-owner=mnote-web``x-mnote-web-shell=document`HTML 包含 `data-page-aggregate-snapshot="mnote.page_aggregate.v1"``data-page-tree-source="page_aggregate.tree.pageSubtree"`,同一临时页 `/api/page-aggregate/:id` 回读 `schema=mnote.page_aggregate.v1`
- 新建页面 Page Aggregate 字段完整性验证通过,证据:`tmp/page-aggregate-single-truth-smoke/20260516-182244/page-aggregate-fields.stdout.json``identity/head/body/tree/stats` 全部为 true`body.blockProjectionVersion=1``projectionSource=documents.content`
- 说明:曾尝试在浏览器 network 中直接捕获 `/api/page-aggregate/:id`,证据 `page-open-network.stderr.log`;当前 Rust SSR 主入口会直接把 Page Aggregate snapshot 写入 HTML,浏览器侧不必出现该 API 请求,因此该尝试不作为失败验收项。
2026-05-16 正文写入后 body 同步证据:
- 新增并运行 `scripts/task-page-aggregate-body-sync-smoke.js`,通过真实 3000 文档页输入正文,等待 `/api/documents/save` 成功后轮询 `/api/page-aggregate/:id`
- 证据:`tmp/page-aggregate-body-sync-smoke/mp87mgz7.json`,截图:`tmp/page-aggregate-body-sync-smoke/mp87mgz7.png`stdout`tmp/page-aggregate-body-sync-smoke/latest.stdout.json`
- 验证结果:`body.revision``0` 更新到 `1``body.conflictDetectionKey``tree_1778927703753_1:0` 更新到 `tree_1778927703753_1:1``body.blockProjectionVersion=1``projectionSource=documents.content``blockDocument.blocks[0]` 回读到文本 `Page Aggregate body sync mp87mgz7``revisionRef=pageRev:1:block:block_1:hash:fnv1a64:208e0e63c853eef5`
- 配套测试已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_save_route_executes_page_body_save_command``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate_get_projects_legacy_content_to_block_document`
2026-05-16 页面设置写入后 pageOptions 同步证据:
- 新增并运行 `scripts/task-page-aggregate-options-sync-smoke.js`,通过真实 3000 文档页打开页面设置,依次修改 `wideLayout=true``smallText=true``layoutDensity=compact`,等待 `/api/documents/options` 返回 `page.layout.updateOptions` 后轮询 `/api/page-aggregate/:id`
- 证据:`tmp/page-aggregate-options-sync-smoke/mp87y6j3.json`,截图:`tmp/page-aggregate-options-sync-smoke/mp87y6j3.png`stdout`tmp/page-aggregate-options-sync-smoke/latest.stdout.json`
- 验证结果:Page Aggregate `layout.pageOptions` 依次回读到 `wideLayout=true``smallText=true``layoutDensity=compact`;运行时 DOM 同步为 `data-page-wide-layout="true"``data-page-small-text="true"``data-layout-density="compact"`island `editorRoot``.editor-surface` 同步,字号从 `16px``15px`,段落间距从 `8px``4px`
- 配套测试已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options``cd wolai-frontend && pnpm test src/lib/documents/page-command-client.test.ts src/components/editor/leptos-tiptap-island-editor-host.test.tsx src/lib/documents/page-option-semantics.test.ts`
2026-05-16 刷新后标题 / 正文 / 页面设置不回退证据:
- 新增并运行 `scripts/task-page-aggregate-refresh-persistence-smoke.js`,同一临时页内依次通过 UI 写入标题、正文和页面设置,等待 Page Aggregate 回读最新 `head/body/layout` 后刷新页面,再断言页头、正文 DOM、页面设置控件、runtime DOM 与 `/api/page-aggregate/:id` 均保持最新值。
- 证据:`tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.json`,截图:`tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.png`stdout`tmp/page-aggregate-refresh-persistence-smoke/latest.stdout.json`
- 验证结果:刷新前后 `head.title=Page Aggregate refresh mp88fr6k``body.revision=1``body.conflictDetectionKey=tree_1778929070007_1:1``blockDocument.blocks[0].text=Page Aggregate refresh body mp88fr6k``layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact`;刷新后页头标题、`.ProseMirror` 正文、设置控件、`documentElement``.document-shell`、island root 和 `.editor-surface` 均保持最新值。
- 修复点:`rust/crates/mnote-web/src/ssr/pages/layout.rs``initializePageUiSurfaces` 延后到 `DOMContentLoaded` 后执行,避免 layout 脚本早于嵌入 Page Aggregate JSON / island DOM 完成时把 runtime 属性按默认 pageOptions 应用。
- 缺陷记录:`bugs/05-editor-mainline/done/5-12-page-options-refresh-runtime-attrs-v1.md`
- 配套测试已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options`
2026-05-16 Convex query 失败时不返回伪 fixture 证据:
- 在 `rust/crates/mnote-web/src/routes/web_shell.rs` 补充负向 route 测试,构造隔离配置 `convex_url=http://127.0.0.1:9``allow_dev_fixtures=false``query_fixtures_json=None`,模拟 Convex query 不可达。
- `/api/page-aggregate/doc_1?workspaceId=ws_demo` 返回 `503 SERVICE_UNAVAILABLE``x-error-code=convex_unavailable``x-error-phase=query_send``x-upstream-service=convex`body 为 `ok=false/code=convex_unavailable`,且没有 `schema` / `result`
- `/documents/doc_1?workspaceId=ws_demo` 返回同类错误 JSONHTML body 不包含 `mnote.page_aggregate.v1``data-mnote-dev-fixture``data-page-aggregate-snapshot`
- 验证命令已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate_endpoint_errors_without_convex_or_fixture -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_without_convex_or_fixture -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate`
验证命令建议:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate
node scripts/task110-page-title-single-truth-smoke.js
node scripts/task-page-aggregate-body-sync-smoke.js
node scripts/task-page-aggregate-options-sync-smoke.js
node scripts/task-page-aggregate-refresh-persistence-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate_endpoint_errors_without_convex_or_fixture
cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_without_convex_or_fixture
```
### 3.2 Page Block AI Tooling
- [ ] 以 `7-10` 为工具执行清单、`7-11` 为 AI runtime 基础设计,逐 phase 检查已勾选项是否都有代码、测试、smoke 证据。
- [x] 给 `mnote.doc.fetch scope=selection` 补真实选区上下文输入和返回结构。
- [x] 给 `mnote.doc.fetch format=page_xml` 补最小 PageXML 输出。
- [x] 给 `mnote.block.fetch format=text/page_xml` 补格式分支。
- [x] 给 tool manifest 补 `annotations`,区分 readonly / destructive / requiresApproval / selectionEffect。
- [x] 定义 `mnote.page_ai_context.v1`,明确 context 只来自 Page Aggregate projection。
- [ ] 定义 `mnote.page_ai_review_session.v1`,明确 accept / reject / retry / abort 语义。
- [ ] 给 `mnote.block.insert_after` 补多块插入限制和返回 inserted block ids。
- [ ] 给 `mnote.block.move_after` 补标题带子块阻断 smoke。
- [ ] 给 `mnote.block.move_after` 补列表项阻断 smoke。
- [ ] 给 `mnote.block.move_after` 补表格 / mindmap / resource 阻断 smoke。
- [x] 给 `mnote.block.replace` 补 stale revision 失败用例。
- [x] 给 `mnote.block.replace` 补 stale blockRevisionRef 失败用例。
- [x] 给写工具补重复 idempotencyKey 的端到端用例。
- [x] 检查 `mnote.page.save` 在 manifest / UI 中继续标为页面级兜底,不显示为精确块编辑主入口。
- [ ] 检查任何新增 AI surface 是否只是基础 review/context/tooling 验收,不是新功能扩展。
- [ ] 更新 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。
- [ ] 更新 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。
2026-05-16 Page Block AI context / format focused smoke 证据:
- 新增并运行 `scripts/task-page-block-ai-context-format-smoke.js`,通过真实 3000 + 测试账号创建临时页,用 `mnote.page.save` 初始化 `p_1/p_2/p_3`,再验证 Hermes mnote tools 的 context / format / annotations / selection guard。
- 证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`,截图:`tmp/page-block-ai-context-format-smoke/mp8ddr4n-page.png`
- 验证结果:
- `GET /api/hermes/tools/mnote/manifest``mnote.doc.fetch``mnote.block.fetch``mnote.block.replace``mnote.page.save` 均包含 `readonly/destructive/idempotent/requiresApproval/approvalMode/runtimeOwner/writeOwner/selectionEffect``mnote.page.save` 明确 `destructive=true``approvalMode=yolo`,继续定位为页面级粗粒度兜底。
- `mnote.doc.fetch scope=selection selectedBlockIds=["p_2"] format=page_xml` 返回 `schema=mnote.page_ai_context.v1``allowedTargetBlockIds=["p_2"]``revision/conflictDetectionKey``revisionRef``content` 只包含 `p_2``第二段 mp8ddr4n`,不包含未选中的 `p_1/p_3`
- `mnote.doc.fetch scope=selection format=text` 返回 `[p_2] 第二段 mp8ddr4n`,不包含未选中块。
- `mnote.block.fetch blockId=p_2 format=page_xml/text` 返回目标块、`revisionRef` 和同父级 before/after 上下文 `p_1/p_3`
- `mnote.doc.apply_block_ops dryRun=true allowedTargetBlockIds=["p_2"]` 尝试 replace `p_1` 被拒绝,HTTP `400`,错误码 `mnote_block_target_out_of_scope`
- 边界:本轮只验证 `mnote.doc.apply_block_ops` 的 selection scope guard;单个 `mnote.block.replace/insert_after/move_after` 尚未校验 `allowedTargetBlockIds`,不能据此勾选完整 selection 写保护矩阵。
- 配套验证已通过:`node --check scripts/task-page-block-ai-context-format-smoke.js``cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture``MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-context-format-smoke.js`
2026-05-16 Page Block AI stale / idempotency focused smoke 证据:
- 新增并运行 `scripts/task-page-block-ai-conflict-idempotency-smoke.js`,通过真实 3000 + 测试账号创建临时页,用 `mnote.page.save` 初始化 `p_1/p_2`,再围绕 `mnote.block.replace` 验证冲突和幂等安全边界。
- 证据:`tmp/page-block-ai-conflict-idempotency-smoke/mp8dyqiq.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mp8dyqiq-page.png`
- 验证结果:
- 首次 `mnote.block.replace` 携带最新 `revision/conflictDetectionKey/blockRevisionRef/idempotencyKey` 成功写入 `p_2`,返回 `commandName=page.body.save``commandId=page_body_save_req_1778938353824_11`
- 使用同一 `idempotencyKey=idem_conflict_replace_mp8dyqiq` 再次调用 `mnote.block.replace`,即使请求 content 改成不同文本,也 replay 同一 `commandId`Page Aggregate 回读 `p_2` 仍为首次写入文本,`revisionAfterFirst=2``revisionAfterReplay=2`,确认不重复写入。
- 使用旧 `revision/conflictDetectionKey` 调用 `mnote.block.replace` 返回 HTTP `400`、错误码 `mnote_tool_conflict`,正文保持首次写入结果。
- 使用最新 `revision/conflictDetectionKey` 但旧 `blockRevisionRef` 调用 `mnote.block.replace` 返回 HTTP `400`、错误码 `mnote_tool_conflict`,正文保持首次写入结果。
- 边界:本轮只覆盖 `mnote.block.replace` 直接 tool executor 的 stale revision / stale blockRevisionRef / idempotency replay;不代表 `mnote.block.insert_after` 幂等矩阵、所有写工具幂等矩阵、review session accept stale 或 accept/reject/retry/abort 已完成。
- 配套验证已通过:`node --check scripts/task-page-block-ai-conflict-idempotency-smoke.js``cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture``MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-conflict-idempotency-smoke.js`
验证命令建议:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime editor_document
node scripts/task-page-block-ai-tools-smoke.js
```
### 3.3 Tree Command Cutover
- [x] `rg -n "documents\\.create|documents\\.title\\.update|documents\\.move|documents\\.archive|documents\\.restore" rust wolai-frontend` 盘点旧命令引用。
- [x] 将仍在 runtime 主链的旧命令按 owner 分类:必须迁移、兼容保留、测试 fixture。
- [x] 确认页面新建默认输出 `tree.node.create` 或正式 tree command。
- [x] 确认页面重命名默认输出 `tree.node.rename` 或正式 page/tree command。
- [x] 确认移动页面默认输出 `tree.subtree.move`
- [x] 确认归档 / 恢复 / 永久删除的 command family 与 resource lifecycle 设计一致。
- [x] 对 compat alias 返回增加 owner / deprecated 标识,避免被当作主链。
- [x] 更新 `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md` 或新增后续 process checklist。
2026-05-16 Tree Command Cutover 静态盘点与 alias 标识证据:
- 只读盘点命令:`rg -n "documents\\.(create|title\\.update|move|archive|restore|delete|purge|copy_tree)" rust wolai-frontend scripts --glob '!node_modules/**'`
- 前端主链:`wolai-frontend/src/lib/documents/tree-command-client.ts` 的新建、重命名、移动、归档、恢复、永久删除和复制均 POST `/api/tree/commands`,返回 meta 使用 `TREE_COMMAND_PROTOCOL.*.preferredCommandName``TREE_COMMAND_PROTOCOL` 中的 `documents.*` 只保留为 `compatCommandName`
- Next tree command route`wolai-frontend/src/app/api/tree/commands/route.ts` 在 create / move / rename / archive / restore 分支分别构造 `tree.node.create``tree.subtree.move``tree.node.rename``tree.node.archive``tree.node.restore`
- Rust Web tree route`rust/crates/mnote-web/src/routes/tree.rs``create_command_wire` 输出 `tree.node.create``tree.node.rename``tree.subtree.move``tree.node.archive``tree.node.restore``tree.node.purge``tree.subtree.copy``tree_command` route 只接收 action,不接收旧 `documents.*` command name 作为主链输入。
- 兼容保留:`bridge-runtime` 仍接受 `documents.create/title.update/move/delete/restore/purge/copy_tree`,但本轮已在对应 execution plan 的 `args_json.commandProtocol` 增加 `family=tree``owner=rust-runtime-kernel``preferredCommandName``compatCommandName``deprecatedAlias`;旧 `documents.*` alias 会标记 `deprecatedAlias=true`
- Transport 边界:`rust/crates/mnote-web/src/transport/convex.rs` 发送给 Convex legacy mutation 前会剥离 `commandProtocol``streamDeltaHint``domainEventHint``domainEventPlan(s)`,避免 legacy validator 把审计字段当写入参数。
- 仍保留为后续兼容收口点:`wolai-frontend/src/app/api/documents/create-child/route.ts` / `page-command-adapter.ts` 仍构造 `documents.create`,当前分类为 compat-only`mnote-cli` 仍有历史 `documents.*` CLI 构造,不属于 3000 主交互链。
2026-05-16 Tree Command Cutover 验证命令:
```bash
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime tree_ -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_strips_tree_archive_artifacts_for_legacy_mutation -- --nocapture
```
验证命令建议:
```bash
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime tree_command
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command
node scripts/task122-rust-web-create-page-ui-smoke.js
```
### 3.4 Tree Realtime Live Cache
- [x] 盘点 Sidebar、Page Tree、File Tree、page subtree 仍依赖 query/refetch/freshness 补偿的位置。
- [x] 确认 `/api/tree/events` snapshot / delta / resync payload 覆盖 page/file/resource row。
- [x] 新建页面后,双浏览器 A/B 检查另一端无需刷新出现页面。
- [x] 重命名页面后,双浏览器 A/B 检查 Sidebar / Breadcrumb / File Tree 一致更新。
- [x] 移动页面后,双浏览器 A/B 检查 tree order 不回闪。
- [x] 删除 / 恢复后检查 trash 与主树事件一致。
- [x] 断开 SSE 后恢复,检查 resync 能把 UI 拉回正确状态。
- [x] 更新 `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md` 的已验证项。
2026-05-16 Tree Realtime Live Cache 静态盘点与 payload 覆盖证据:
- 只读审查确认 React `AppLayoutShell` 同时接入 `useSidebarData``useSidebarTreeStream``usePreferredSidebarSnapshot` 负责 freshness 仲裁;Page Tree / File Tree 通过 preferred snapshot 消费 `kernelSidebarTree``kernelFileTreeProjection`
- 仍未统一的补偿链:`useSidebarData` 还保留 Convex query、HTTP `/api/sidebar` fallback 与手动 `refetch`mutation 后仍有 `refreshTree` / `sidebarQuery.refetch`Rust SSR shell 与 React hook 目前各自可建立 EventSourcepage subtree 仍从 Page Aggregate client state 派生,不应误标为已统一 live cache。
- Rust stream 盘点确认 workspace snapshot 已同时加载 `KernelProjectionKind::SidebarTree``KernelProjectionKind::FileTree`SSE payload 的 `data.dataset.kernel_sidebar_projection` / `data.dataset.kernel_file_tree_projection` 覆盖 page/file rowsubtree snapshot 仍只覆盖 `page_tree`,不含 file tree projection。
- 本轮新增 `stream_change_preserves_remove_asset_delta_fields`,确认 `tree.resource.delete``remove_asset` delta 字段保真;`structural_delta_requires_projection_snapshot` 已覆盖 `remove_asset` 需要 projection snapshot,避免资源删除类事件只靠局部 patch。
- 本轮强化 `scripts/task123-rust-web-tree-live-stream-consumer-smoke.js`:解析 SSE `snapshot` data,断言 `kind=snapshot``stream=workspace``projection=sidebar_tree``x-mnote-tree-stream-owner=rust-web`,并确认 workspace snapshot 中有 `kernel_sidebar_projection``kernel_file_tree_projection` 和临时页 `doc:<documentId>` file tree row。
2026-05-16 Tree Realtime Live Cache 验证命令:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web stream_change_preserves_remove_asset_delta_fields -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web structural_delta_requires_projection_snapshot -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::stream_support -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_events -- --nocapture
cd wolai-frontend && pnpm test src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/use-sidebar-tree-stream.test.tsx
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js
```
3000 smoke 证据:
- `tmp/tree-live-cache-smoke/20260516-task123/task123.stdout.json`,结果 `owner=rust-web``stream=/api/tree/events``snapshotProjection=sidebar_tree``fileTreeRows=59`
2026-05-16 双浏览器 no-refresh 验证与 purge 修复证据:
- 复用 `scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js` 做真实 3000 双浏览器验证:A 端通过真实 auth + `/api/tree/commands` 创建页面、归档、恢复、彻底删除与清空垃圾箱;B 端同时打开 File Tree 与 Trash,断言目标行无刷新出现 / 消失,且 `navigationEvents` 在初始打开后为空。
- 修复前该 smoke 在 `purge-visible-on-b` 前失败:`documents:purge` 收到 artifact-only 字段 `commandProtocol`Convex legacy validator 返回 `ArgumentValidationError: Object contains extra field commandProtocol`
- 修复点:`rust/crates/mnote-web/src/transport/convex.rs``tree.node.purge` / `documents.purge` 加入 `strip_tree_artifact_fields` 剥离范围,避免 compat mutation 接收 tree command audit 字段。
- 缺陷记录:`bugs/04-tree-domain/done/4-41-tree-node-purge-command-protocol-leaks-to-convex-v1.md`
- 修复后 `task432` 通过,证据 `tmp/tree-live-cache-smoke/20260516-task432/result.json``ok=true``create-visible-on-b` 通过 `tree:resync` 出现在 B 端 File Tree`archive-visible-on-b` 通过 `tree:delta remove_document` 同步 File Tree 与 Trash`restore-visible-on-b` 通过 `tree:delta upsert_document` 同步恢复;`purge-visible-on-b``empty-trash-visible-on-b` 通过 `tree:resync` 拉回正确状态。
2026-05-16 purge 修复验证命令:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_strips_tree_purge_artifacts_for_legacy_mutation -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_strips_tree_archive_artifacts_for_legacy_mutation -- --nocapture
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js
```
2026-05-16 双浏览器 rename live cache 修复与验证记录:
- 新增 regression smoke`scripts/task446-tree-rename-dual-browser-live-smoke.js`。脚本使用 A/B 独立 browser contextA 端通过正式 `/api/tree/commands` 创建并执行 `{ action: "rename", workspaceId, documentId, title }`,B 端分别保持目标文档页与 File Tree 页面不刷新,记录 `tree:snapshot/tree:delta/tree:resync``/api/tree/events` 请求、DOM 状态、导航事件与截图路径。
- RED 证据:修复前真实 3000 smoke 失败,B 端已收到 `tree:delta``op=upsert_document``liveApplied=delta``liveError=""`Sidebar / Page Tree 与 File Tree `{renamedTitle}.md` 已更新,但当前打开目标文档页的 `titleInputValue`、Breadcrumb 与 `document.title` 仍停留旧标题,导致 `waitForBDocumentRename` 超时。
- 根因:Rust SSR 文档 shell 的 `updateTitleEverywhere` 已更新 Page Tree / File Tree 行标题,但 title-only `upsert_document` 未同步当前文档页 chrome(页头标题输入框、Breadcrumb、`document.title`)。
- 修复点:`rust/crates/mnote-web/src/ssr/pages/layout.rs``updateTitleEverywhere(documentId, title)` 现在同步当前文档 chrome,并保持 scoped tree row 更新;字符串合同单测新增当前页 title input 与 Breadcrumb selector 覆盖。
- 构建阻断补齐:当前工作区已有 `AppState.editor_actor` / `AppConfig.enable_editor_actor` 脏改动但缺少真实 `editor_actor.rs` 文件,导致 mnote-web 无法编译启动;本轮把 misplaced 的 `EditorRuntimeActor` 实现补到 `rust/crates/mnote-web/src/editor_actor.rs`,并补齐测试配置中的 `enable_editor_actor` 字段,以恢复 3000 验证入口。
- GREEN 证据:重新启动最新 `desktop:hot` 后运行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task446-tree-rename-dual-browser-live-smoke.js` 通过。证据 `tmp/tree-live-cache-smoke/20260516-task446-rename/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task446-rename/b-document-after-rename.png``tmp/tree-live-cache-smoke/20260516-task446-rename/b-filetree-after-rename.png`
- 验证结果:B 文档页 `documentTitle/titleInputValue/breadcrumbTitle/sidebarTitle` 均为新裸标题;B File Tree `fileTreeTitle={renamedTitle}.md`;两端 `liveApplied=delta``liveError=""`rename 后 `navigationEvents=[]`,确认 B 端无刷新 / 无导航。
- 缺陷记录:`bugs/05-editor-mainline/done/5-13-tree-rename-live-document-chrome-stale-v1.md`
2026-05-16 双浏览器 move order live cache 修复与验证记录:
- 新增 regression smoke`scripts/task447-tree-move-order-dual-browser-live-smoke.js`。脚本使用 A/B 独立 browser contextA 端创建同父级 `A/B/C` 后执行正式 `/api/tree/commands``{ action: "move", documentId: C, parentId: root, sortOrder: 1 }`,B 端分别保持目标文档页与 File Tree 页面不刷新,记录 direct child order、tree events、导航事件与截图路径。
- RED 证据:合法 `sortOrder=1` 下修复前真实 3000 smoke 失败。B 端收到 `tree:delta move_document`payload 含 `sortOrder=1``liveApplied=delta``liveError=""`,但 Page Tree / File Tree DOM 顺序仍为 `A/B/C`,导致 `waitForExpectedOrder` 超时。
- 根因:Rust SSR 文档 shell 的 `applyMoveDocumentDelta(data)` 未读取 `sortOrder``moveDocumentRowForMode(mode, documentId, parentId)` 固定 `appendChild` 到目标父节点末尾;`tree:local-command` optimistic move 分支也未传 `body.sortOrder`
- 修复点:`rust/crates/mnote-web/src/ssr/pages/layout.rs` 增加 `sortOrderFromDelta``insertTreeNodeAtSortOrder`,让 live delta 与 local command move 按目标父节点直系 sibling index 插入;`rust/crates/mnote-web/src/routes/tree.rs` 补充 route/artifact 单测断言 `sortOrder` 保留在 result 与 `streamDelta`
- GREEN 证据:重启最新 `desktop:hot` 后运行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task447-tree-move-order-dual-browser-live-smoke.js` 通过。证据 `tmp/tree-live-cache-smoke/20260516-task447-move-order/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task447-move-order/b-document-after-move.png``tmp/tree-live-cache-smoke/20260516-task447-move-order/b-filetree-after-move.png`
- 验证结果:B 文档页与 B File Tree 页面中的 Page Tree / File Tree direct child order 均从 `A/B/C` 变为 `A/C/B`;两端 `liveApplied=delta``liveError=""`move 后 `navigationEvents=[]`,确认无刷新 / 无导航。
- 缺陷记录:`bugs/04-tree-domain/done/4-42-tree-move-live-delta-sort-order-ignored-v1.md`
2026-05-16 SSE resync / reconnect recovery 验证记录:
- 新增 `scripts/task448-tree-resync-recovery-dual-browser-smoke.js`,通过 B 端 EventSource URL 注入 `pollMs=5000`,A 端在同一 poll 间隔内连续创建两个子页,强制 `/api/tree/events` 进入非单条 delta 的 `event: resync` 分支。
- `task448` 验证结果:B 文档页与 B File Tree 页面均收到 `tree:resync``liveApplied=resync``liveError=""`Page Tree / File Tree direct child order 从仅有初始子页恢复为包含新增 `b/c` 两个子页,`navigationEvents=[]`。证据 `tmp/tree-live-cache-smoke/20260516-task448-resync/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task448-resync/b-document-after-resync.png``tmp/tree-live-cache-smoke/20260516-task448-resync/b-filetree-after-resync.png`
- 新增 `scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js`,通过 Playwright `context.setOffline(true)` 模拟 B 端 SSE 断线,A 端离线期间连续创建两个子页,B 端恢复在线后等待 EventSource 恢复事件把 UI 拉回最新。
- `task449` 验证结果:B 文档页与 B File Tree 页面恢复后 `liveStatus=connected``liveApplied=resync``liveError=""`,新增 `b/c` 子页出现在 Page Tree 与 File Tree`navigationEvents=[]`。证据 `tmp/tree-live-cache-smoke/20260516-task449-reconnect/result.json`,截图 `tmp/tree-live-cache-smoke/20260516-task449-reconnect/b-document-after-reconnect.png``tmp/tree-live-cache-smoke/20260516-task449-reconnect/b-filetree-after-reconnect.png`
- 说明:当前 Rust SSR controller 的真实恢复合同是“断线期间错过多条变化后,恢复时通过 snapshot/resync 类完整投影拉回 UI”;本轮实测恢复事件为 `tree:resync`,不是浏览器刷新或导航。
验证命令建议:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_events
node --check scripts/task447-tree-move-order-dual-browser-live-smoke.js
node --check scripts/task448-tree-resync-recovery-dual-browser-smoke.js
node --check scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js
node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js
node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js
node scripts/task446-tree-rename-dual-browser-live-smoke.js
node scripts/task447-tree-move-order-dual-browser-live-smoke.js
node scripts/task448-tree-resync-recovery-dual-browser-smoke.js
node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js
```
### 3.5 定向 Bug Hunt
- [ ] 建立 bugs 分类:Page Aggregate 问题归 `bugs/05-editor-mainline/process/`
- [ ] Tree command / realtime / File Tree 问题归 `bugs/04-tree-domain/process/`
- [ ] AI tools 问题归 `bugs/07-ai/process/`
- [ ] 每个 bug 必须包含复现步骤、期望、实际、证据截图或 JSON、owner 判断。
- [ ] 先补最小 failing smoke,再修实现。
- [ ] 修复后移动到对应 `done/`,并记录验证命令。
重点 bug 方向:
- [ ] Page Aggregate 与页头标题不一致。
- [ ] Page Aggregate 与 File Tree `{title}.md` 不一致。
- [ ] AI 写入成功但 Page Aggregate 回读旧内容。
- [ ] stale revision 未阻断写入。
- [ ] tree stream 断线恢复后 UI 停在旧快照。
- [ ] 旧 compat / debug route 在 3000 首屏被误用。
### 3.6 2026-05-16 暂停交接进展摘要
本轮按用户要求暂停继续实现,只记录当前进展与恢复点;未继续推进 `3.2 Page Block AI Tooling` 的实现或 smoke。
已完成到可继续接力的阶段:
- `3.1 Page Aggregate 单一真源` 已完成本 checklist 中的当前验证项,并补齐真实 3000 smoke、Rust route 测试、前端单测和失败降级证据。关键证据包括 `tmp/page-aggregate-single-truth-smoke/20260516-182244/``tmp/page-aggregate-body-sync-smoke/mp87mgz7.json``tmp/page-aggregate-options-sync-smoke/mp87y6j3.json``tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.json`
- `3.3 Tree Command Cutover` 已完成当前收口:3000 主链使用 `tree.*` preferred command`documents.*` 保留为 compat alias,并在 Rust execution plan / artifact 中标记 owner、preferredCommandName、compatCommandName、deprecatedAlias。
- `3.4 Tree Realtime Live Cache` 已完成本轮重点验证与修复:snapshot/delta/resync payload 覆盖、双浏览器新建 / 归档 / 恢复 / purge、rename 当前文档 chrome 同步、move sortOrder live 排序、SSE resync 与 reconnect recovery。
- 本轮新增或更新的 Tree Realtime 关键证据:
- `tmp/tree-live-cache-smoke/20260516-task432/result.json`
- `tmp/tree-live-cache-smoke/20260516-task446-rename/result.json`
- `tmp/tree-live-cache-smoke/20260516-task447-move-order/result.json`
- `tmp/tree-live-cache-smoke/20260516-task448-resync/result.json`
- `tmp/tree-live-cache-smoke/20260516-task449-reconnect/result.json`
- 本轮已归档的缺陷:
- `bugs/04-tree-domain/done/4-41-tree-node-purge-command-protocol-leaks-to-convex-v1.md`
- `bugs/04-tree-domain/done/4-42-tree-move-live-delta-sort-order-ignored-v1.md`
- `bugs/05-editor-mainline/done/5-12-page-options-refresh-runtime-attrs-v1.md`
- `bugs/05-editor-mainline/done/5-13-tree-rename-live-document-chrome-stale-v1.md`
当前暂停点:
- 已进入 `3.2 Page Block AI Tooling` 的只读审计阶段,但尚未勾选 3.2 的任何新 checkbox。
- 已确认现有代码中 `mnote.doc.fetch` 已支持 `scope=selection``format=page_xml/text/markdown/json``schema=mnote.page_ai_context.v1``allowedTargetBlockIds`、truncation/warnings/continuation 等基础字段;`mnote.block.fetch` 已支持 `format=page_xml/text`manifest 已包含 `readonly/destructive/idempotent/requiresApproval/approvalMode/runtimeOwner/writeOwner/selectionEffect` 等 annotations。
- 已确认 `scripts/task-page-block-ai-tools-smoke.js` 覆盖基础块工具闭环,但还没有专门覆盖 `scope=selection``page_xml/text`、manifest annotations、选区外写入阻断、review session / conflict / idempotency 的完整验收。
- 用户目标中提到的 `design/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md` 当前在 `design/07-ai/process/` 下不存在;实际可读历史参考位于 `design/old/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md`,当前执行口径应继续以 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 为准。
恢复时的下一步建议:
- 继续从 `3.2 Page Block AI Tooling` 开始,不要跳到定向 bug hunt。
- 优先新增一个聚焦 smoke,例如 `scripts/task-page-block-ai-context-format-smoke.js`,只验证基础底座,不扩新 AI 功能:
- manifest annotations 中 `mnote.doc.fetch` / `mnote.block.fetch` / `mnote.block.replace` / `mnote.page.save` 的 readonly、destructive、requiresApproval、selectionEffect、owner 字段。
- `mnote.doc.fetch scope=selection selectedBlockIds=[...] format=page_xml` 只返回选区块,返回 `schema=mnote.page_ai_context.v1``allowedTargetBlockIds``revisionRef`
- `mnote.doc.fetch scope=selection format=text` 不包含未选中块。
- `mnote.block.fetch format=page_xml/text` 返回稳定块内容和 `revisionRef`
- 选区冻结后写工具尝试修改 `allowedTargetBlockIds` 外块时返回 `mnote_block_target_out_of_scope`
- 配套验证建议从这些命令开始:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture
node --check scripts/task-page-block-ai-context-format-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-context-format-smoke.js
```
交接注意事项:
- 当前工作区有大量既有未提交改动和文件迁移,继续执行时必须只碰当前小项相关文件,不要回滚、删除或清理无关脏文件。
- `git diff --check` 全仓可能仍会被既有删除文件阻断;验证本轮改动时可先用限定路径或 `git diff --no-index --check /dev/null <new-file>`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs` 中已有 Tree Realtime rename / move order 相关改动,同时可能混有先前 editor actor 相关改动;继续改动前必须重新读 diff,避免覆盖用户或前序 agent 的更改。
- `3000` 相关 smoke 必须继续保留 JSON / 截图证据路径,并在本 checklist 与对应 `design/07-ai``bugs/*` 文档同步记录。
---
## 4. 当前不优先做
- [ ] 不优先增加新的编辑器 UI 大功能。
- [ ] 不优先新增 AI Agent 工作流功能。
- [ ] 不优先扩展 AI 功能面;AI 只做基础上下文、工具合同、审阅会话、冲突与回滚。
- [ ] 不优先大规模替换 Convex。
- [ ] 不优先重写 `leptos-tiptap` 输入层。
- [ ] 不把 `mnote.page.save` 包装成精确块编辑长期方案。
- [ ] 不在 compat route 继续扩写长期业务语义。
- [ ] 不把 BlockNote / Tiptap AI runtime 作为 mnote runtime 依赖。
---
## 5. 完成定义
本 checklist 不能迁入 `done/`,直到:
- [ ] Page Aggregate 的标题 / 正文 / 页面设置 / page tree / AI fetch 回读形成同一组可验证真相。
- [ ] `7-10` 的 Page Block AI Tooling 剩余矩阵完成或明确拆出后续 process 文档。
- [ ] `7-11` 的 AI 基础 runtime 口径完成:context、format、manifest annotations、review session、状态机边界都有代码或明确后续 checklist。
- [ ] `tree.*` command 面对主要页面生命周期动作成为唯一 preferred runtime 主链。
- [ ] tree realtime live cache 覆盖 Sidebar、Page Tree、File Tree、page subtree 的关键写后更新。
- [ ] 至少一轮定向 bug hunt 完成,所有 P0/P1 blocker 已归档到 `bugs/*/done/` 或明确保留为后续 process。
---
## 6. 给后续 /goal 的持续执行 Prompt
```text
/goal objective: 持续执行 /mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md,按 P0 -> P1 顺序推进 MNOTE 架构收口、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md 和 design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源;2) tree command cutover3) tree realtime live cache4) AI 基础工具与审阅底座,只补 context/selection/page_xml/tool annotations/review session/conflict/idempotency,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新本 checklist 和对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。
```
@@ -0,0 +1,255 @@
# 09 页面 AI 快速块编辑 Runtime Review
> 状态:`process`
>
> 日期:2026-05-16
>
> 关联主线:
> - `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
> - `design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
---
## 1. 本轮结论
页面 AI 块写入的慢点不在 Rust 块工具本身,也不在 Convex 持久化本身。
本轮真实浏览器 smoke 显示:
- 旧路径:页面 AI 先调用 `/api/page-ai/block-edit-workflow`,快路径模型阶段耗时约 `7695ms`,随后因模型输出的块定位未命中 projection,`doc_apply_block_ops` 返回 `mnote_block_not_found`,前端继续 fallback 到 `/api/hermes/client/runs`,最终由 Hermes agent 调 `mnote.doc.apply_block_ops` 写入,总可见耗时约 `13246ms`
- 新路径:同一类简单中文块操作先由 mnote 本地 planner 解析为 `replace/insert_after/delete` operations,再直接调用 Rust `doc_apply_block_ops`。浏览器 smoke 回读可见耗时 `788ms`;后端日志显示 `operation_source=local_rule``model_ms=0``apply_ms=42``total_ms=42`,且未进入 `/api/hermes/client/runs`
因此,当前要继续做的是 Hermes 工具路由、上下文冻结、dry-run/review 与 Rust 写入校验基建,而不是继续让 Hermes agent 反复执行同一个端到端动作,也不是在 mnote 内新建第二套 AI runtime。
---
## 2. 慢的直接原因
### 2.1 通用 agent 编排链路太长
旧页面 AI 编辑链路实际包含:
```text
浏览器输入
-> Hermes session/profile/tools 初始化
-> 模型理解页面任务
-> 模型选择工具
-> 工具调用
-> 回读或继续推理
-> 最终回复
```
这适合复杂任务,但不适合“把 A 替换为 B / 在 A 后插入 C / 删除 D”这类明确块操作。
### 2.2 模型直接产 operations 不够可靠
本轮快路径第一版已经绕开 Hermes agent,但仍让 DeepSeek 直接根据 `page_xml/page_text` 生成 operations。失败点是:
```text
model_ms ~= 7695
operations = 3
apply -> mnote_block_not_found
fallback -> Hermes agent
```
说明模型输出了看似正确的操作数,但 block 定位信息没有命中当前 Page Aggregate projection。这个问题不能靠增加 smoke 次数解决,必须让 mnote runtime 在模型前后都掌握定位与校验。
### 2.3 失败后 fallback 放大了耗时
第一版前端逻辑在快路径失败时继续进入 `/api/hermes/client/runs`。这导致一次用户请求可能经历:
```text
快路径模型失败成本 + Hermes agent 成功成本
```
本轮已改为:只有后端明确返回 `page_ai_workflow_not_block_edit` 才允许 fallback;其他快路径错误直接在本轮 run 中失败展示,避免重复写入和重复等待。
---
## 3. 已完成的修正
### 3.1 页面 AI 快速块编辑 route
已新增并接入:
- `POST /api/page-ai/block-edit-workflow`
- 文件:`rust/crates/mnote-web/src/routes/page_ai_workflow.rs`
该 route 负责:
- 接收当前页面冻结后的 `mnote.page_ai_context.v1`
- 为明确中文编辑语句先走本地 operation planner。
- 无法本地解析时才调用小模型生成 operations。
- 最终统一走 `mnote.doc.apply_block_ops`,由 Rust 生成 canonical content 并保存。
### 3.2 简单块操作本地 planner
已支持这类明确表达:
```text
把「A」替换为「B」;
在「C」后插入「D」;
删除「E」。
```
输出直接是:
```json
[
{"op":"replace","matchText":"A","content":"B"},
{"op":"insert_after","matchText":"C","content":"D"},
{"op":"delete","matchText":"E"}
]
```
该层的目的不是做通用自然语言理解,而是把高频、低歧义、可确定的块编辑从模型路径剥离出来。
### 3.3 快路径可观测性
后端日志已记录:
- workflow started/completed
- operation source`local_rule``model`
- operations 数量
- model 耗时
- apply 耗时
- total 耗时
smoke 也已在失败时写出 evidence JSON,并记录 `/api/page-ai/*``/api/hermes/client/*` 请求/响应。
---
## 4. 验证证据
命令:
```bash
cargo test -p mnote-web page_ai_workflow -- --nocapture
cargo test -p mnote-web hermes_tools -- --nocapture
MNOTE_PAGE_AI_FAST_TIMEOUT_MS=60000 node scripts/task-page-ai-block-edit-workflow-smoke.js
```
结果:
- `page_ai_workflow`2 passed。
- `hermes_tools`22 passed。
- 浏览器 smoke:通过。
最新浏览器 evidence
```text
/mnt/Data1T/mnote/tmp/page-ai-block-edit-workflow-smoke/mp86uciu.json
```
关键值:
```json
{
"timingsMs": {
"pageAiWriteVisible": 788
},
"usedFastWorkflow": true,
"usedHermesRun": false,
"finalTexts": [
"第一段 mp86uciu",
"插入段 mp86uciu",
"第二段已修改 mp86uciu"
]
}
```
后端关键日志:
```text
operation_source="local_rule"
model_ms=0
apply_ms=42
total_ms=42
```
---
## 5. 剩余问题
### 5.1 不能把本地 planner 当成完整 AI runtime
当前本地 planner 只覆盖低歧义中文引号表达。它证明了正确的 runtime 方向,但不是最终答案。
需要继续建设:
- PageAIContextBuilder:冻结 selection/page context,减少模型输入。
- PageAIIntentParser:先判定是明确块操作、结构化改写、摘要问答、还是复杂编辑。
- PageAIOperationPlanner:把明确操作转成 `mnote.doc.apply_block_ops`,复杂操作才调用小模型。
- PageAIOperationValidator:模型输出后必须用 projection 校验 blockId/matchText/allowedTargetBlockIds。
- PageAIApplyController:统一处理 yolo 写入、失败展示、回读验证、审计日志。
### 5.2 模型输出 operations 仍需修
当请求不能被本地 planner 解析时,仍会调用模型。该路径必须补:
- 输出 schema 更严格,禁止模型臆造 blockId。
- 优先 `matchText` 或由服务端根据 text resolve block,而不是信任模型 blockId。
- 模型输出后做 dry-run validate,不命中时不要 fallback Hermes agent 重新跑。
- 把 validation error 反馈给用户或进入后续 clarify/retry,而不是隐式整页写。
### 5.3 Hermes agent 不应承担短路径编辑
Hermes 仍适合:
- 多步骤页面理解。
- 跨页面检索。
- 工具不可直接表达的复杂任务。
- 外部 skill/plugin 编排。
但对当前页面小段落块增删改,mnote 自己的 runtime 应该在浏览器/Rust route 内完成 `intent -> operations -> apply -> readback`
---
## 6. 下一步建议
优先级应从“继续 smoke”切到“补 AI runtime 基建”:
1. `PageAIIntentParser`
- 输入:用户 prompt、scope、selection、page context。
- 输出:`direct_block_ops | model_block_ops | question | unsupported`
- 目标:不让每条简单编辑都进入 Hermes agent。
2. `PageAIOperationPlanner`
- 扩展当前本地 planner。
- 支持常见中文/英文明确表达。
- 支持选区内“改成/润色/拆成列表”等可控操作。
3. `PageAIOperationValidator`
- 所有 operations apply 前先 resolve projection。
- 对 blockId、matchText、allowedTargetBlockIds、editable、children、revisionRef 做统一校验。
- 失败返回结构化错误,不 fallback 通用 agent。
4. `PageAIApplyController`
- 统一 yolo 模式下的写入、回读、状态展示和失败提示。
- 后续再接 preview/review session,而不是现在把 review 作为默认阻塞。
5. 模型路径瘦身
- 对必须调用小模型的任务,发送 `page_xml/text + allowed operations schema`
- 模型只负责生成候选 operations;最终定位、校验和写入仍由 Rust runtime 负责。
---
## 7. 当前判断
小段落编辑低于 10s 已经被证明可达,且当前 smoke 为 `788ms`
下一阶段的关键不是再证明“能写”,而是把这条快路径产品化:
```text
用户意图
-> mnote intent/parser
-> operation planner
-> projection validator
-> Rust apply
-> readback
-> UI 状态/审计
```
Hermes agent 应从默认编辑执行器退回到复杂任务编排器。
+6
View File
@@ -40,6 +40,9 @@
- `process/` 放推进中的测试流程与对标执行稿
9. `09-siyuan-reference/`
- `process/` 放思源参考、借鉴边界与能力盘点稿
10. `10-review/`
- `done/` 放阶段性代码 / 架构审查证据和结论
- 只作为当前实现状态证据和历史审查归档,不作为新的主线优先级入口
## 迁移规则
@@ -55,3 +58,6 @@
- `old/`
- 已废弃或被替代的历史稿件,标题统一标记 `[recycle]`
- 每个大类继续按 `process/``done/` 分层
- `design/design/`
- Wolai / Stitch / icon 等取证素材与静态参考资产;这是当前 `design/` 目录下的素材子目录,不是主线设计稿根目录
- 不参与 `[done]/[process]/[recycle]` 主线状态判断
@@ -0,0 +1,440 @@
# 7-11 [process][recycle] BlockNote / Tiptap AI 参考与 mnote 自有 AI 工具 runtime v1
> 更新时间:2026-05-16
>
> 当前状态:`RECYCLE`
>
> 回收说明(2026-05-16):
> - 本稿的参考取证仍有价值,但“mnote 自有 AI 工具 runtime”口径容易误导后续实现继续扩出第二套页面 AI runtime。
> - 当前长期口径已被 `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 覆盖。
> - 后续方向固定为:Hermes 继续作为唯一页面 AI agent runtimemnote 只建设 Hermes 可消费的工具路由、工具 manifest、上下文冻结、dry-run/review、Rust 写入校验与 readback。
> - 本稿仅作为 BlockNote / Tiptap 参考材料和历史判断保留,不再作为当前执行口径。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocknote-ai/MNOTE-REFERENCE.md`
---
## 1. 结论
BlockNote AI 和 Tiptap AI Toolkit 都值得参考,但参考层级不同:
- Tiptap AI Toolkit 适合作为“AI tool contract”参考:`tiptapRead``tiptapEdit``tiptapReadSelection``toolDefinitions()``executeTool/streamTool`、review options。
- BlockNote AI 适合作为“AI runtime shape”参考:`AIExtension``DocumentStateBuilder``StreamToolsProvider``AIRequest``aiMenuState``acceptChanges/rejectChanges/retry/abort`
- mnote 不能直接采用 BlockNote / Tiptap 的 editor truth。mnote 的事实源仍是 Rust kernel、Page Aggregate、EditorCommand、Convex revision/conflict key 与 Hermes audit。
下一步不应把 BlockNote 重新拉回运行时主链,也不应把 Tiptap Pro / BlockNote XL AI 当成硬依赖。正确方向是用它们的架构形态,打造 mnote 自己的 Rust-owned AI tool runtime。
---
## 2. 已拉取参考代码
已将 BlockNote 官方仓库以 sparse clone 方式拉到:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocknote-ai/`
当前提交:
`c255558b2d4f2be6453c67df81bb702a1a586909`
保留范围:
- `docs/content/docs/features/ai/`
- `examples/09-ai/`
- `packages/xl-ai/src/`
- `packages/xl-ai-server/src/`
- 对应 `package.json` / `LICENSE`
许可证边界:
- `@blocknote/xl-ai` 当前标注为 `GPL-3.0 OR PROPRIETARY`
- 本目录只能作为 `reference-code` 研究材料。
- 不允许未审查许可证就复制实现进入 mnote runtime。
---
## 3. BlockNote AI 可借鉴能力
### 3.1 AI lifecycle 状态机
参考:
- `reference-code/blocknote-ai/packages/xl-ai/src/AIExtension.ts`
BlockNote 的 AI menu state 包含:
```text
closed
user-input
thinking
ai-writing
user-reviewing
error
```
并提供:
- `openAIMenuAtBlock(blockId)`
- `closeAIMenu()`
- `invokeAI(opts)`
- `acceptChanges()`
- `rejectChanges()`
- `retry()`
- `abort(reason)`
- `setAIResponseStatus(...)`
mnote 应吸收为:
- `PageAIController`
- `PageAIRunState`
- `PageAIReviewSession`
但状态源必须绑定 mnote 的 `documentId/workspaceId/blockId/revision/conflictDetectionKey`,不能绑定 BlockNote editor instance。
### 3.2 DocumentStateBuilder
参考:
- `reference-code/blocknote-ai/packages/xl-ai/src/api/formats/DocumentStateBuilder.ts`
- `reference-code/blocknote-ai/packages/xl-ai/src/api/aiRequest/builder.ts`
BlockNote 把 AI 上下文拆成两种:
- 无选区:全量 document blocks + cursor position。
- 有选区:selected blocks + whole document context;模型只能对 selection 发操作。
mnote 应吸收为 `PageAIContextBuilder`
```json
{
"schema": "mnote.page_ai_context.v1",
"workspaceId": "tree_x",
"documentId": "tree_y",
"runId": "run_1",
"scope": "selection",
"revision": 12,
"conflictDetectionKey": "body:...",
"selectedBlocks": [],
"contextBlocks": [],
"allowedTargetBlockIds": []
}
```
约束:
- context 必须从 Page Aggregate block projection 生成。
- selection 必须在 AI run 开始时冻结,不能随用户后续选区漂移。
- `allowedTargetBlockIds` 必须作为写工具校验输入的一部分。
### 3.3 StreamToolsProvider
参考:
- `reference-code/blocknote-ai/packages/xl-ai/src/api/formats/formats.ts`
- `reference-code/blocknote-ai/packages/xl-ai/src/api/formats/base-tools/`
- `reference-code/blocknote-ai/packages/xl-ai/src/streamTool/`
BlockNote 的 stream tool 把 add/update/delete block 建模为 schema + validate + executor。
mnote 已有对应底座:
- `mnote.doc.fetch`
- `mnote.doc.find`
- `mnote.block.fetch`
- `mnote.doc.plan_update`
- `mnote.block.replace`
- `mnote.block.insert_after`
- `mnote.block.delete`
- `mnote.block.move_after`
- `mnote.doc.apply_block_ops`
mnote 下一步要补的不是工具名,而是工具 manifest 的能力注解:
```json
{
"name": "mnote.block.replace",
"annotations": {
"readonly": false,
"destructive": false,
"idempotent": false,
"requiresApproval": true,
"selectionEffect": "destroy",
"runtimeOwner": "mnote-web",
"writeOwner": "rust-runtime-kernel"
}
}
```
### 3.4 Review / accept / reject
参考:
- `reference-code/blocknote-ai/packages/xl-ai/src/AIExtension.ts`
- `reference-code/blocknote-ai/packages/xl-ai/src/prosemirror/rebaseTool.ts`
- `reference-code/tiptap-docs/src/content/content-ai/capabilities/ai-toolkit/api-reference/review-options.mdx`
BlockNote 在 editor 内通过 suggestions / fork ydoc 完成 reviewTiptap AI Toolkit 则提供 `review/preview/trackedChanges` 模式。
mnote 不能照搬 ProseMirror suggestion 作为事实源。mnote 应建立自己的 review session
```json
{
"schema": "mnote.page_ai_review_session.v1",
"reviewSessionId": "review_1",
"runId": "run_1",
"documentId": "tree_y",
"workspaceId": "tree_x",
"status": "previewing",
"baseRevision": 12,
"baseConflictDetectionKey": "body:...",
"operations": [],
"diff": [],
"warnings": [],
"risk": "low"
}
```
状态:
```text
draft
planning
previewing
awaiting_user
accepted
rejected
applying
applied
failed
aborted
stale
```
写入规则:
- AI 写工具默认先生成 review session。
- 用户 accept 后才执行 `mnote.doc.apply_block_ops` 或单个 `mnote.block.*`
- accept 时重新校验 `revision/conflictDetectionKey/revisionRef`
- reject 只关闭 session,不改变正文。
---
## 4. Tiptap AI Toolkit 可借鉴能力
Tiptap AI Toolkit 已在 `7-9` 中完成第一轮映射,本文补充它与 BlockNote 的组合边界。
| Tiptap / BlockNote 能力 | mnote 对应 | 吸收内容 | 禁止吸收 |
| --- | --- | --- | --- |
| `tiptapRead` | `mnote.doc.fetch` / `mnote.block.fetch` | 先读、带范围、返回 AI 友好表示 | 不把 Tiptap JSON 当长期工具格式 |
| `tiptapEdit` | `mnote.doc.plan_update` + `mnote.block.*` | 操作列表、dry-run、diff、reviewable edit | 不让浏览器 editor command 成为事实源 |
| `tiptapReadSelection` | `mnote.doc.fetch scope=selection` | selection-aware workflow | 不持久化 ProseMirror selection range |
| `toolDefinitions()` | Rust Hermes manifest | schema、description、capability、annotations | 不依赖私有 npm 包 |
| `AIExtension` | `PageAIController` | AI lifecycle、menu state、abort/retry | 不引入 BlockNote runtime |
| `DocumentStateBuilder` | `PageAIContextBuilder` | selection/context 分离 | 不从 DOM 拼上下文 |
| `StreamToolsProvider` | `MnoteAIToolProvider` | 工具集合按能力开放 | 不在前端直接执行正式持久化 |
| `acceptChanges/rejectChanges` | `PageAIReviewSession` | preview -> accept/reject | 不以 ProseMirror suggestion 作为最终事实 |
---
## 5. mnote 自有 AI runtime 设计
### 5.1 分层
```text
Page Aggregate / Rust kernel
-> PageAIContextBuilder
-> MnoteAIToolProvider
-> Hermes / model runtime
-> PageAIReviewSession
-> EditorCommand / page.body.save
-> Page Aggregate 回读验证
```
### 5.2 组件职责
`PageAIContextBuilder`
- 输入:`workspaceId/documentId/scope/selection/blockId/query/maxBlocks`
- 输出:`mnote.page_ai_context.v1`
- 数据源:Page Aggregate block projection。
- 负责 selection 冻结、上下文裁剪、`allowedTargetBlockIds`
`MnoteAIToolProvider`
- 输入:profile、document capability、selection scope、feature flags。
- 输出:可用工具 manifest。
- 负责 `readonly/destructive/requiresApproval/selectionEffect` 等注解。
`PageAIController`
- 管理当前页面 AI run。
- 对齐状态:`user-input/thinking/ai-writing/user-reviewing/error/aborted`
- 不直接写正文,只组织 context、tool calls、review session。
`PageAIReviewSession`
- 保存 plan/diff/warnings/risk/operations。
- 提供 `accept/reject/retry/abort`
- accept 时走 Rust 写工具,并二次校验 revision。
`PageAIReviewSurface`
- 页面内展示 AI 结果、工具卡、diff、风险、按钮。
- 不把 tool result 当正文。
- 不绕过 review session 调写工具。
---
## 6. 格式策略
当前 mnote 不能把 HTML 作为长期 AI contract。推荐格式分三层:
`json`
- 面向工具执行。
- 保留 block id、type、attrs、children、revisionRef。
- 作为写工具输入和回读校验主格式。
`text`
- 面向摘要、问答、轻量改写。
- 不可直接作为精确写入定位依据。
`page_xml`
- 面向模型理解结构。
- 形态示例:
```xml
<page id="tree_y" revision="12">
<block id="p_1" type="paragraph" revisionRef="body:12:p_1">第一段</block>
<block id="h_1" type="heading" level="2" revisionRef="body:12:h_1">标题</block>
</page>
```
约束:
- `page_xml` 只能是 Page Aggregate projection 的序列化视图。
- 写入仍必须转成 `mnote.block.*``mnote.doc.apply_block_ops`
---
## 7. 执行 checklist
### Phase A:参考口径冻结
- [x] 拉取 BlockNote AI sparse reference 到 `reference-code/blocknote-ai/`
- [x] 标注来源 commit 与许可证边界。
- [x] 明确 BlockNote 只作为 AI runtime shape 参考,不作为 mnote runtime dependency。
- [x] 明确 Tiptap AI Toolkit 只作为 tool contract 参考,不作为事实源。
### Phase BPageAIContextBuilder
- [x] 定义 `mnote.page_ai_context.v1` schema。
- [x] `doc.fetch` 支持 `scope=selection`
- [x] selection run 开始时冻结 `selectedBlockIds` / `allowedTargetBlockIds`
- [x] `doc.fetch` 支持 `format=text/page_xml/json`
- [x] `block.fetch` 支持 `format=text/page_xml/json`
- [ ] 大页面 context 默认裁剪,返回 `truncated/warnings/continuation`
验收:
- [ ] AI run 期间用户改变选区,不影响本 run 的 selection context。
- [ ] 写工具不能修改 `allowedTargetBlockIds` 外的块。
- [ ] `page_xml` 与 JSON projection 的 block id / revisionRef 一致。
### Phase CMnoteAIToolProvider
- [x] tool manifest 增加 `annotations`
- [x] 当前 yolo 模式下写工具标注 `requiresApproval=false` / `approvalMode=yolo`
- [x] selection scope 下,写工具自动带 `allowedTargetBlockIds` 约束。
- [ ] profile tool toggle 与 capability annotation 同时生效。
- [ ] manifest 输出能直接转换给 Hermes/model runtime。
验收:
- [ ] 禁用工具不出现在当前 AI run 的可用工具集中。
- [ ] 复杂块工具返回 `unsupportedReason`,不伪装成可编辑。
- [ ] manifest 能说明工具是否 destructive / readonly / requiresApproval。
### Phase DPageAIReviewSession
- [ ] 定义 `mnote.page_ai_review_session.v1`
- [ ] `mnote.doc.plan_update` 可创建 review session。
- [ ] `mnote.doc.apply_block_ops dryRun=true` 可返回 review session draft。
- [ ] 页面 AI UI 展示 diff、warnings、risk、blocked。
- [ ] `accept` 执行写入前二次校验 revision/conflictDetectionKey/revisionRef。
- [ ] `reject` 不改变正文。
- [ ] `abort` 停止 run,关闭未提交 review session。
- [ ] `retry` 使用最新 Page Aggregate 重新构建 context。
验收:
- [ ] accept 后 Page Aggregate 与 `mnote.doc.fetch` 都能读回变化。
- [ ] stale revision 时 accept 被阻断,session 进入 `stale`
- [ ] reject 后页面正文、revision、block ids 不变化。
### Phase E:页面 AI 状态机
- [ ] 实现 `PageAIController` 状态枚举。
- [ ] AI menu / side panel 共享同一 run state。
- [ ] tool event 按 `toolCallId` 聚合展示。
- [ ] 错误态支持 retry / close。
- [ ] abort 态不会留下半写入正文。
验收:
- [ ] `thinking -> ai-writing -> user-reviewing -> accepted/rejected` 链路可见。
- [ ] 网络失败、tool parse 失败、revision stale 三类错误可区分。
- [ ] 页面刷新后未提交 review session 不会自动写入正文。
---
## 8. 当前优先级
优先级顺序:
1. `PageAIContextBuilder + scope=selection`
2. `format=page_xml/text`
3. `PageAIReviewSession`(后续可选 review 模式;当前默认 yolo 不阻塞写入)
4. `PageAIController` 状态机与 UI surface
5. 复杂块移动 / 删除 / 多块操作阻断矩阵
原因:
- selection 和 context 是 AI 质量与安全边界的前置条件。
- `page_xml/text` 能降低模型误读 projection 的概率。
- review session 是生产写入前必须补齐的用户确认层。
- 状态机和 UI surface 应建立在稳定 session 语义之上。
---
## 9. 禁止项
- 不把 BlockNote 重新设为文档页默认主编辑器。
- 不把 `@blocknote/xl-ai` 作为 mnote runtime dependency。
- 不复制 GPL/PROPRIETARY 源码进入 mnote 运行时代码。
- 不让前端 editor instance 直接执行长期持久化写入。
- 不以 HTML / Tiptap JSON / ProseMirror position 作为 mnote 长期 AI tool contract。
- 不绕过 `dryRun/idempotencyKey/revision/conflictDetectionKey/revisionRef` 执行 AI 写工具。
---
## 10. 与 `7-10` 的关系
`7-10` 继续作为页面块 AI 工具执行 checklist。
本文补充的是 `7-10` 后半段缺口的架构口径:
- `scope=selection`
- `format=page_xml/text`
- `持久审阅/preview UI`
- `AI lifecycle 状态机`
- `accept/reject/retry/abort`
因此本文不替代 `7-10`,而是作为下一阶段 AI runtime 收口设计稿。