Files
mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md
T
lix-2026 f292c6710a 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
2026-05-16 22:03:30 +08:00

10 KiB
Raw Blame History

2-1 [done] 页面块存储与投影对齐方案 v1

更新时间:2026-05-16

当前状态: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/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/done/7-9-page-block-ai-tooling-roadmap-v1.md

代码依据:

  • /mnt/Data1T/mnote/wolai-frontend/convex/schema.ts
  • /mnt/Data1T/mnote/wolai-frontend/convex/documents.ts
  • /mnt/Data1T/mnote/wolai-frontend/convex/blocks.ts
  • /mnt/Data1T/mnote/rust/crates/storage-convex-bridge/src/mapping.rs
  • /mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs

1. 结论

页面块 AI 工具要可读、可写、可移动,必须先把三层对齐:

Rust canonical block model
  <-> Page Aggregate block projection
  <-> Convex persisted page body snapshot

当前真实持久化主链仍是:

documents.content
documents.content_revision
documents.content_conflict_key

blocks 表存在,但当前 schema 没有 order_key 字段,mutation 也没有维护父子排序和正文 snapshot 的一致性;因此第一阶段不能把 blocks 表描述为页面正文事实源。

第一阶段正确策略是:

Rust 先持有块语义和命令计划,Convex 仍保存 canonical snapshot 到 documents.contentPage Aggregate 从 snapshot 投影出稳定 block view。等块投影、revision、刷新链路稳定后,再评估是否把 blocks 表升级为写主链。


2. 当前存储状态

2.1 documents

wolai-frontend/convex/schema.tsdocuments 保存:

  • content: v.any()
  • content_revision: v.optional(v.number())
  • content_conflict_key: v.optional(v.union(v.string(), v.null()))
  • raw_text
  • 页面标题、父级、排序、页面设置、统计等字段

documents.tsupdateContent / updateDocumentContentRecord 已经支持:

  • expectedRevision
  • conflictDetectionKey
  • revision 自增
  • conflict key 更新为 ${documentId}:${nextRevision}
  • raw text 回写
  • ingest job debounce

这说明当前页面正文冲突检测已经有页面级版本基础。

2.2 blocks

blocks 表当前字段:

  • id
  • workspace_id
  • page_id
  • parent_block_id
  • type
  • props
  • content
  • child_block_ids
  • created_by
  • created_at
  • updated_at

当前问题:

  • schema 没有 order_key,但 blocks.ts 的 append/insertAfter 入参包含 orderKey 却未持久化。
  • insertAfter 只插入新 block,不维护 sibling order。
  • replace/remove 操作的是 blocks 表,不会同步 documents.content
  • storage mapping 中旧 insert_block/update_block/move_block/delete_block 指向 blocks 类命令,但当前长期页面正文保存仍映射到 documents:updateContent

因此 blocks 表当前只能视为历史/辅助/实验 substrate,不是页面正文单一真源。

2.3 Rust command 到 Convex 的映射

storage-convex-bridge 当前将:

  • documents.save
  • page.body.save
  • blocks.patch
  • tree.node.embed

映射到 documents:updateContent

这说明当前正式页面正文写入边界仍是 page.body.save -> documents:updateContent


3. 对齐目标

3.1 短期目标

短期固定为:

AI tool / editor command
  -> Rust EditorCommand
  -> canonical EditorBlockDocument
  -> legacy content snapshot
  -> page.body.save
  -> documents:updateContent
  -> Page Aggregate reload

允许:

  • 底层整份 snapshot 回写。
  • documents.content 继续是 persisted body。
  • blocks 表保持只读参考或历史兼容。

不允许:

  • AI 直接写 documents.content 私有数组。
  • AI 直接写 blocks 表并期待页面正文自动变化。
  • 前端临时 Tiptap JSON 成为存储事实源。

3.2 中期目标

中期应让 PageAggregate.page_body 输出:

  • content
  • revision
  • conflictDetectionKey
  • blockDocument
  • blockProjectionVersion
  • projectionSource

并保证:

  • blockDocument 可由 documents.content 可靠生成。
  • blockDocument 可生成 canonical documents.content
  • 保存后 /api/page-aggregate/:idmnote.doc.fetch 看到相同 block ids。
  • editor session 能基于 revision 刷新。

3.3 长期目标

长期如果升级 blocks 表为写主链,必须先满足:

  • blocks 表补齐 order_key 或等价排序字段。
  • blocks 表与 documents.content 的关系明确:谁是事实源,谁是 materialized snapshot。
  • 每个 block write 都能产出 page body revision。
  • 页面读取、搜索、引用、评论、历史、实时事件都消费同一投影。
  • 有迁移、回填、校验和回滚方案。

在这些完成前,不把 blocks 表提升为页面正文事实源。


4. Revision 与 Conflict Key

4.1 页面级 revision

页面正文当前使用:

content_revision: number
content_conflict_key: `${documentId}:${revision}`

AI 写工具必须携带:

  • expectedRevision
  • conflictDetectionKey
  • idempotencyKey
  • dryRun

缺少 expectedRevisionconflictDetectionKey 时:

  • 只允许 dry-run。
  • 不允许真实写入。

