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
@@ -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 节更新引用
@@ -1,971 +0,0 @@
# 7-9 [process] 页面/块 AI 工具体系规划 v1
> 更新时间:2026-05-16
>
> 当前状态:`PROCESS`。
>
> 本稿承接 `7-6` 的 mnote Hermes plugin tool 合同、`7-8` 的 Hermes Runtime BFF 方向,以及近期页面 AI 工具实测中暴露的问题:当前 `mnote.page.get/save/update_title/update_options` 已能完成页面级读写,但工具粒度仍偏粗,不能长期代表“AI 能精确编辑页面/块”。
>
> 核心参考:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-vscode-main`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/ai-toolkit-demos`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-main`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-apcore`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-ai-autocomplete`
>
> 上位依据:
> - `/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/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/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`
---
## 1. 结论
不要把问题理解成二选一:
- 不是“等 Rust kernel / Page Aggregate / editor module 完全稳定后,才开始设计工具”。
- 也不是“现在立刻扩一批块级写工具,让 Hermes 直接调用当前 Convex / 前端私有 shape”。
正确路径是:
> **现在继续写工具体系设计,冻结 AI 面向的稳定工具合同;实现上分阶段推进,先做只读、定位、dry-run、plan 和最小单块写入。复杂块移动、块嵌入、多块批量改写、媒体块编排,要等 Rust block model、Page Aggregate command、Convex 持久化和 editor session 刷新链路达到硬门槛后再开放。**
当前 `mnote.page.save` 可以继续作为页面级兜底工具,但不能继续被描述为长期块编辑方案。长期工具体系必须基于稳定的 `Page Aggregate / EditorBlockDocument / Rust runtime command` 中间合同,而不是让 AI 直接操作前端 Tiptap JSON、Convex `documents.content` 私有结构或历史 `blocks` 表。
---
## 2. 为什么参考飞书 CLI
`cli-main` 的价值不在于“飞书有很多命令,所以 mnote 也应该堆很多工具”,而在于它把文档工具做成了三层:
1. **Shortcut 层**:面向人和 Agent 的高层命令,例如 `docs +fetch``docs +update``docs +media-insert`
2. **稳定文档操作协议层**:用 `doc-format``scope``detail``command``block_id``revision_id``dry-run` 表达文档读写。
3. **底层 API adapter 层**:把稳定协议翻译成真实平台 API,例如 `docs_ai/v1/documents` 或 MCP tool call。
这正好对应 mnote 当前问题:
- Rust kernel、Page Aggregate、Convex、leptos-tiptap 还在收口。
- 如果现在让 Hermes 直接调用底层 shape,后续底层一变,skill/tool 就会失效。
- 如果先冻结一个 AI 面向的稳定文档操作协议,底层变动可以收口在 adapter。
因此 mnote 应参考的是飞书的“稳定投影 DSL + 少量高层命令 + dry-run 诊断层”,而不是照搬 Go CLI 或飞书 API。
---
## 3. `cli-main` 具体参考位置
参考根目录:
```text
/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main
```
优先参考下表,不需要整仓搬运:
| mnote 主题 | 参考文件 | 搜索点 | 可吸收内容 | 不采用内容 |
| --- | --- | --- | --- | --- |
| 文档工具注册 | `shortcuts/doc/shortcuts.go` | `Shortcuts()``docs +fetch``docs +update` | 工具分组、命令命名、文档域工具入口 | 不照搬 CLI 交互层 |
| 文档读取 v2 | `shortcuts/doc/docs_fetch_v2.go` | `executeFetchV2``buildReadOption``--detail``--scope` | `simple/with-ids/full``outline/range/keyword/section`、局部读取 | 不使用飞书 token/API |
| 文档更新 v2 | `shortcuts/doc/docs_update_v2.go` | `validCommandsV2``buildUpdateBody``revision-id``dry-run` | `str_replace/block_insert_after/block_replace/block_delete/block_move_after/append/overwrite` | 不直接把飞书 command 当 mnote 内部命令名 |
| v1 兼容更新 | `shortcuts/doc/docs_update.go` | `CallMCPTool``update-doc` | 兼容层与主线层并存时的隔离方式 | 不保留多套长期真相 |
| 更新前诊断 | `shortcuts/doc/docs_update_check.go` | `CheckDocsUpdateArgs``warning` | 工具调用前给 Agent 的静态语义警告 | 不只靠 LLM 自觉避免危险编辑 |
| 媒体插入编排 | `shortcuts/doc/doc_media_insert.go` | `dry-run``steps``batch_update` | 多步工具先 dry-run 展示计划,再执行 | 第一阶段不做完整媒体工具 |
| 文档 XML DSL | `skills/lark-doc/references/lark-doc-xml.md` | `<title>``<callout>``<grid>``<img>``<cite>` | 用 PageXML/PageMarkdown 屏蔽底层块结构 | 不采用飞书专有块类型作为 mnote 类型 |
| 文档更新说明 | `skills/lark-doc/references/lark-doc-update.md` | `str_replace``block_insert_after``revision``warnings` | 面向 Agent 的工具使用说明、返回结构 | 不把说明当实现 |
| 文档读取说明 | `skills/lark-doc/references/lark-doc-fetch.md` | `detail``scope``with-ids` | 读取前先定位、再编辑的 workflow | 不让 AI 默认整页读取超大正文 |
| 工具抽象 | `shortcuts/common/types.go` | `Shortcut``Validate``Execute``Risk` | 元信息、权限、风险、dry-run、validate 一体化 | 不复制 Go 框架 |
| dry-run 通用能力 | `internal/cmdutil/dryrun.go` | `DryRun` | 所有写工具都能返回计划和风险 | 不做纯 CLI 文本输出 |
| 风险提示 | `internal/cmdutil/risk.go` | `Risk` | 工具风险等级进入确认和审计 | 不用 CLI prompt 作为 Web 确认机制 |
快速定位命令:
```bash
cd /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main
rg -n "validCommandsV2|buildReadOption|buildUpdateBody|revision|dry-run|CheckDocsUpdateArgs|Shortcut|Risk|CallMCPTool|scope|detail" shortcuts internal skills/lark-doc
```
---
## 4. 官方 Tiptap AI Toolkit 与补充参考的吸收边界
Tiptap AI Toolkit 的核心价值是把编辑器能力拆成 AI 可调用工具,而不是让外部系统直接暴露编辑器内部实现。Context7 与本地 `tiptap-docs` 公开文档能确认的主线能力包括:
- `toolDefinitions()` 向 AI SDK 暴露工具定义。
- `tiptapRead` 用高效格式读取文档,支持 range/chunk 类读取模型。
- `tiptapEdit` 用 operations 列表编辑文档。
- `tiptapReadSelection` 读取当前选区。
- `executeTool` / `streamTool` 把 AI 生成的 tool call 应用到 editor,并返回 `docChanged`、错误和工具结果。
- review options 支持 `disabled/review/preview/trackedChanges`,但 preview/suggestions 与持久 tracked changes 是两类语义。
需要修正之前口径:
- 不能再说 “Tiptap 没有官方 AI 文档工具”。它有官方 AI Toolkit 工具层。
- 也不能说 “Tiptap AI Toolkit 可以直接解决 mnote 块工具”。公开文档没有给出完整 `tiptapEdit.operations` schema,且它面向 Tiptap/ProseMirror 文档层,不覆盖 mnote 的 Rust kernel、Page Aggregate、Convex revision/conflict key、Hermes audit。
- 当前 npm registry 无法直接获取 `@tiptap-pro/ai-toolkit*` 完整实现源码;本仓可参考的是公开 docs 与 `tiptap-main/packages/server-ai-toolkit``extension-unique-id``extension-drag-handle``extension-node-range` 等开源代码。
这些能力应映射为 mnote 自己的工具层:
| Tiptap AI Toolkit | mnote 对应 | 吸收内容 | 不吸收内容 |
| --- | --- | --- | --- |
| `tiptapRead` | `mnote.doc.fetch` / `mnote.block.fetch` | 先读、带范围、返回适合 AI 的文档表示 | 不把 Tiptap JSON 作为长期工具格式 |
| `tiptapEdit` | `mnote.doc.plan_update` + `mnote.block.*` | 操作列表、reviewable edit、meta justification | 不让浏览器 editor command 成为事实源 |
| `tiptapReadSelection` | `mnote.doc.fetch scope=selection` | selection-aware workflow | 不直接持久化浏览器 selection range |
| `toolDefinitions()` | Rust Hermes manifest | schema、description、capability、annotation | 不依赖私有 npm 包作为运行时硬依赖 |
`tiptap-apcore` 的补充价值更偏工具基础设施。它已经把 Tiptap command 分成 query、format、content、destructive、selection、history,并为工具提供:
- `inputSchema` / `outputSchema`
- `readonly` / `destructive` / `idempotent` / `requiresApproval`
- ACL role`readonly``editor``admin`
- `selectionEffect`
- executor 前置检查、ACL、query/command 分发
mnote 应吸收这些元数据,但命名和执行面必须改成 Rust-owned
```json
{
"name": "mnote.block.replace",
"capabilityScope": ["page.write", "block.write"],
"annotations": {
"readonly": false,
"destructive": false,
"idempotent": false,
"requiresApproval": true,
"selectionEffect": "destroy"
},
"runtimeOwner": "mnote-web",
"writeOwner": "rust-runtime-kernel"
}
```
`tiptap-ai-autocomplete` 的价值限于交互层:
- ghost text 定位。
- 选区 bubble menu。
- streaming preview。
- accept/reject 后再写入。
它不应进入 Hermes tool contract 的核心,只能作为页面 AI 面板、选区 AI 菜单和 preview UI 的参考。
`tiptap-main` 开源代码给 mnote 的补充约束:
- `extension-unique-id` 可作为 Tiptap runtime 节点 id 辅助,但不能替代 Rust `EditorBlock.block_id`
- `server-ai-toolkit``_hash` 是 AI 编辑定位/变化检测辅助,不能替代业务 `blockId`;它可以参与 mnote `revisionRef` 的 hash 部分。
- `extension-drag-handle``extension-node-range` 说明块选择、拖拽、selection toolbar AI 应共享一套 node range 计算,而不是每个入口重新解析 DOM。
- schema awareness / editor context 应进入 `mnote.doc.fetch` 或工具 manifest 的上下文生成,避免 AI 猜测当前可用块类型。
---
## 5. 当前 mnote 稳定性判断
### 5.1 已经可以承载工具设计的边界
- Page Aggregate 读合同已经稳定为 `mnote.page_aggregate.v1`
- 页面主读链已经可以通过 Rust `/api/page-aggregate/:id` 返回 meta/content/options/body snapshot。
- Hermes 页面 AI 已有 `mnote.*` 工具注册、dispatch、trace/audit、idempotency、dryRun 基础。
- 页面级工具 `mnote.page.get/save/update_title/update_options` 已能完成最小闭环。
- Rust `core-protocol` 已有工具规格方向,`bridge-runtime` 已出现 `doc_insert_blocks``doc_replace_range` 等工具/测试雏形。
这些足以支撑“工具合同设计”和“只读/定位/dry-run 工具实现”。
### 5.2 还不适合大规模开放块级写工具的边界
- 当前保存主链仍是 `documents.content` legacy JSON snapshot,不是 `EditorBlockDocument` 原生落库,也不是 Tiptap JSON 原生落库。
- 历史 `blocks` 表仍存在,但没有维护正文主链所需的 order、父子顺序和 `documents.content` 同步闭环,不能描述为当前正文事实源。
- Page Aggregate 当前 `body.content` 仍主要是 `documents:getContent` 的 projection 包装,不是强类型 block protocol。
- Page Aggregate 当前仍有 compat join 痕迹,不是所有页面正文语义都已经 kernel-native。
- 部分 Rust block/editor command 已有 runtime 映射,但 Convex 执行面、editor session 刷新和真实页面可见性还需要逐条验收。
- 块移动、块复制、块嵌入、块引用维护需要稳定块 id、revision、父子关系、排序、权限和冲突处理;当前不应直接给 Hermes 开生产写入口。
- `mnote.page.save` 当前是整页/追加/前置级写入,适合作为过渡和兜底,不适合承诺“精确块编辑”。
- `mnote.page.save` 当前要求 `dryRun/idempotencyKey`,但页面级 revision/conflict key 还不是强制写入门槛;块级写工具开放前必须补上强制 CAS 或等价冲突阻断。
### 5.3 设计上的硬边界
- AI 工具不得绕过 Rust runtime 直接写 Convex。
- AI 工具不得把前端 Tiptap JSON 当长期外部合同。
- AI 工具不得依赖浏览器临时 DOM id 或 runtime-only selection。
- 写工具必须有 `dryRun``idempotencyKey``revision` 或等价冲突键。
- 写工具必须返回 `warnings`,并能阻止明显错误的编辑假设。
- 写工具成功后必须能通过真实页面、`/api/page-aggregate` 和再次 `mnote.*.fetch/get` 三处验收。
---
## 6. 工具体系分层
### L0:内部 kernel / adapter 命令
只给 Rust runtime、Page Aggregate adapter、Convex bridge 使用,不直接暴露给 Hermes。
示例:
- `page.aggregate.get`
- `page.body.save`
- `page.body.apply_patch`
- `editor.block.insert_after`
- `editor.block.replace`
- `editor.block.delete`
- `editor.block.move_after`
- `tree.node.create`
- `tree.subtree.move`
要求:
- 可以随内核演进调整。
- 必须有测试覆盖。
- 由 L1 canonical tools 翻译调用。
### L1AI 可调用 canonical tools
Hermes skill/plugin 对外暴露的稳定工具层。第一阶段只做少量、可解释、可验收工具。
建议命名:
- `mnote.doc.fetch`
- `mnote.doc.find`
- `mnote.doc.plan_update`
- `mnote.doc.apply_update`
- `mnote.block.fetch`
- `mnote.block.insert_after`
- `mnote.block.replace`
- `mnote.block.delete`
- `mnote.block.move_after`
- `mnote.page.append`
- `mnote.page.overwrite`
其中:
- `mnote.doc.fetch/find/plan_update` 可先做。
- `mnote.block.insert_after/replace` 是最小块写入切片。
- `mnote.block.delete/move_after` 等架构稳定后再做。
- `mnote.page.overwrite` 是高风险兜底,必须强确认和 dry-run。
### L2workflow tools
面向用户任务的编排工具,不应第一阶段优先做。
示例:
- `mnote.workflow.write_weekly_report`
- `mnote.workflow.rewrite_section`
- `mnote.workflow.extract_action_items`
- `mnote.workflow.create_meeting_note`
- `mnote.workflow.generate_project_plan`
要求:
- L2 必须调用 L1,不直接写 L0。
- L2 的产物先走 `plan_update` / `dryRun`,用户确认后再执行。
- 不用 L2 掩盖 L1 工具合同不稳定的问题。
---
## 7. 稳定文档表示
### 7.1 PageMarkdown
适合纯文本、标题、列表、引用、代码、简单表格等常见 AI 输出。
用途:
- AI 生成大纲、摘要、会议纪要。
- `append/prepend/overwrite`
- `str_replace` 简单替换。
限制:
- 不表达复杂属性、块引用、嵌入、资源块、页面块关系。
- 不作为唯一长期格式。
### 7.2 PageXML
参考飞书 DocxXML,但定义 mnote 自己的 PageXML。用于需要稳定 block id、属性、资源引用和结构化块的场景。
最小形态示例:
```xml
<page title="项目计划">
<heading level="2" block-id="heading_1">目标</heading>
<paragraph block-id="p_1">第一段文字</paragraph>
<todo block-id="todo_1" checked="false">确认方案</todo>
<callout tone="info">重要说明</callout>
</page>
```
原则:
- `block-id` 只引用 mnote 已存在或本次 dry-run 分配的新 id。
- PageXML 是 AI 外部合同,内部可翻译为 `EditorBlockDocument`
- 不暴露 Tiptap 节点私有字段。
- 不复制飞书专有块;只取 DSL 思想。
### 7.3 EditorBlockDocument
Rust 内部结构化文档模型,是 PageXML/PageMarkdown 到 Page Aggregate / editor session 的中间形态。
要求:
- 持有稳定 `block_id`
- 能表达 block type、text、attrs、children、parent、order、path、revisionRef。
- 能生成 diff plan。
- 能映射到当前 Convex-backed 保存链路。
- 能在 Tiptap JSON、legacy `documents.content` 与 Page Aggregate block projection 之间做受控转换。
---
## 8. Canonical tool schema 草案
### 8.1 `mnote.doc.fetch`
用途:读取当前文档或局部文档,返回可供 AI 定位和编辑的稳定投影。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"format": "markdown",
"detail": "with_ids",
"scope": "full",
"startBlockId": null,
"endBlockId": null,
"keyword": null,
"sectionTitle": null,
"contextBefore": 2,
"contextAfter": 2,
"maxDepth": 6
}
```
字段约束:
- `format`: `markdown | page_xml | text | json`
- `detail`: `simple | with_ids | full`
- `scope`: `full | outline | range | keyword | section | selection | block`
返回:
```json
{
"ok": true,
"revision": "rev_12",
"conflictDetectionKey": "doc_1:12",
"format": "markdown",
"detail": "with_ids",
"scope": "section",
"content": "## 目标 <!-- block:heading_1 -->\n正文 <!-- block:p_1 -->",
"blocks": [
{ "blockId": "heading_1", "type": "heading", "text": "目标", "depth": 0, "revisionRef": "pageRev:rev_12:block:heading_1:hash:aaa" },
{ "blockId": "p_1", "type": "paragraph", "text": "正文", "depth": 1, "revisionRef": "pageRev:rev_12:block:p_1:hash:bbb" }
],
"warnings": []
}
```
验收标准:
- [ ] `scope=full` 能返回当前页面正文,且 `with_ids` 包含稳定 `blockId`
- [ ] `scope=outline` 只返回标题/层级和必要 block id。
- [ ] `scope=keyword` 返回命中块和前后上下文。
- [ ] 返回内容与 `/api/page-aggregate/:id` 的 block snapshot 一致。
- [ ] 不依赖浏览器 DOM。
### 8.2 `mnote.doc.find`
用途:在当前文档中查找关键词、块类型、标题或引用目标,帮助 AI 先定位再编辑。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"query": "待办",
"match": "text",
"limit": 20
}
```
返回:
```json
{
"ok": true,
"matches": [
{
"blockId": "todo_1",
"type": "todo",
"text": "确认待办",
"path": ["项目计划", "本周"],
"score": 0.92
}
]
}
```
验收标准:
- [ ] 可以按文本查找。
- [ ] 可以按 block type 查找。
- [ ] 返回结果能直接作为 `block.replace/insert_after` 的 anchor。
### 8.3 `mnote.doc.plan_update`
用途:只生成变更计划和诊断,不实际写入。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"revision": "rev_12",
"conflictDetectionKey": "doc_1:12",
"command": "block_replace",
"format": "markdown",
"blockId": "p_1",
"content": "替换后的段落",
"dryRun": true
}
```
支持命令:
- `str_replace`
- `block_insert_after`
- `block_replace`
- `block_delete`
- `block_move_after`
- `append`
- `overwrite`
返回:
```json
{
"ok": true,
"dryRun": true,
"revision": "rev_12",
"plan": [
{
"op": "replace",
"targetBlockId": "p_1",
"before": "旧段落",
"after": "替换后的段落"
}
],
"warnings": [],
"requiresConfirmation": true,
"risk": "medium"
}
```
诊断规则参考 `docs_update_check.go`
- 替换目标匹配多个位置时,提示先用 `blockId` 精确定位。
- `str_replace` 找不到唯一匹配时,不执行。
- 跨多个块的自然语言替换必须转成 `block_delete + block_insert_after``block_replace`
- `overwrite` 必须标高风险。
- 缺少 `revision` 时只允许 dry-run,不允许真实写入。
验收标准:
- [ ] 所有写命令都能先 dry-run。
- [ ] dry-run 不改变 Convex 内容、不触发 editor 内容变化。
- [ ] 返回的 `plan` 可被 UI 折叠展示。
- [ ] 明显不安全的编辑返回 `warnings``blocked=true`
### 8.4 `mnote.doc.apply_update`
用途:执行已经 dry-run 过的文档变更计划。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"revision": "rev_12",
"conflictDetectionKey": "doc_1:12",
"idempotencyKey": "idem_1",
"planId": "plan_1",
"command": "block_replace",
"format": "markdown",
"blockId": "p_1",
"content": "替换后的段落",
"dryRun": false
}
```
执行要求:
- 必须校验 `revision`
- 必须校验 `idempotencyKey`
- 必须落 Rust runtime / Page Aggregate command。
- 成功后必须返回新 revision 和受影响 block id。
返回:
```json
{
"ok": true,
"revision": "rev_13",
"changedBlocks": [
{ "blockId": "p_1", "op": "replace" }
],
"audit": {
"effect": "write",
"commandName": "page.body.apply_update",
"commandId": "cmd_1"
}
}
```
验收标准:
- [ ] 执行后 `/api/page-aggregate/:id` 能读到更新。
- [ ] 当前打开页面能自动刷新或通过 editor session reload 看到更新。
- [ ] 再次 `mnote.doc.fetch` 能读到更新。
- [ ] 重复同一 `idempotencyKey` 不造成重复写入。
- [ ] revision 冲突返回 `mnote_tool_conflict`
### 8.5 `mnote.block.fetch`
用途:读取单个块及可选上下文,作为 `tiptapReadSelection` / selection-aware editing 的稳定服务端版本。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"blockId": "p_1",
"revision": "rev_12",
"includeChildren": true,
"contextBefore": 1,
"contextAfter": 1,
"format": "json"
}
```
字段约束:
- `blockId` 必须来自 `mnote.doc.fetch/find` 或 Page Aggregate block projection。
- `format`: `json | markdown | page_xml | text`
- `includeChildren` 默认 `true`,但复杂块可返回 `unsupportedReason`
- `contextBefore/contextAfter` 只返回同父级上下文。
返回:
```json
{
"ok": true,
"revision": "rev_12",
"block": {
"blockId": "p_1",
"type": "paragraph",
"text": "正文",
"attrs": {},
"path": [2],
"parentBlockId": null,
"order": "00020000",
"revisionRef": "pageRev:rev_12:block:p_1:hash:abc",
"editable": true
},
"context": {
"before": [],
"after": [
{ "blockId": "p_2", "type": "paragraph", "text": "下一段" }
]
},
"warnings": []
}
```
验收标准:
- [ ] `block.fetch` 能读取 `doc.find` 返回的 block id。
- [ ] 返回 `revisionRef`,可被后续写工具用于冲突检测。
- [ ] 不存在 block 返回 `mnote_block_not_found`
- [ ] 不可编辑块返回 `editable=false``unsupportedReason`
### 8.6 `mnote.block.replace`
用途:最小精确块写工具,作为第一批块级写入候选。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"blockId": "p_1",
"revision": "rev_12",
"conflictDetectionKey": "doc_1:12",
"blockRevisionRef": "pageRev:rev_12:block:p_1:hash:abc",
"format": "markdown",
"content": "替换后的块内容",
"idempotencyKey": "idem_1",
"dryRun": true
}
```
验收标准:
- [ ] 只替换目标 block,不影响相邻 block。
- [ ] block id 稳定,刷新后仍可定位。
- [ ] 支持 `dryRun=true`
- [ ] 支持真实页面 smoke。
### 8.7 `mnote.block.insert_after`
用途:在指定块后插入一个或多个块。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"anchorBlockId": "p_1",
"revision": "rev_12",
"conflictDetectionKey": "doc_1:12",
"anchorRevisionRef": "pageRev:rev_12:block:p_1:hash:abc",
"format": "markdown",
"content": "- 新待办",
"idempotencyKey": "idem_2",
"dryRun": true
}
```
验收标准:
- [ ] 插入位置准确。
- [ ] 新 block id 由 Rust/runtime 分配或确认,不由 AI 自造。
- [ ] 当前页面能看到新块。
- [ ] 再次 fetch 能拿到新 block id。
### 8.8 `mnote.block.move_after`
用途:移动一个块到同父级 anchor 块后。第一阶段仅作为受限结构性写工具开放。
入参:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"blockId": "p_3",
"anchorBlockId": "p_1",
"revision": "rev_12",
"conflictDetectionKey": "doc_1:12",
"blockRevisionRef": "pageRev:rev_12:block:p_3:hash:aaa",
"anchorRevisionRef": "pageRev:rev_12:block:p_1:hash:bbb",
"idempotencyKey": "idem_move_1",
"dryRun": true
}
```
第一阶段允许:
- `blockId``anchorBlockId` 同父级。
- 普通 paragraph。
- 普通 heading 叶子块。
- 普通 todo 叶子块。
第一阶段阻断:
- 跨父级移动。
- 标题带子块整体移动。
- 列表项跨层级移动。
- 表格、resource、mindmap、page reference。
- 跨页面移动。
返回:
```json
{
"ok": true,
"dryRun": true,
"risk": "medium",
"diff": [
{
"op": "move_after",
"blockId": "p_3",
"anchorBlockId": "p_1",
"from": { "parentBlockId": null, "order": "00030000" },
"to": { "parentBlockId": null, "afterOrder": "00010000" }
}
],
"warnings": []
}
```
验收标准:
- [ ] dry-run 不改变页面。
- [ ] 正式写入后 moving block id 保持不变。
- [ ] 同父级顺序正确。
- [ ] 不支持场景返回 `blocked=true` 和明确 warning。
- [ ] 再次 `mnote.doc.fetch` 能读回新顺序。
---
## 9. 实施阶段
### Phase A:只读和定位
目标:
- 建立 `mnote.doc.fetch``mnote.doc.find`
- 支持 `format``detail``scope`
- 让 AI 能稳定读到 block id、标题路径、上下文。
可做:
- [ ] 从 Page Aggregate snapshot 生成 PageMarkdown。
- [ ] 从 Page Aggregate snapshot 生成 PageXML 最小子集。
- [ ] 支持 `scope=full/outline/keyword/block`
- [ ] 支持 `detail=simple/with_ids/full`
- [ ] Hermes tool event UI 展示 fetch/find 摘要。
验收:
- [ ] 真实登录页面中,Hermes 调用 `mnote.doc.fetch` 能读出当前页面。
- [ ] `with_ids` 返回的 block id 与 DOM `data-block-id` / Page Aggregate 一致。
- [ ] `keyword` 读取不会返回整页超大正文。
- [ ] 失败时返回权限/不存在/空页面的结构化错误。
### Phase Bdry-run / plan
目标:
- 建立 `mnote.doc.plan_update`
- 写入前先给出可解释 diff、风险、warnings。
可做:
- [ ] 支持 `str_replace` dry-run。
- [ ] 支持 `block_replace` dry-run。
- [ ] 支持 `block_insert_after` dry-run。
- [ ] 支持 `append` dry-run。
- [ ] 实现静态诊断规则。
验收:
- [ ] dry-run 不改变页面内容。
- [ ] UI 可折叠展示 plan。
- [ ] 多重匹配、不存在 block、缺 revision 都返回明确 warning。
### Phase C:最小块级写入
目标:
- 开放 `mnote.block.replace``mnote.block.insert_after` 的真实写入。
前置硬门槛:
- [ ] Page Aggregate 中每个可编辑块都有稳定 `blockId`
- [ ] Rust runtime 能把 block replace/insert 变更映射到持久化结构。
- [ ] Convex-backed 保存链能保存变更并刷新。
- [ ] 当前 editor session 能 reload 或 live refresh。
- [ ] smoke 覆盖 AI tool -> Rust -> Convex -> 页面可见 -> fetch 回读。
验收:
- [ ] AI 替换单个段落后,页面只变这一段。
- [ ] AI 在指定块后插入待办后,页面位置正确。
- [ ] 刷新页面后内容仍在。
- [ ] 版本冲突不会覆盖用户刚刚输入的内容。
### Phase D:多块和结构性写入
目标:
- 开放 `block_delete``block_move_after``block_copy_insert_after`
前置硬门槛:
- [ ] 块父子关系、排序、缩进、折叠状态稳定。
- [ ] 删除/移动能处理子树。
- [ ] 冲突检测覆盖移动前后的邻居和父节点。
- [ ] 有撤销或可审计回滚策略。
验收:
- [ ] 移动标题块时,其子块处理规则明确且测试覆盖。
- [ ] 删除块需要确认,并返回被删除范围。
- [ ] 复制插入生成新 id,不复用旧 id。
### Phase Eworkflow tools
目标:
- 基于 L1 工具做高层 AI 写作工作流。
可做:
- [ ] 生成会议纪要。
- [ ] 生成周报。
- [ ] 重写某一节。
- [ ] 提取待办并插入当前页。
验收:
- [ ] workflow 只调用 L1 canonical tools。
- [ ] 每个 workflow 都能先 plan,再执行。
- [ ] 不出现直接整页覆盖用户内容的默认行为。
---
## 10. 架构优先级判断
当前应并行推进,但优先级要明确:
1. **先稳定 Page Aggregate / block identity / editor save-refresh 链路**:这是块级写工具能否长期可靠的根。
2. **同时设计并冻结 L1 canonical tools 合同**:避免继续把临时页面保存扩展成长期能力。
3. **先实现只读和 dry-run**:这部分对底层写链依赖小,能立即改善 AI 可靠性。
4. **等硬门槛满足后开放最小块写入**:先 `replace` / `insert_after`,不要一口气做完整块操作。
5. **最后做 workflow**:工具层不稳定时,workflow 只会放大错误。
因此,回答“先优化稳定架构,还是继续写工具设计”:
> **架构稳定是实现复杂写工具的前置;工具设计现在就应该继续,而且必须用于反向约束架构稳定的验收标准。**
---
## 11. 近期不做清单
- [ ] 不让 Hermes 直接调用 Convex mutation。
- [ ] 不把 Tiptap JSON 暴露成 AI 长期工具入参。
- [ ] 不把 `mnote.page.save` 包装成所有块编辑的长期方案。
- [ ] 不先做大量 `workflow.*` 工具。
- [ ] 不做无 revision / 无 dry-run / 无 idempotency 的写工具。
- [ ] 不在工具里读取浏览器 DOM 来决定写入位置。
- [ ] 不用页面当前可见文本做唯一定位依据;必须支持 block id 或唯一匹配诊断。
---
## 12. 执行 checklist
### 12.1 设计冻结
- [ ] 确认 `mnote.doc.fetch/find/plan_update/apply_update` 命名。
- [ ] 确认 `PageMarkdown` 最小语法。
- [ ] 确认 `PageXML` 最小语法。
- [ ] 确认 `revision` 来源。
- [ ] 确认 `blockId` 来源只来自 Page Aggregate / Rust runtime。
- [ ] 确认 `warnings` / `risk` / `requiresConfirmation` 返回格式。
验收标准:
- [ ] `design/07-ai/process` 中有稳定工具合同。
- [ ] `7-6` 不再把 `mnote.page.save` 描述为长期块编辑合同。
- [ ] `5-6` 的 Page Aggregate checklist 能引用本稿作为 AI 写入门槛。
### 12.2 只读工具实现
- [ ] Rust tool manifest 增加 `mnote.doc.fetch`
- [ ] Rust tool manifest 增加 `mnote.doc.find`
- [ ] Rust tool manifest 增加 `mnote.block.fetch`
- [ ] dispatch 调用 Page Aggregate snapshot,而不是 Convex 私有 shape。
- [ ] 支持 `detail=simple/with_ids/full`
- [ ] 支持 `scope=full/outline/keyword/block`
- [ ] 工具结果进入 Hermes tool event UI。
验收标准:
- [ ] `cargo test -p mnote-web hermes` 相关测试通过。
- [ ] 真实网页登录后,AI 能通过 `mnote.doc.fetch` 读取当前页。
- [ ] AI 能通过 `mnote.doc.find` 找到指定文本所在 block id。
- [ ] `/api/page-aggregate/:id` 与 tool 返回 block id 一致。
### 12.3 dry-run 实现
- [ ] 增加 `mnote.doc.plan_update`
- [ ] 支持 `str_replace` 计划。
- [ ] 支持 `block_replace` 计划。
- [ ] 支持 `block_insert_after` 计划。
- [ ] 返回 `plan/warnings/risk/requiresConfirmation`
- [ ] 静态诊断阻止多重匹配和缺 revision 的真实写入。
验收标准:
- [ ] dry-run 不改变页面。
- [ ] UI 能显示工具计划。
- [ ] 多重匹配返回 warning。
- [ ] 不存在 block 返回结构化错误。
### 12.4 最小块写入实现
- [ ] 打通 `mnote.block.replace`
- [ ] 打通 `mnote.block.insert_after`
- [ ] 写入统一走 Rust runtime / Page Aggregate command。
- [ ] 成功后触发当前 editor session reload 或 live refresh。
- [ ] 成功后返回新 revision 和 changedBlocks。
验收标准:
- [ ] AI 替换单块,页面立即可见。
- [ ] AI 插入新块,位置准确。
- [ ] 刷新后内容仍在。
- [ ] 再次 fetch 能读回变更。
- [ ] revision 冲突被拦截。
### 12.5 真实网页 smoke
- [ ] 新建测试页面,写入唯一前缀 `TEST-AI-TOOL-<timestamp>`
- [ ] `mnote.doc.fetch scope=full detail=with_ids` 读取页面。
- [ ] `mnote.doc.find` 定位测试段落。
- [ ] `mnote.doc.plan_update command=block_replace dryRun=true` 生成计划。
- [ ] `mnote.block.replace dryRun=false` 替换段落。
- [ ] 页面截图证明内容可见。
- [ ] `/api/page-aggregate/:id` 证明内容持久化。
- [ ] `mnote.doc.fetch` 再次证明 AI 可读回。
验收标准:
- [ ] 证据目录写入 `tmp/hermes-tester/<run-id>/`
- [ ] 失败时记录到 `bugs/07-ai/process/` 或真正 owner 分类。
- [ ] 通过后才能把对应 checklist 勾到 done。
---
## 13. 与当前页面 AI 的关系
当前已有工具继续保留:
- `mnote.page.get`
- `mnote.page.save`
- `mnote.page.update_title`
- `mnote.page.update_options`
- `mnote.artifact.*`
但口径调整为:
- `mnote.page.get` 是页面级读取,不是长期精确块读取。
- `mnote.page.save` 是页面级兜底写入,只适合 append/prepend/replace 等粗粒度操作。
- 精确编辑应迁移到 `mnote.doc.*` / `mnote.block.*`
- 页面 AI 面板展示工具时,应把 `page.save` 标为高风险或粗粒度。
---
## 14. 迁移完成定义
本稿不能标记 `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”的顺序调用。