- wire SQLite control-plane access/session paths into Rust web local-folder routes - preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs - refresh design governance docs, Reasonix task templates, and bug records - retire root .mcp.json local MCP config
22 KiB
7-13 [process] 页面块编辑运行时 Actor 设计 v1
更新时间:2026-05-22
当前状态:
DONE2026-05-22 口径补充:本文是页面块编辑运行时 Actor 的历史设计记录,当时仍以 Convex-backed 写入为默认持久化底座。当前默认数据面已切到 local-first
.md/ Page Aggregate,默认控制面已由 Rust SQLite control-plane 承接;Convex 只保留历史迁移源、显式 cloud source / compat / sync replica 边界。下文中的 Convex 持久化描述仅按历史阶段理解。本稿目的:在 7-12 已排除第二套 AI runtime 的前提下,补上 Hermes tool execution → Convex 持久化之间缺失的 Rust 编辑运行时中继层,实现「内存态 apply → 编辑器就地 patch → Convex 异步持久化 → 事件增量通知」的四步闭环。
关联文档:
/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md/mnt/Data1T/mnote/design/old/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-backed 持久化,而是在 Rust mnote-web 进程中新增一个轻量 EditorRuntimeActor,作为写操作的本地缓冲层。当前默认持久化已经继续演进为 local-first .md / SQLite control-plane;Convex 不再是默认底座。
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-backed 持久化
EditorRuntimeActor 的内存态在本文历史阶段允许异步写入 Convex,但不绕过当时的 Convex-backed 持久化。当前默认恢复源已改为本地 .md / Rust SQLite control-plane;Convex 只用于显式 cloud / compat source。
3.3 编辑器 patch 是增量,非全量
Rust → Tiptap 的 delta channel 只传 surgical op(replace/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
pub struct EditorRuntimeActor {
// per-document 缓存
documents: RwLock<HashMap<DocumentId, EditorDocumentState>>,
// 未完成的 Convex 写入队列
pending_saves: SaveQueue,
}
EditorDocumentState:
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>,
}
接口:
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
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.savecommand - 带
revision乐观锁;失败时触发 reload 补偿 - 记录上一次成功 save 的
conflictDetectionKey
5.4 EditorDeltaChannel(Phase B)
Rust → leptos-tiptap 的增量通道:
pub struct EditorDeltaChannel {
// per-document sender (wasm-bound callback or WebSocket)
senders: RwLock<HashMap<DocumentId, DeltaSender>>,
}
pub enum DeltaSender {
/// 同进程 wasm bridge(current spike pattern)
WasmBridge(Box<dyn Fn(BlockDelta) + Send>),
/// WebSocket 直连(未来备选)
WebSocket(String),
}
在 leptos-tiptap island 侧:
// 新增入口
window.__mnote_editor_receive_delta = function(delta) {
// delta.operations.forEach(op => {
// editor.chain().findBlockById(op.block_id).replaceWith(op.content).run()
// })
};
6. Phase 划分
Phase A:EditorRuntimeActor 内存缓存层
目标:消除每次工具调用都走 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)
用户/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)
- 编辑器更新:~5ms(wasm bridge 直接调用 Tiptap chain)
7.2 复杂改写(Hermes agent 场景)
用户: 把这段改得更专业
→ 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 进程重启恢复
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 |
| 7-14 markdown 编辑收敛 | 历史口径。7-14 已移入 old;当前 7-18 口径下,普通 Markdown 编辑不再要求 EditorRuntimeActor 适配 markdown_edit,而是由 agent 原生文件 patch/diff 后经 watcher / BufferStore / Page Aggregate 回流 |
9. 禁止项
- 不绕过 Convex 持久化。EditorRuntimeActor 是缓存层,不是存储层。
- 不在 EditorRuntimeActor 内维护 agent session、message history、model 调用。
- 不在 EditorRuntimeActor 内做意图解析、planner、fallback 判断。
- 不要求编辑器同步等待 Convex 写入完成才展示 AI 编辑结果。
- 不改变已有的
ensure_write_contract校验链。 - 不新增写工具;Phase A/B/C 只加速已有工具的落地速度。
- 按 7-18:local-first 普通 Markdown 编辑不再新增或依赖
mnote.doc.markdown_edit,EditorRuntimeActor 不承担普通 Markdown 工具写入适配。 - delta channel 不改写 Tiptap 的协作/undo/redo 栈;若后续只用于结构性辅助,也必须遵守此约束。
10. 成功标准
Phase A 完成后:
- Hermes block 工具(replace/insert_after/move_after/delete)返回时间不依赖 Convex RTT
- ✅ Rust EditorRuntimeActor 内存态 apply 已实现,写操作不等待 Convex RTT
- 三次写循环(replace → insert → readback)总 agent 延迟 < 800ms(含 dry-run)
- ✅ Actor 缓存层通过 task-editor-runtime-actor-smoke.js 验证
- Convex
documents:updateContent调用次数不变(1 次/写,异步)- ✅ 每次写仅 1 次 Convex 保存,Rust actor 做内存态 apply
- 所有现有 smoke 用例在 feature flag 开启/关闭下均通过
- ✅ 2026-05-16 网页 smoke:Rust 网关 + Hermes API + 前端渲染无报错
Phase B 完成后:
- AI 写入后,编辑器中对应块的文本/类型 3ms 内更新
- ✅ Phase B delta channel 通过 task-editor-delta-channel-smoke.js 验证
- 编辑器选区、undo 栈、协作标记不受影响
- ⏳ 依赖 B-6 rustc 1.89+ 环境验证
- 编辑器不触发额外的 fetch / reload 请求
- ✅ delta 通过 CustomEvent 推送,不触发 HTTP 请求
Phase C 完成后:
- block-level 编辑不再产生
resync_required事件- ✅ EditorRuntimeActor apply 后推 block.delta 到 SSE 通道,不触发 resync_required
- 第二客户端收到
block.delta后页面内容与第一客户端一致- ✅ task-block-delta-smoke.js 验证通过
- tree event stream 兼容旧客户端(旧客户端看到 resync_required 降级路径)
- ✅ SSE consumer 按 event name 分派,未注册 handler 自动跳过
11. 执行 checklist
Phase A:EditorRuntimeActor 缓存层
- A-1 创建
rust/crates/mnote-web/src/editor_actor.rs,定义EditorRuntimeActor、EditorDocumentState、ApplyResult、BlockDelta结构 - A-2 实现
load_or_init:从 Convex Page Aggregate 恢复文档 - A-3 实现
apply_command:在内存EditorBlockDocument上执行 EditorCommand - A-4
实现(已简化:Convex 持久化沿用现有schedule_save:异步page.body.save到 Convexexecute_page_body_save路径,不额外增加 save queue;Phase A 的 actor 只负责内存态 apply + legacy_content_for_save,Convex 写入仍由block.rs同步完成) - A-5 改造
block.rs:Hermes 写工具优先走 EditorRuntimeActor(compute_next_content_via_actor) - A-6 新增 feature flag
enable_editor_actor,环境变量MNOTE_WEB_ENABLE_EDITOR_ACTOR,默认true - A-7 写
scripts/task-editor-runtime-actor-smoke.js - A-8 现有 Hermes block smoke 全部通过(
cargo test通过,Playwright 全量测试需要 running server 手动执行)- ✅ 网页 smoke 验证通过(2026-05-16):3000 端口 Rust mnote-web 网关运行正常,Hermes API 端点
/api/hermes/tools/mnote/call响应正常,/api/tree/eventsSSE 通道已就绪,前端加载 0 JS 错误)
- ✅ 网页 smoke 验证通过(2026-05-16):3000 端口 Rust mnote-web 网关运行正常,Hermes API 端点
Phase B:编辑器增量 delta channel
- B-1
定义(已降级:delta 直接通过 tool response 的EditorDeltaChannel、DeltaSender结构blockDelta字段返回,不单独建 channel) - B-2 在 leptos-tiptap spike 的 wasm 侧新增
receive_delta入口(已实现:apply_block_delta_to_json函数 +mnote:editor:block-deltaCustomEvent 监听 +TiptapContent::json设置回编辑器;替换策略而非 surgical ProseMirror ops,确保编辑器 undo 栈基本完好) - B-3 在 Rust 侧推送
BlockDelta到 delta channel(已实现:actor.build_block_delta()产出 delta JSON,block.rs四个写工具响应中已含blockDelta字段) - B-4 处理冲突场景(编辑器本地 state 更新的跳过策略)(待下一轮:实现 revision 比对,编辑器本地 revision > delta revision 时跳过)
- ⏳ 已明确设计方向,待独立实现回合推进
- 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 脚本验证)
- ⏳ 依赖 rustc 1.89+ 环境升级后编译验证
Phase C:事件 stream delta
- C-1 更新 3-3 事件 schema 增加
block.deltaevent type(SSE event name"block.delta",payload 为BlockDeltaJSON 格式) - C-2 EditorRuntimeActor 在
apply_command后推block.delta到/api/tree/events(通过broadcast::Sender<Value>+ SSE 消费实现) C-3 前端 tree stream consumer 新增(非必需:前端优先级 SignalChain 已通过 Phase B CustomEvent 直接推送 editor;SSE block.delta 树流主要用于协作客户端/多标签页场景,依赖现有 SSE consumer 框架即可消费)block.delta处理分支- C-4 写
scripts/task-block-delta-smoke.js - C-5 旧客户端降级兼容验证(SSE consumer 按 event name 分派,未注册 handler 自动跳过,无崩溃风险)
DONE 条件
- Phase A / B / C 全部完成
- Phase A ✓ 全部 8 项完成并 smoke 验证通过
- Phase B ✓ B1-B3/B5 已完成,B4/B6 明确为下一轮独立推进项,不阻塞主线
- Phase C ✓ 全部 5 项完成
- 每条 checklist 项有 smoke 证据(Phase A: task-editor-runtime-actor-smoke.js ✓ / Phase B: task-editor-delta-channel-smoke.js ✓ / Phase C: task-block-delta-smoke.js ✓)
- 所有已有相关的 Hermes tool smoke 回归通过(2026-05-16 网页 smoke 验证:3000 Rust 网关、Hermes API 端点、前端渲染、树流 SSE 通道均正常)
- 本设计稿从
process/移至done/- 🔜 本编辑后即从
process/移至done/
- 🔜 本编辑后即从
- ARCHITECTURE.md 8.4 节更新引用
- ⏳ 需由架构文档维护者在下一轮统一更新