Files
mnote/design/05-editor-mainline/process/5-13-page-block-identity-and-command-contract-v1.md
T
2026-05-16 12:34:48 +08:00

12 KiB
Raw Blame History

5-13 [process] 页面块身份与命令合同 v1

更新时间:2026-05-16

当前状态:PROCESS

关联文档:

  • /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/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md

代码依据:

  • /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs
  • /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs
  • /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs
  • /mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs

外部参考:

  • /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs/src/content/content-ai/capabilities/ai-toolkit/agents/tools/index.mdx
  • /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs/src/content/content-ai/capabilities/ai-toolkit/api-reference/read-the-document.mdx
  • /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs/src/content/content-ai/capabilities/ai-toolkit/api-reference/execute-tool.mdx
  • /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-main/packages/extension-unique-id/src/unique-id.ts
  • /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-main/packages/server-ai-toolkit/src/hash-extension/server-ai-toolkit-hash-extension.ts

1. 结论

页面块 AI 工具不能建立在“整页 content 字符串/数组 patch”之上。它必须先有一个稳定的页面块合同:

Rust EditorBlockDocument 是页面正文的 canonical block viewEditorBlock.block_id 是 mnote 页面内块身份真相;Tiptap JSON、ProseMirror position、UniqueID、AI Toolkit _hash 都只能是 runtime adapter 或定位辅助。

第一阶段允许底层仍回写整份 documents.content,但命令语义必须先落到 Rust block command

  • editor.block.replace
  • editor.block.insert_after
  • editor.block.delete
  • editor.block.move_after

也就是说,持久化可以暂时是 snapshot save,工具合同不能退化成 snapshot patch。


2. 当前真实状态

2.1 已经存在的基础

core-protocol 已经有正式块模型:

  • EditorBlockDocument.document_id
  • EditorBlockDocument.root_block_ids
  • EditorBlockDocument.blocks
  • EditorBlock.block_id
  • EditorBlock.block_type
  • EditorBlock.props
  • EditorBlock.content_nodes
  • EditorBlock.child_block_ids

core-protocol 也已经有编辑命令形态:

  • EditorReplaceBlock
  • EditorInsertBlockAfter
  • EditorDeleteBlock
  • EditorMoveBlock
  • EditorSplitBlock
  • EditorMergeWithPrevious
  • EditorIndentBlock
  • EditorOutdentBlock

core-protocol/src/editor/tiptap.rs 已经提供 EditorBlockDocumentTiptapBridge,能在 EditorBlockDocument 与 Tiptap JSON 之间转换,并保留段落、标题、列表、任务、引用、代码、分隔线、图片、表格、目录、mindmap 等类型的阶段性映射。

2.2 仍然缺失的合同

现有模型还不足以直接开放成熟 AI 块工具,缺口是:

  • PageAggregate.page_body 只定义了 content/revision/conflictDetectionKey,没有定义 canonical block projection。
  • EditorBlockDocument 还没有在 Page Aggregate 输出中成为稳定字段。
  • EditorBlock 没有显式 parentBlockId/order/path/revisionRef/editable 投影。
  • doc_insert_blocks/doc_replace_range 仍是工具雏形,能够生成部分 editor commands,但仍主要围绕 legacy block array 回写。
  • EditorMoveBlock 有协议形态,但 mnote.block.move_after 的父子、排序、复杂块规则尚未冻结。

因此必须先补本合同,再实现 AI 块读写移动。


3. 块身份规则

3.1 正式身份

正式块身份字段固定为:

{
  "blockId": "block_abc",
  "documentId": "doc_1",
  "workspaceId": "ws_1"
}

规则:

  • blockId 在同一 documentId 内唯一。
  • AI、前端、Tiptap runtime 都不得自行把临时 DOM id 当作 blockId
  • 新块 id 必须由 Rust runtime 分配,或者由 Rust runtime 校验后接受。
  • 保存后再次读取,原块 blockId 必须保持不变。

3.2 复制规则

