Files
mnote/design/old/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md
T
Agent Board b798f628ee chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
2026-07-21 05:13:05 +08:00

403 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# [recycle] 5-13 [done] 页面块身份与命令合同 v1
> 更新时间:2026-05-16
>
> 当前状态:`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/done/5-6-page-aggregate-alignment-checklist-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/old/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 view`EditorBlock.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 正式身份
正式块身份字段固定为:
```json
{
"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` 需要新增稳定块投影,建议字段为:
```json
{
"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 正式边界
正式边界固定为:
```text
AI / Hermes tool
-> mnote.doc.* / mnote.block.*
-> Rust canonical tool adapter
-> EditorBlockDocument + EditorCommand
-> Tiptap bridge / legacy content bridge / Convex save adapter
```
不得变成:
```text
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`、父级、同级顺序。
输入:
```json
{
"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 块同父级后插入一个或多个新块。
输入:
```json
{
"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 块之后。
第一阶段输入:
```json
{
"documentId": "doc_1",
"blockId": "b3",
"anchorBlockId": "b1",
"expectedRevision": 12,
"blockRevisionRef": "pageRev:12:block:b3:hash:aaa",
"anchorRevisionRef": "pageRev:12:block:b1:hash:bbb"
}
```
第一阶段只允许:
- `blockId``anchorBlockId` 同父级。
- 目标块是叶子块。
- 类型为 `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`,done 边界是“页面块身份、命令和 Tiptap boundary 合同已经冻结为当前代码可执行合同”;后续完整执行矩阵继续由 `7-10` 承接。
- [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` 私有结构当长期合同。