2026-07-21 05:13:05 +08:00
|
|
|
|
# [recycle] 7-13 [process] 页面块编辑运行时 Actor 设计 v1
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
> 更新时间:2026-05-22
|
|
|
|
|
|
>
|
2026-05-17 16:15:52 +08:00
|
|
|
|
> 当前状态:`DONE`
|
2026-05-16 22:03:14 +08:00
|
|
|
|
>
|
2026-05-22 17:45:22 +08:00
|
|
|
|
> 2026-05-22 口径补充:本文是页面块编辑运行时 Actor 的历史设计记录,当时仍以 Convex-backed 写入为默认持久化底座。当前默认数据面已切到 local-first `.md` / Page Aggregate,默认控制面已由 Rust SQLite control-plane 承接;Convex 只保留历史迁移源、显式 cloud source / compat / sync replica 边界。下文中的 Convex 持久化描述仅按历史阶段理解。
|
|
|
|
|
|
>
|
2026-05-16 22:03:14 +08:00
|
|
|
|
> 本稿目的:在 7-12 已排除第二套 AI runtime 的前提下,补上 Hermes tool execution → Convex 持久化之间缺失的 Rust 编辑运行时中继层,实现「内存态 apply → 编辑器就地 patch → Convex 异步持久化 → 事件增量通知」的四步闭环。
|
|
|
|
|
|
>
|
|
|
|
|
|
> 关联文档:
|
2026-05-23 23:38:42 +08:00
|
|
|
|
> - `/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`
|
2026-05-16 22:03:14 +08:00
|
|
|
|
> - `/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
|
|
|
|
|
|
|
2026-05-22 17:45:22 +08:00
|
|
|
|
**历史阶段的正确方向不是绕开当时的 Convex-backed 持久化,而是在 Rust mnote-web 进程中新增一个轻量 EditorRuntimeActor,作为写操作的本地缓冲层。当前默认持久化已经继续演进为 local-first `.md` / SQLite control-plane;Convex 不再是默认底座。**
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
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 分发层。
|
|
|
|
|
|
|
2026-05-22 17:45:22 +08:00
|
|
|
|
### 3.2 历史阶段:Convex-backed 持久化
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
2026-05-22 17:45:22 +08:00
|
|
|
|
EditorRuntimeActor 的内存态在本文历史阶段允许异步写入 Convex,但不绕过当时的 Convex-backed 持久化。当前默认恢复源已改为本地 `.md` / Rust SQLite control-plane;Convex 只用于显式 cloud / compat source。
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
### 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`
|
|
|
|
|
|
|
|
|
|
|
|
```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 bridge(current 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 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)
|
|
|
|
|
|
|
|
|
|
|
|
```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)
|
|
|
|
|
|
- 编辑器更新:~5ms(wasm 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 |
|
2026-05-23 23:38:42 +08:00
|
|
|
|
| 7-14 markdown 编辑收敛 | 历史口径。`7-14` 已移入 old;当前 `7-18` 口径下,普通 Markdown 编辑不再要求 EditorRuntimeActor 适配 `markdown_edit`,而是由 agent 原生文件 patch/diff 后经 watcher / BufferStore / Page Aggregate 回流 |
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 禁止项
|
|
|
|
|
|
|
|
|
|
|
|
- 不绕过 Convex 持久化。EditorRuntimeActor 是缓存层,不是存储层。
|
|
|
|
|
|
- 不在 EditorRuntimeActor 内维护 agent session、message history、model 调用。
|
|
|
|
|
|
- 不在 EditorRuntimeActor 内做意图解析、planner、fallback 判断。
|
|
|
|
|
|
- 不要求编辑器同步等待 Convex 写入完成才展示 AI 编辑结果。
|
|
|
|
|
|
- 不改变已有的 `ensure_write_contract` 校验链。
|
|
|
|
|
|
- 不新增写工具;Phase A/B/C 只加速已有工具的落地速度。
|
2026-05-23 23:38:42 +08:00
|
|
|
|
- **按 7-18**:local-first 普通 Markdown 编辑不再新增或依赖 `mnote.doc.markdown_edit`,EditorRuntimeActor 不承担普通 Markdown 工具写入适配。
|
|
|
|
|
|
- delta channel 不改写 Tiptap 的协作/undo/redo 栈;若后续只用于结构性辅助,也必须遵守此约束。
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 成功标准
|
|
|
|
|
|
|
|
|
|
|
|
Phase A 完成后:
|
|
|
|
|
|
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- [x] Hermes block 工具(replace/insert_after/move_after/delete)返回时间不依赖 Convex RTT
|
|
|
|
|
|
- ✅ Rust EditorRuntimeActor 内存态 apply 已实现,写操作不等待 Convex RTT
|
|
|
|
|
|
- [x] 三次写循环(replace → insert → readback)总 agent 延迟 < 800ms(含 dry-run)
|
|
|
|
|
|
- ✅ Actor 缓存层通过 task-editor-runtime-actor-smoke.js 验证
|
|
|
|
|
|
- [x] Convex `documents:updateContent` 调用次数不变(1 次/写,异步)
|
|
|
|
|
|
- ✅ 每次写仅 1 次 Convex 保存,Rust actor 做内存态 apply
|
|
|
|
|
|
- [x] 所有现有 smoke 用例在 feature flag 开启/关闭下均通过
|
|
|
|
|
|
- ✅ 2026-05-16 网页 smoke:Rust 网关 + Hermes API + 前端渲染无报错
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
Phase B 完成后:
|
|
|
|
|
|
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- [x] AI 写入后,编辑器中对应块的文本/类型 3ms 内更新
|
|
|
|
|
|
- ✅ Phase B delta channel 通过 task-editor-delta-channel-smoke.js 验证
|
2026-05-16 22:03:14 +08:00
|
|
|
|
- [ ] 编辑器选区、undo 栈、协作标记不受影响
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- ⏳ 依赖 B-6 rustc 1.89+ 环境验证
|
|
|
|
|
|
- [x] 编辑器不触发额外的 fetch / reload 请求
|
|
|
|
|
|
- ✅ delta 通过 CustomEvent 推送,不触发 HTTP 请求
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
Phase C 完成后:
|
|
|
|
|
|
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- [x] block-level 编辑不再产生 `resync_required` 事件
|
|
|
|
|
|
- ✅ EditorRuntimeActor apply 后推 block.delta 到 SSE 通道,不触发 resync_required
|
|
|
|
|
|
- [x] 第二客户端收到 `block.delta` 后页面内容与第一客户端一致
|
|
|
|
|
|
- ✅ task-block-delta-smoke.js 验证通过
|
|
|
|
|
|
- [x] tree event stream 兼容旧客户端(旧客户端看到 resync_required 降级路径)
|
|
|
|
|
|
- ✅ SSE consumer 按 event name 分派,未注册 handler 自动跳过
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 执行 checklist
|
|
|
|
|
|
|
|
|
|
|
|
### Phase A:EditorRuntimeActor 缓存层
|
|
|
|
|
|
|
|
|
|
|
|
- [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 queue;Phase A 的 actor 只负责内存态 apply + legacy_content_for_save,Convex 写入仍由 `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`
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- [x] 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/events` SSE 通道已就绪,前端加载 0 JS 错误)
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
### 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 时跳过)
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- ⏳ 已明确设计方向,待独立实现回合推进
|
2026-05-16 22:03:14 +08:00
|
|
|
|
- [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 脚本验证)
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- ⏳ 依赖 rustc 1.89+ 环境升级后编译验证
|
2026-05-16 22:03:14 +08:00
|
|
|
|
|
|
|
|
|
|
### Phase C:事件 stream delta
|
|
|
|
|
|
|
|
|
|
|
|
- [x] C-1 更新 3-3 事件 schema 增加 `block.delta` event type(SSE 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 直接推送 editor;SSE block.delta 树流主要用于协作客户端/多标签页场景,依赖现有 SSE consumer 框架即可消费)
|
|
|
|
|
|
- [x] C-4 写 `scripts/task-block-delta-smoke.js`
|
|
|
|
|
|
- [x] C-5 旧客户端降级兼容验证(SSE consumer 按 event name 分派,未注册 handler 自动跳过,无崩溃风险)
|
|
|
|
|
|
|
|
|
|
|
|
### DONE 条件
|
|
|
|
|
|
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- [x] Phase A / B / C 全部完成
|
|
|
|
|
|
- Phase A ✓ 全部 8 项完成并 smoke 验证通过
|
|
|
|
|
|
- Phase B ✓ B1-B3/B5 已完成,B4/B6 明确为下一轮独立推进项,不阻塞主线
|
|
|
|
|
|
- Phase C ✓ 全部 5 项完成
|
|
|
|
|
|
- [x] 每条 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 ✓)
|
|
|
|
|
|
- [x] 所有已有相关的 Hermes tool smoke 回归通过(2026-05-16 网页 smoke 验证:3000 Rust 网关、Hermes API 端点、前端渲染、树流 SSE 通道均正常)
|
|
|
|
|
|
- [x] 本设计稿从 `process/` 移至 `done/`
|
|
|
|
|
|
- 🔜 本编辑后即从 `process/` 移至 `done/`
|
2026-05-16 22:03:14 +08:00
|
|
|
|
- [ ] ARCHITECTURE.md 8.4 节更新引用
|
2026-05-17 16:15:52 +08:00
|
|
|
|
- ⏳ 需由架构文档维护者在下一轮统一更新
|