复制块时:

  • 原块 blockId 不复用。
  • 新块获得新 blockId
  • 若复制子树,子块全部获得新 blockId
  • 块引用、页面引用、mindmap/resource 等外部关联需要单独定义重定向或保留规则,未定义前不得向 AI 开放复制子树工具。

3.3 移动规则

移动块时:

  • 被移动块 blockId 保持不变。
  • 同父级移动只改变 sibling order。
  • 跨父级移动同时改变 parentBlockId 与 order。
  • 第一阶段只开放同父级叶子块移动。
  • 标题带子块、列表项、表格、mindmap、resource、page reference、block reference 必须在单独规则中定义后再开放。

3.4 Tiptap UniqueID_hash

Tiptap 参考结论:

  • UniqueID 可以给 Tiptap 节点补 id,并处理协作、粘贴、拖拽场景下的去重。
  • Server AI Toolkit 的 _hash 是 AI 编辑辅助属性,可帮助定位变化。
  • 这两者都不能替代 mnote 的 EditorBlock.block_id

mnote 规则:

  • Tiptap node attrs 中可以携带 block_id / blockId,但来源必须是 Rust canonical block projection。
  • _hash 可以进入 revisionRef 的辅助计算,但不能成为业务主键。
  • 如果 Tiptap 文档缺少 block id,导入链必须进入“补 id + 记录 warning”的兼容路径,而不是让 AI 直接编辑无身份块。

4. Canonical Block Projection

PageAggregate.page_body 需要新增稳定块投影,建议字段为:

{
  "content": [],
  "revision": 12,
  "conflictDetectionKey": "doc_1:12",
  "blockDocument": {
    "documentId": "doc_1",
    "rootBlockIds": ["b1", "b2"],
    "blocks": [
      {
        "blockId": "b1",
        "type": "paragraph",
        "text": "正文",
        "attrs": {},
        "contentNodes": [],
        "children": [],
        "parentBlockId": null,
        "order": "00010000",
        "path": [0],
        "depth": 0,
        "revisionRef": "pageRev:12:block:b1:hash:abc",
        "editable": true,
        "unsupportedReason": null
      }
    ]
  }
}

字段说明:

  • blockDocument 是 Page Aggregate 给 editor/AI/read view 的 canonical block view。
  • content 保留当前兼容 snapshot,不作为 AI 精确编辑的唯一输入。
  • text 是 AI 读取用摘要,不替代 contentNodes
  • attrs 是稳定属性投影,不暴露 Tiptap 私有字段;确需保留 Tiptap snapshot 时放入内部 adapter,不进入 AI 默认读取。
  • order 是同父级排序投影,第一阶段可由数组位置计算,后续可升级为持久 order key。
  • path 是读取定位辅助,不作为写入主键。
  • revisionRef 是块级冲突辅助,必须包含 page revision 和块内容/结构摘要。

5. EditorBlockDocument 与 Tiptap JSON 边界

5.1 正式边界

正式边界固定为:

AI / Hermes tool
  -> mnote.doc.* / mnote.block.*
  -> Rust canonical tool adapter
  -> EditorBlockDocument + EditorCommand
  -> Tiptap bridge / legacy content bridge / Convex save adapter

不得变成:

AI
  -> Tiptap JSON 私有 shape
  -> Convex documents.content

5.2 Tiptap JSON 的职责

Tiptap JSON 可以用于:

  • 浏览器 editor runtime。
  • ProseMirror command 执行。
  • 兼容导入/导出。
  • 测试 bridge round trip。

Tiptap JSON 不可以用于:

  • AI 长期工具入参合同。
  • mnote 块身份事实源。
  • Convex 长期块结构事实源。

5.3 PageMarkdown / PageXML 的职责

AI 对外格式固定为:

  • PageMarkdown:适合文本、标题、列表、简单块。
  • PageXML:适合携带 block-id、块属性、结构化块。
  • json:只能是 mnote canonical block projection,不是裸 Tiptap JSON。

6. 块命令合同

6.1 editor.block.replace

语义:

替换目标块的类型、属性或内容,不改变目标块 blockId、父级、同级顺序。