4.2 块级 revisionRef

块级工具需要额外返回 revisionRef

pageRev:{revision}:block:{blockId}:hash:{hash}

用途:

  • 防止 AI 基于旧块内容写入。
  • 防止移动时 anchor 已改变。
  • 帮助 dry-run 返回精确 warning。

说明:

  • hash 可以用 canonical block JSON 或文本/结构摘要计算。
  • Tiptap AI Toolkit _hash 可作为参考,但不能替代 mnote blockId
  • 第一阶段 revisionRef 不需要单独持久化,可以由 Page Aggregate projection 生成。

4.3 冲突粒度

第一阶段冲突粒度:

  • 写入仍以页面级 revision 为硬门槛。
  • 块级 revisionRef 作为额外预检和 warning 来源。

后续如果引入块级持久化:

  • 可以把块级 revision 升级为硬冲突键。
  • 但仍必须推进 page body revision,保证页面快照和实时事件一致。

5. Projection 对齐

5.1 Page Aggregate 输出

PageAggregate.page_body 应从当前:

{
  "content": [],
  "revision": 12,
  "conflictDetectionKey": "doc_1:12"
}

扩展为:

{
  "content": [],
  "revision": 12,
  "conflictDetectionKey": "doc_1:12",
  "blockDocument": {
    "documentId": "doc_1",
    "rootBlockIds": [],
    "blocks": []
  },
  "blockProjectionVersion": 1,
  "projectionSource": "documents.content"
}

5.2 投影来源

投影来源必须显式标记:

  • documents.content:从当前 persisted snapshot 生成。
  • editorDocument:从 canonical editor document 直接生成。
  • tiptapDocument:从 Tiptap JSON bridge 生成。
  • blocksTable:仅在未来 blocks 表成为受控投影或主链后使用。

5.3 Page Aggregate 与 AI 工具一致性

mnote.doc.fetchmnote.block.fetchmnote.block.replacemnote.block.insert_aftermnote.block.move_after 都必须以 Page Aggregate block projection 为读来源。

写入完成后必须验证三处一致:

  • /api/page-aggregate/:id
  • 当前 editor session 可见内容
  • 再次 mnote.doc.fetch / mnote.block.fetch

6. Editor Session Refresh

块写工具成功后,必须解决“后端已写、前端编辑器还停在旧文档”的问题。

第一阶段可选策略:

  1. 触发 page aggregate reload。
  2. 向当前页面 stream/event 发 page.body.saved 或等价 body refresh event。
  3. editor island 收到 revision 变化后 reload canonical content。

最低要求:

  • 写工具返回新 revision。
  • 前端能检测当前 editor session revision 已落后。
  • smoke 中必须证明刷新页面后内容仍在。

不得把“Convex 已保存”当成用户可见编辑闭环。


7. 实施顺序

Phase 1snapshot canonicalization

  • documents.content 稳定生成 EditorBlockDocument
  • EditorBlockDocument 稳定生成 canonical documents.content
  • 为每个可编辑块生成 blockId/type/text/attrs/children/order/path/revisionRef
  • Page Aggregate 输出 block projection。

Phase 2read projection

  • mnote.doc.fetch scope=block/full/outline/keyword 消费 Page Aggregate。
  • mnote.block.fetch 消费 Page Aggregate。
  • 返回 page revision、conflict key、block revision refs。

Phase 3write through Rust command

  • mnote.block.replace 生成 EditorCommand::ReplaceBlock
  • mnote.block.insert_after 生成 EditorCommand::InsertBlockAfter
  • Rust 将命令应用到 canonical EditorBlockDocument
  • 生成 canonical content 后走 page.body.save -> documents:updateContent

Phase 4move gate

  • mnote.block.move_after 先只做同父级叶子块 dry-run。
  • 同父级移动真实写入通过后再考虑标题子树、列表、复杂块。

Phase 5blocks 表评估

  • order_key 设计。
  • 明确 blocks 表是主链还是 materialized projection。
  • 设计迁移和双写校验。
  • 通过后再移动长期写主链。

8. 不做清单

  • 不让 AI 直接调用 documents:updateContent
  • 不让 AI 直接调用 blocks:* mutation。
  • 不把 blocks 表描述为当前页面正文事实源。
  • 不把 Tiptap UniqueID_hash 当作业务 block id。
  • 不在没有 page revision 的情况下真实写入。
  • 不在 editor session refresh 未闭环时宣布块写工具完成。

9. 完成定义

本文已归档为 DONE,done 边界是“页面块存储与投影关系已经冻结为当前代码可执行合同”;后续更宽执行验收继续由 7-10 承接。

  • Page Aggregate 输出稳定 block projection。
  • documents.content -> EditorBlockDocument -> documents.content 有测试覆盖。
  • mnote.block.fetch 能读取 projection 中任意可编辑块。
  • mnote.block.replacemnote.block.insert_after 已进入真实页面最小 smoke。
  • 写入后 revision/conflict key 更新,刷新页面和 AI 回读一致的最小闭环已有 smoke 证据。
  • blocks 表当前定位在代码和设计中不再被误称为正文主链;当前正文主链仍是 documents.content / Page Aggregate block projection 过渡态。