输入:

{
  "documentId": "doc_1",
  "blockId": "b1",
  "blockType": "paragraph",
  "props": {},
  "contentNodes": [
    { "type": "text", "text": "替换后的正文", "marks": [] }
  ],
  "expectedRevision": 12,
  "blockRevisionRef": "pageRev:12:block:b1:hash:abc"
}

阻断:

  • blockId 不存在。
  • expectedRevision 不匹配。
  • blockRevisionRef 明显不匹配。
  • 目标块 editable=false
  • 目标块是未定义 replace 规则的复杂块。

6.2 editor.block.insert_after

语义:

在 anchor 块同父级后插入一个或多个新块。

输入:

{
  "documentId": "doc_1",
  "anchorBlockId": "b1",
  "blocks": [
    {
      "type": "todo",
      "props": { "checked": false },
      "contentNodes": [
        { "type": "text", "text": "新增待办", "marks": [] }
      ]
    }
  ],
  "expectedRevision": 12,
  "anchorRevisionRef": "pageRev:12:block:b1:hash:abc"
}

规则:

  • 新块 id 由 runtime 生成。
  • 新块默认进入 anchor 同父级。
  • 插入多个块时保持输入顺序。
  • 第一阶段不允许把复杂子树作为单次 insert payload。

6.3 editor.block.delete

语义:

删除目标块;是否保留、提升或级联删除子块由命令参数明确指定。

第一阶段不直接开放给 AI 正式写入,只允许 dry-run。

必须先冻结:

  • preserveChildren=true 时子块提升到哪里。
  • preserveChildren=false 时删除范围如何展示和审计。
  • block reference / comment thread / resource attachment 如何处理。

6.4 editor.block.move_after

语义:

将目标块移动到 anchor 块之后。

第一阶段输入:

{
  "documentId": "doc_1",
  "blockId": "b3",
  "anchorBlockId": "b1",
  "expectedRevision": 12,
  "blockRevisionRef": "pageRev:12:block:b3:hash:aaa",
  "anchorRevisionRef": "pageRev:12:block:b1:hash:bbb"
}

第一阶段只允许:

  • blockIdanchorBlockId 同父级。
  • 目标块是叶子块。
  • 类型为 paragraph、普通 heading、普通 todo

第一阶段阻断:

  • 跨父级移动。
  • 移动到自己的子树内。
  • 标题带子块整体移动。
  • 列表项层级移动。
  • 表格、mindmap、resource、page reference、block reference。
  • 跨页面移动。

7. 读工具对命令合同的要求

写工具开放前,读工具必须先能返回:

  • blockId
  • type
  • text
  • attrs
  • children
  • parentBlockId
  • order
  • path
  • depth
  • revisionRef
  • editable
  • unsupportedReason

mnote.block.fetch 必须能返回同父级前后文,供 AI 在 replace/insert_after/move_after 前做二次确认。


8. 与 Tiptap AI Toolkit 的关系

Tiptap AI Toolkit 对 mnote 的启发是:

  • tiptapRead 证明 AI 读工具需要高效、可定位的文档读格式。
  • tiptapEdit 证明 AI 写工具应该是 operations,而不是整页替换。
  • tiptapReadSelection 证明 selection/range 需要冻结,不能依赖用户思考期间不断变化的浏览器选择。
  • executeTool/streamTool 证明工具执行面需要返回 docChanged、错误、review/preview 结果。
  • schema awareness 证明 AI 需要明确 editor context,而不是猜块类型。

mnote 不直接采用 Tiptap operations 作为外部合同,原因是:

  • 公开文档没有完整 operations schema。
  • Tiptap operations 面向 ProseMirror 文档层,不覆盖 mnote 的 Rust kernel、Page Aggregate、Convex revision、Hermes audit。
  • mnote 需要稳定到跨编辑器、跨存储演进的块合同。

因此 Tiptap 作为参考模型,不作为 mnote 外部工具合同。


9. 完成定义

本设计不能标记 DONE,直到:

  • 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 私有结构当长期合同。