chore: 保存当前架构收口与 bug 修复快照

归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
This commit is contained in:
lix-2026
2026-05-18 17:01:35 +08:00
parent 61ee4a38a2
commit a2cb1338c8
953 changed files with 14383 additions and 211845 deletions
@@ -0,0 +1,371 @@
# 7-25 [process] ACP Session Runtime 增强规划 v1
> 更新时间:2026-05-17
> 参考:`hermes-vscode-main` (ACP client) / `hermes-web-ui-0.5.18` (HTTP API)
## 1. 当前状态
当前 mnote-web 的 ACP 实现是半成品,但已有的 route 和 runtime 壳并不等于“本地会话持久化已完成”:
| 领域 | 已有 | 缺失 |
|------|------|------|
| Wire protocol | `AcpClient`JSON-RPC 2.0 over stdio)✅ | — |
| Session lifecycle | `AcpSessionManager`create/prompt/cancel)✅ | 持久化、多会话管理 ❌ |
| Event dispatch | `AcpSessionEvent` 枚举 + SSE 输出 ✅ | — |
| Session CRUD | `POST /client/sessions` 创建;`GET /client/sessions``GET /client/sessions/{session_id}``POST /client/sessions/{session_id}/resume` 已注册,但当前仍是 Hermes proxy / runtime 壳语义 | 列表/查询/重命名/删除 的本地持久化 API ❌ |
| Run lifecycle | `create_run` / `stream_events` / `abort_run` ✅ | — |
| Session persistence | Hermes HTTP 路径仍遵循 Hermes 持有会话真相;ACP 路径目前没有本地后端 store | ACP-local runtime/session cache 或独立持久化边界未定 ❌ |
| Message history | — | 消息不持久化 ❌ |
| Session search | — | 无搜索能力 ❌ |
| Usage tracking | `usage_update` 已能映射为 SSE `usage.updated` | 无 session 级持久聚合、查询 API 和 UI ❌ |
| Permission request | acp_client.rs 仅日志(bug 3-20 | 无协议响应 ❌ |
| Session resume | route 已注册,`resume_session` 目前仅回显 `get_session` 结果 | 真实 resume / 历史注入 / 续跑语义不完整 ❌ |
| Auto-title | — | 无自动命名 ❌ |
| Conversation export | — | 无导出 ❌ |
| Session list UI | 当前只有页面 AI 的本地短期历史列表(localStorage),不是后端 ACP 会话列表 | 后端会话列表 / 详情 / 搜索 还未接到 UI ❌ |
### 1.1 正确性结论
本文对“ACP runtime 已接入但 session runtime 仍不完整”的判断成立;但“在 mnote-web 中新增本地 `sessions/messages` 并替代内存表”不能直接理解为新的 AI 聊天真相层。
既有 `7-4``7-5``7-8` 已明确:Hermes HTTP 路径的 session/message/tool event/usage/model 真相归 Hermesmnote-web proxy 不保存完整聊天真相。因此本计划若引入本地存储,默认只能用于:
- ACP-local run payload、runtime registry、断线重连、短期恢复所需的技术状态;
- mnote 自己产生的 tool audit、业务写入结果、artifact / page / tree 事实;
- 经单独架构决策确认后的 ACP 会话缓存或索引。
如果要把 mnote 的存储升级为跨 Hermes HTTP / ACP 的长期会话真源,必须先更新 `7-5` / `7-8` 的边界,而不能作为本计划的隐含前提。并且 mnote 是多用户系统,AI session 必须按用户、workspace、document 进行隔离;需要数据库时使用当前 Convex 底座,不新增 SQLite。
## 2. 参考实现分析
### hermes-vscode-mainACP 客户端参考)
- **`acpClient.ts`** — JSON-RPC 2.0 over stdio,支持 request/response/notification/incoming request。 基础结构已移植到 `acp_client.rs`
- **`sessionManager.ts`** — Session 生命周期管理 + `session/update` 事件分发。基础结构已移植到 `acp_session_manager.rs`
- **`sessionStore.ts`** — **关键缺失部分**。会话持久化(VS Code workspaceState),支持:
- 创建 session`createSession`
- 切换 session`switchTo`
- 删除 session`deleteSession`
- 重命名 session`rename`
- 自动命名 session`autoTitle`)— 从第一条用户消息提取标题
- 消息追加(`appendMessage`
- 历史加载(`loadHistory`
- ACP session ID 关联
- **`protocol.ts`** — 事件解析 + 去重。`deduplicateChunk` 逻辑已移植。
- **`types.ts`** — `ChatSession``StoredMessage``TodoState` 等类型。
### hermes-web-ui-0.5.18HTTP REST API 参考)
- **`controllers/hermes/sessions.ts`** — 完整 REST 接口:
- `GET /api/hermes/sessions` — 会话列表
- `GET /api/hermes/sessions/:id` — 会话详情(含消息)
- `DELETE /api/hermes/sessions/:id` — 删除
- `POST /api/hermes/sessions/:id/rename` — 重命名
- `POST /api/hermes/sessions/batch-delete` — 批量删除
- `POST /api/hermes/sessions/:id/workspace` — 工作区关联
- `GET /api/hermes/sessions/conversations` — 会话摘要列表
- `GET /api/hermes/sessions/conversations/:id/messages` — 分页消息
- `GET /api/hermes/search/sessions` — 会话全文搜索
- `GET /api/hermes/sessions/usage` — 使用量统计
- `GET /api/hermes/sessions/:id/export` — 导出
- **`db/hermes/sessions-db.ts`** — 参考实现中的会话存储,支持:
- 完整 CRUD
- 全文搜索(FTS5
- 用量统计
- 消息分页
- **`db/hermes/session-store.ts`** — 本地 JSON 文件备选存储
## 3. 建议新增功能
按优先级分三阶段:
### Phase ASession 持久化与管理(P0 — 缺少则 ACP 无实用价值)
1. **会话持久化存储**
- 默认先建设 ACP-local runtime store / TTL cache,不改 Hermes HTTP 会话真相归属
- 持久化如需数据库,必须落到 Convex;不在 mnote-web 内新增 SQLite
- 表结构先服务 `ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 的可靠生命周期,并且所有记录必须带 `userId` / `workspaceId` / `documentId` 作用域
- `sessions/messages` 如要保存完整聊天历史,必须先完成架构决策并标明只覆盖 ACP 路径还是统一覆盖 Hermes HTTP + ACP
2. **会话管理 API**
- `GET /client/sessions` — 会话列表(分页、排序)
- `GET /client/sessions/{session_id}` — 会话详情(含消息)
- `DELETE /client/sessions/{session_id}` — 删除
- `POST /client/sessions/{session_id}/rename` — 重命名
- `POST /client/sessions/{session_id}/auto-title` — 自动命名
3. **消息持久化**
- ACP `stream_events` 执行过程中按架构决策写入 user message + agent response,或只写 runtime/event 索引
- 支持追加消息到已有 ACP-local sessionHermes HTTP 历史仍从 Hermes 读取
4. **会话搜索**
- `GET /client/sessions/search?q=...` — 全文搜索(基于 Convex 查询能力,必要时再接专用搜索索引)
- 返回匹配的 session 摘要 + snippet
5. **Permission request 响应修复**bug 3-20
- 已知 incoming request 应返回结构化 error 而非仅日志
- `session/request_permission` 至少需要 auto-deny + 通知前端
- 配合 Phase C 的 UI 实现人工确认
### Phase B:历史消息与管理增强(P1 — 常用功能)
6. **会话恢复(Resume**
- 当前 `POST /client/sessions/{session_id}/resume` 已有 route,需实现
- 从 ACP-local store 或 Hermes session store 加载历史消息,作为后续 prompt 的 context 来源
- 支持新模型继续已有会话
7. **分页消息查询**
- `GET /client/sessions/{session_id}/messages?cursor=...&limit=...`
- 支持时间范围和角色过滤
8. **用量记录**
- 从 ACP `usage_update` 事件提取 `contextUsed` / `contextSize`
- 写入 `sessions` 表的 usage 字段
- `GET /client/sessions/usage` — 聚合统计
9. **会话导出**
- `GET /client/sessions/{session_id}/export?format=json|markdown`
- JSON 格式:完整结构化导出
- Markdown 格式:人类可读对话导出
10. **ACP run payload 生命周期修复**bug 3-21
- 停止在 `stream_events``.remove(run_id)` 释放 payload
- 改为基于 Convex session/runtime 记录或 TTL 缓存
### Phase C:前端交互与高级功能(P2 — 用户体验提升)
11. **前端会话列表**
- Sidebar 或独立面板展示历史会话
- 支持切换、删除、重命名
- 支持继续已有会话
12. **会话搜索 UI**
- 搜索框,全字段搜索(标题、消息内容)
- 结果高亮 + 跳转
13. **Token 用量可视化**
- 每次 AI 调用后展示 token 消耗
- 会话级别统计累计用量
14. **权限请求 UI**
- Agent 发起 `session/request_permission` 时前端弹窗确认
- 支持 auto-allow / auto-deny 配置
15. **多会话并发**
- 支持同时运行多个 ACP session
- 前端标签页切换
## 4. 技术方案
### 4.0 当前 Convex 源目录口径
当前仓库状态下:
- `infra/convex/` 只承载自托管 Convex backend/dashboard 的 Docker 与说明,不是 functions/schema 源目录;
- `wolai-frontend/convex/` 已不在当前工作区可读路径中,不能作为新的实现落点;
- `recycle/wolai-frontend/convex/` 只作为历史参考;
- 本轮执行已在仓库根 `convex/` 建立 ACP-local runtime store 的最小 functions/schema,并让 `scripts/run-convex-deploy.js` 优先以仓库根作为 Convex CLI cwd。
后续若要恢复完整业务 Convex functions,需要把历史 `recycle/wolai-frontend/convex/` 中仍需要的 documents/workspaces/media 等 functions 正式迁移到根 `convex/`,不能继续依赖已删除的 `wolai-frontend/` 路径。
### 4.1 存储方案
注意:以下结构是 Convex-backed ACP-local store 候选形状,不默认推翻 Hermes 持有聊天真相的既有边界。mnote-web 不新增 SQLite;多用户隔离字段是硬要求。
```rust
// sessions 记录
struct SessionRow {
id: String, // 主键
user_id: String, // 多用户隔离
workspace_id: Option<String>,
document_id: Option<String>,
title: Option<String>,
profile: String,
model: Option<String>,
source: String, // "acp" | "hermes-http"; hermes-http 默认只保存引用/索引,不保存聊天真相
started_at: i64,
ended_at: Option<i64>,
end_reason: Option<String>,
message_count: u32,
input_tokens: u64,
output_tokens: u64,
preview: Option<String>, // 首条消息摘要
}
// messages 记录
struct MessageRow {
id: i64, // 自增
user_id: String, // 多用户隔离
session_id: String, // FK -> sessions
role: String, // "user" | "assistant" | "tool"
content: String,
reasoning: Option<String>,
tool_calls: Option<Value>, // JSON
token_count: Option<u32>,
created_at: i64,
}
```
### 4.2 现有代码改动范围
- **新增/修改** 根 `convex/` 相关 schema / functions — ACP-local session/runtime 记录、用户隔离查询、TTL 清理;`infra/convex/` 只负责自托管服务部署
- **修改** `acp_session_manager.rs` — 按架构决策写入 ACP-local 消息或只写 runtime/event 索引
- **修改** `hermes_client.rs` — 新增 session 管理 route handlers
- **修改** `routes/mod.rs` — 注册新 route
- **修改** Convex bridge / transport 调用点 — mnote-web 通过已有 Convex 底座读写 session/runtime 索引
### 4.3 与现有 bug 的关系
| Bug | Phase | 说明 |
|-----|-------|------|
| 3-20 ACP incoming request 无响应 | A | permission request 必须有协议响应 |
| 3-21 ACP run payload 消费后移除 | B | Convex-backed store / TTL cache 化后 payload 由 session 管理,不再依赖内存 map |
| 7-19 Hermes 指导优先 apply_block_ops | C | 需统一到 markdown_edit 口径 |
| 7-20 page_ai_workflow 绕过 tool executor | C | 建议统一到 ACP tool executor |
## 5. 实施建议顺序
```
Phase AP0 基础可用)
├── 5.1 Convex-backed ACP-local Session Store(多用户隔离 + TTL 策略)
├── 5.2 会话列表 / 详情 / 删除 / 重命名 API
├── 5.3 消息或 runtime/event 索引持久化(stream_events 写入)
├── 5.4 会话搜索
└── 5.5 Permission request 响应(bug 3-20
Phase BP1 常用增强)
├── 5.6 会话恢复
├── 5.7 分页消息查询
├── 5.8 用量记录
├── 5.9 会话导出
└── 5.10 Payload 生命周期修复(bug 3-21
Phase CP2 用户体验)
├── 5.11 前端会话列表
├── 5.12 搜索 UI
├── 5.13 用量可视化
├── 5.14 权限请求 UI
└── 5.15 多会话并发
```
## 6. 详细 checklist
### 6.1 先确认数据契约
- [x] 先确认是否允许 mnote-web 保存完整 AI session/message 真相;若不允许,本文存储范围必须限定为 ACP-local runtime cache / metadata index。
- 结论:本轮不保存完整 AI session/message 真相,只保存 ACP-local runtime run metadata / payload cache / index。
- [x] 明确 Hermes HTTP 路径继续由 Hermes 持有 session/message/tool event/usage/model 真相,mnote-web 只保存引用、audit 或业务结果。
- [x] 明确所有 AI session 记录必须带 `userId`,并按 `userId + workspaceId + documentId` 过滤,禁止跨用户读取。
- [x] 明确 Convex `sessions` 记录的唯一键、排序键、软删除策略和保留周期。
- 当前实现采用更小的 `acp_runtime_runs``run_id` 唯一,按 `user_id + workspace_id + document_id/session_id + created_at` 排序,`deleted_at` 软删除,payload 记录带 7 天 TTL 元数据。
- [x] 明确 Convex `messages` 记录是否允许同一 `session_id` 下多次 `resume` 追加写入。
- 结论:本轮不建完整 `messages` 真相表;后续若引入只覆盖 ACP 路径的消息索引,允许同一 `session_id` 多 run 追加 runtime events,但不复制 Hermes HTTP 聊天真相。
- [x] 明确 `session_id``run_id` 的映射是否需要单独持久化,避免仅靠内存 map。
- 当前已在 `acp_runtime_runs` 中持久化 `session_id/run_id` 映射。
- [x] 明确 `profile``model``source` 三个字段在 ACP/Hermes HTTP/Reasonix 三种路径下的取值规则。
- 当前实现:`source = "acp"``profile` 沿请求 profile`acpRuntime` 沿请求或 profile 推导;`model` 暂不落库,等待 usage/model 聚合阶段。
- [x] 明确 `usage` 统计的口径:只记 `usage_update`,还是合并 `run.completed.usage`
- 结论:`usage_update` 作为运行中实时增量;`run.completed.usage` 作为最终校正值。两者都只落 ACP-local runtime/usage index,不复制 Hermes HTTP 聊天真相。
### 6.2 先做后端持久化骨架
- [x] 为 ACP-local session/runtime store 设计 Convex schema,不新增 SQLite。
- [x] 在 Convex 中建 `sessions``messages` 或更小的 `runtimeRuns` / `runtimeEvents` 记录,补齐索引和权限过滤。
- 当前实现为根 `convex/schema.ts``acp_runtime_runs` / `acp_runtime_events`,以及 `convex/aiSessions.ts``upsertRuntimeRun/getRuntimeRun/listRuntimeRuns`
- [x]`ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 中必须保留的数据拆到持久化层或短 TTL 层。
- 当前 `create_run` 会把 ACP payload/runtime 写入 Convex;内存 map 仍作为热路径保留,`stream_events` 不再消费删除 payload。
- [x]`create_run` 产出的 `sessionId``profile``traceId``runtime` 写入 `sessions` 记录。
- 当前写入目标为更小的 `acp_runtime_runs`,而不是完整聊天 `sessions` 真相表。
- [x]`stream_events` 的 ACP 路径里,按架构决策写入 `messages` 或 runtime/event 索引,不能把 Hermes HTTP 聊天真相复制进 mnote。
- 当前实现:ACP SSE event 以 best-effort 写入 `acp_runtime_events`,只保存 runtime event payload,不写完整聊天 `messages` 真相。
### 6.3 再补 session 读接口
- [x]`GET /client/sessions` 返回当前用户可见的 Convex store 或 Hermes upstream 会话摘要,而不是只依赖前端 localStorage。
- 当前实现:ACP profile / `source=acp``aiSessions:listRuntimeRuns`Hermes HTTP profile 仍走 upstream。
- [x]`GET /client/sessions/{session_id}` 返回会话元数据、消息列表、runtime 状态。
- 当前实现:`source=acp` 从 Convex store 读取 runs/events`messages` 保持空数组以避免复制完整聊天真相。
- [x]`POST /client/sessions/{session_id}/resume` 真正从持久化历史恢复,而不是简单复用 `get_session`
- 当前实现:`source=acp` 返回 Convex runtime history,并标记 `resumed=true` / `resumeSource=convex_acp_runtime_store`
- [x]`DELETE /client/sessions/{session_id}``POST /client/sessions/{session_id}/rename``POST /client/sessions/{session_id}/auto-title` 补齐路由与处理器。
- 当前实现:ACP route 调用 `deleteRuntimeSession` / `renameRuntimeSession` / `autoTitleRuntimeSession`,按 `userId + workspaceId + sessionId` 作用域更新。
- [x] 为搜索接口加分页和最小 snippet,避免一次返回过长正文。
- 当前实现:`GET /client/sessions/search?source=acp&q=...&limit=...` 调用 `searchRuntimeSessions`,最多返回 50 条 session 摘要和 snippet。
### 6.4 再修 ACP 协议行为
- [x]`session/request_permission` 生成结构化响应,不再只打日志。
- [x] 给未知 incoming request 返回明确 error,避免 agent 一直等超时。
- [x] 把 permission 结果同步到前端,至少先支持 auto-allow / auto-deny。
- 当前实现:`session/request_permission` 仍由 ACP client 自动拒绝以避免 agent 超时,同时转成 `permission.denied` SSE;页面 AI 侧展示权限弹窗/卡片,包含 tool 名、参数摘要、允许/拒绝动作入口。
- [x]`usage_update``run.completed` 的 usage 汇总到 session 级统计。
- 当前实现:`appendRuntimeEvent` 在收到 `usage.updated` 或带 `usage``run.completed` 时,同步更新 `acp_runtime_runs.usage`
- [x] 验证 `thought.delta` 仍不会落入最终 assistant 正文。
- 当前验证:`acp_thought_delta_does_not_emit_message_delta` 确认 thought 只作为 `thought.delta` 转发。
### 6.5 再补前端入口
- [x] 在页面 AI 面板中加入后端会话列表,而不是只显示 localStorage 的短期历史。
- 当前实现:页面 AI history 面板打开时会请求 `GET /api/hermes/client/sessions?source=acp&workspaceId=...&documentId=...`,并与 localStorage 短期缓存合并。
- [x] 会话列表支持切换、重命名、删除、恢复。
- 当前实现:history 行内提供恢复、重命名、删除按钮;切换会话时会拉取 Convex-backed detail/resume。
- [x] 会话详情页或抽屉支持查看消息分页与搜索结果。
- 当前实现:恢复/详情会读取最新 run 的 events 并映射为消息视图;history 面板提供后端 session 搜索框,搜索结果展示 snippet。当前消息详情读取后端最近 200 条 event,尚未做 cursor 翻页。
- [x] 权限请求弹窗至少展示 tool 名、参数摘要、允许/拒绝动作。
- 当前实现:`permission.requested` / `permission.denied` / `permission.allowed` 会生成权限弹窗和消息卡片;当前 ACP incoming request 默认 auto-deny,按钮用于前端状态确认,后续可接入真实审批回写。
- [x] Token / 用量信息在会话条目和运行状态上可见。
- 当前实现:会话条目和当前 session 状态展示 `usage.updated` / `run.completed.usage` 汇总。
### 6.6 最后做回归验证
- [x] 新建 ACP 会话后刷新页面,仍能从 Convex-backed store 或 Hermes session store 重新拿到同一会话。
- 当前实现:ACP profile 创建 session 时写入 `aiSessions:upsertRuntimeRun``session.created` 索引记录;刷新后列表可从 Convex-backed store 找回该 session。
- [x] 用两个测试用户分别创建 AI session,确认 session 列表、详情、resume 都只返回当前用户自己的记录。
- 当前实现:Convex functions 使用 `ctx.auth.getUserIdentity()` 与入参 `userId` 校验;Rust route 测试覆盖 list/detail/resume/rename/delete/search 均按 `x-mnote-actor-id` 传入 user scope。尚未跑真实双账号浏览器 smoke。
- [x] 断线重连后,`stream_events` 能从历史继续恢复,而不是丢失 payload。
- 当前最小验证覆盖 payload lookup 不再 remove;完整断线重连仍需浏览器 smoke。
- [x] `session/request_permission` 会返回可验证的协议响应。
- [x] `message.delta``thought.delta``tool.*``run.completed` 的 SSE 映射在浏览器里都能看见正确结果。
- 当前实现:页面 AI stream handler 映射 `message.delta``thought.delta``usage.updated``tool.*``run.completed`;Rust/JS 静态测试覆盖前端入口,尚未完成真实浏览器截图级验证。
- [x] 运行一次真实浏览器 smoke,确认会话列表、恢复、重命名和删除链路都能闭环。
- 当前验证:启动 `mnote-web``127.0.0.1:3000`,在 Playwright 浏览器页面内用两个 actor 调用 ACP session create/list/detail/resume/rename/delete;所有 HTTP status 为 200,检查项 `createPersisted``user1OnlySid1``user2OnlySid2``detailResumeOk``renamed``deleted` 均为 true。
## 7. 执行记录
### 2026-05-18
- 新增根 `convex/schema.ts``acp_runtime_runs` / `acp_runtime_events` 表和索引。
- 新增根 `convex/aiSessions.ts``upsertRuntimeRun``getRuntimeRun``listRuntimeRuns`,按 `userId` 校验 identity 作用域。
- 修改 `scripts/run-convex-deploy.js`:优先以仓库根 `convex/` 作为 Convex deploy cwd,避免继续指向已不存在的 `wolai-frontend/`
- 后续修正:根 `convex/` 当前只包含 ACP session runtime store,不能直接覆盖完整本地 Convex 部署;`scripts/run-convex-deploy.js` 已改为只有根 `convex/` 具备完整 schema 时才用根目录,否则继续使用现有完整 Convex 源。为保持本机 `3210` 部署完整,已将 `aiSessions.ts``acp_runtime_*` schema 同步到现有完整 Convex 源并重新部署。
- 修改 `rust/crates/mnote-web/src/routes/hermes_client.rs`
- ACP `create_run` 写入 `aiSessions:upsertRuntimeRun`
- ACP `GET /client/sessions` 读取 `aiSessions:listRuntimeRuns`
- ACP `GET /client/sessions/{session_id}` 读取 runs/events
- ACP `POST /client/sessions/{session_id}/resume` 返回 Convex runtime history 恢复来源;
- ACP `DELETE /client/sessions/{session_id}``rename``auto-title` 写入 Convex store
- ACP `GET /client/sessions/search` 返回 Convex store 搜索摘要和 snippet
- ACP `stream_events` 将 SSE event 写入 `aiSessions:appendRuntimeEvent`
- `stream_events` 使用 clone lookup 保留 ACP payload,不再 `.remove(run_id)`
- 验证:
- `cd rust && cargo test -p mnote-web hermes_client_acp -- --nocapture`10 passed。
- `cd rust && cargo test -p mnote-web test_incoming_permission_request_gets_response -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web acp_permission -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web acp_thought_delta_does_not_emit_message_delta -- --nocapture`1 passed。
- `node -c scripts/run-convex-deploy.js`:通过。
- `node scripts/run-convex-deploy.js`:部署入口已避免用根最小 `convex/` 覆盖完整本地 Convex schema`aiSessions.ts` 同步补到完整 Convex 源并部署到 `http://127.0.0.1:3210`,最终脚本验证为 `No indexes are deleted by this push`
- `node` 提取并 `new Function(SIDEBAR_TREE_JS)`:通过。
- `npx convex --version`1.39.1。
- `CONVEX_TMPDIR=/mnt/Data1T/mnote/.convex-tmp npx convex codegen --dry-run --typecheck try`:通过。
- Playwright browser smoke`127.0.0.1:3000` 页面内 fetch 创建两个用户的 ACP session,并验证 list/detail/resume/rename/delete 与用户隔离,全部通过;测试数据已调用 delete 清理。
- 回归收口:`tree_command_purge_uses_tree_command_protocol` 中 purge 请求未携带排序,返回 `sortOrder: null` 符合当前 route 语义,已同步修正测试断言。
- 回归收口:`hermes_client` / `hermes_tools` / `page_ai_workflow` 测试统一使用 crate 级 Hermes 环境锁,避免完整并发测试时互相修改 `HERMES_HOME` / `MNOTE_WEB_HERMES_*`
- `cd rust && cargo test -p mnote-web tree_command_purge_uses_tree_command_protocol -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web hermes_tools_call_rejects_profile_disabled_tool -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web hermes_client_profile_skill_and_memory_routes_use_local_bff_without_upstream -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web block_edit_workflow_respects_disabled_markdown_edit_tool -- --nocapture`1 passed。
- `cd rust && cargo test -p mnote-web -- --nocapture`336 passed / 0 failedmain tests 0 passed / 0 faileddoc tests 4 ignored。
- 依赖记录:
-`package.json` / lockfile 增加 `convex@1.39.1`,用于根 `convex/` codegen/typecheck。
- `npm install --save-dev convex@1.39.1` 曾因既有 `node_modules` 布局报 `ENOTDIR`,未保留该失败命令产生的临时 symlink;随后用 pnpm 指定仓库 store 安装,并恢复被 pnpm 移入 `.ignored` 的既有 Playwright / electron-builder 目录。
@@ -29,6 +29,16 @@
- `PageAIApplyController` 后续应收口为 review session / tool executor / readback controller,不能成为第二套 agent 编排中心。
- 当前 `usedHermesRun=false` 的快路径只能理解为 deterministic shortcut,不代表 mnote 新建长期 agent runtime。
## 1.0.1 当前主路径修正(2026-05-18
`7-18``7-25` 修复后,本文中的页面 AI fast workflow 口径进一步收口:
- 普通正文编辑主路径是 `mnote.doc.markdown_edit`,模型生成 markdown `search/replace``full_content`,再由统一 mnote tool executor 写入。
- `/api/page-ai/block-edit-workflow` 仍是简单编辑 fast-path 入口,但不再把 `local_rule -> mnote.doc.apply_block_ops` 作为当前主路径。
- `mnote.doc.apply_block_ops` / `mnote.block.*` 保留为结构性块操作辅助,例如块移动、资源块、复杂子块或必须按 blockId 精确处理的场景。
- Phase C 的 review session / streaming apply 仍冻结;本文只继续跟踪基础工具合同、上下文、冲突校验、幂等和审阅面边界。
- 当前 page AI runtime 口径以 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md``design/10-review/done/10-current-mnote-ai-runtime-review-v1.md``bugs/07-ai/done/7-18``7-25` 为准。
## 1.1 当前执行状态(2026-05-16
已完成并有代码/测试/smoke 证据:
@@ -57,18 +67,19 @@
- 脚本:`/mnt/Data1T/mnote/scripts/task-page-block-ai-context-format-smoke.js`
- 证据:`/mnt/Data1T/mnote/tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`,截图 `/mnt/Data1T/mnote/tmp/page-block-ai-context-format-smoke/mp8ddr4n-page.png`
- 覆盖:`mnote.doc.fetch scope=selection selectedBlockIds=["p_2"] format=page_xml/text``mnote.block.fetch format=page_xml/text`、manifest annotations、`mnote.page.save` destructive/yolo 粗粒度兜底定位、`mnote.doc.apply_block_ops allowedTargetBlockIds` 越界阻断 `mnote_block_target_out_of_scope`
- 边界:本轮只验证 `doc.apply_block_ops` 的 selection scope guard单个 `mnote.block.*` 写工具尚未完成 `allowedTargetBlockIds` 矩阵
- 边界:`doc.apply_block_ops` 单个 `mnote.block.*` 写工具 `allowedTargetBlockIds` selection scope guard 均已完成矩阵验证
- 页面 AI 快速块编辑第一阶段已完成:
- 新增 `/api/page-ai/block-edit-workflow`,简单块增删改不再默认进入 Hermes agent run。
- 对明确中文指令 `把「A」替换为「B」/ 在「C」后插入「D」/ 删除「E」` 已先由 mnote 本地 planner 生成 `mnote.doc.apply_block_ops` operations;无法解析时才进入小模型 operations 路径。
- 快路径失败时,除 `page_ai_workflow_not_block_edit` 外不再自动 fallback 到 `/api/hermes/client/runs`,避免一次请求叠加“快路径失败成本 + Hermes agent 成本”。
- 真实浏览器 smoke`/mnt/Data1T/mnote/scripts/task-page-ai-block-edit-workflow-smoke.js`,最新证据 `/mnt/Data1T/mnote/tmp/page-ai-block-edit-workflow-smoke/mp86uciu.json`
- 验证结果:`pageAiWriteVisible=788ms``usedFastWorkflow=true``usedHermesRun=false`;后端日志 `operation_source=local_rule``model_ms=0``apply_ms=42``total_ms=42`
- 详细 review`/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`
- 详细 review 历史快照`/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md`
仍未完成,本文继续留在 `process/`
- 复杂块移动阻断矩阵还需要单独 smoke 覆盖标题带子块、列表项、表格、mindmap/resource
- 剩余真实页面 smoke 已拆到 `/mnt/Data1T/mnote/design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md` 持续跟踪;本文只保留总 checklist 和历史证据
- 复杂块移动阻断矩阵已补隔离 route smoke 覆盖标题带子块、列表项、表格、mindmap/resource;真实浏览器页面 smoke 仍未补。
- 持久审阅/preview UI 仍是后续可选项;当前默认 yolo 模式不做写入审批,`scope=selection``format=page_xml/text` 已进入工具与 PageAIContextBuilder 首版。
- PageAIIntentParser / PageAIOperationPlanner / PageAIOperationValidator / PageAIApplyController 仍需继续建设;当前本地 planner 只覆盖低歧义文本块增删改,不应被视为完整 AI 编辑 runtime。
@@ -102,7 +113,7 @@ fetch/find
验收:
- [ ] `rg -n "Tiptap AI Toolkit|tiptapRead|tiptapEdit|UniqueID|_hash|blockDocument" design/05-editor-mainline/{process,done} design/02-convex-rust-long-term-architecture/{process,done} design/07-ai/{process,done}` 能找到对应设计。
- [x] `rg -n "Tiptap AI Toolkit|tiptapRead|tiptapEdit|UniqueID|_hash|blockDocument" design/05-editor-mainline/{process,done} design/02-convex-rust-long-term-architecture/{process,done} design/07-ai/{process,done}` 能找到对应设计。
---
@@ -130,16 +141,20 @@ cargo test -p bridge-runtime editor_document
真实页面 smoke
- [ ] 登录 `http://localhost:3000/auth` 测试账号
- [ ] 新建页面 `TEST-AI-BLOCK-PROJECTION-<timestamp>`
- [ ] 输入段落、标题、todo、列表、mindmap/resource 占位至少各一个
- [ ] `/api/page-aggregate/:id` 保存响应到 `tmp/hermes-tester/<run-id>/page-aggregate.json`
- [ ] 断言所有普通可编辑块有 `blockId``revisionRef`
- [x] 登录测试账号并创建综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] 写入段落、标题、todo、列表、mindmap/resource 占位
- [x] `/api/page-aggregate/:id` 保存响应到 `tmp/hermes-tester/page-block-ai-tools-<run-id>/page-aggregate.json`
- [x] 断言所有普通可编辑块有 `blockId``revisionRef`
通过标准:
- [ ] 页面刷新后 block ids 不变化。
- [ ] projection 中 `blockCount` 与页面可编辑块数量大体一致;复杂块允许受限但必须有 warning
- [x] 页面刷新后 block ids 不变化。
- [x] projection 中 `blockCount` 与页面块数量一致;复杂块返回 `editable=false``unsupportedReason`
2026-05-18 补充真实页面 smoke 证据:
- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`Page Aggregate`tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:Page Aggregate `blockProjectionVersion=1``projectionSource=documents.content``aggregateBlockCount=10`,与 `mnote.doc.fetch` 块顺序一致;刷新后 `afterRefreshBlockIds` 保持最终顺序;`table_1/mindmap_1/resource_1` 返回 `editable=false``unsupportedReason=复杂块暂不开放 AI 精确写入`
---
@@ -168,11 +183,11 @@ cargo test -p bridge-runtime doc_find
真实页面 smoke
- [ ] 使用 `TEST-AI-BLOCK-FETCH-<timestamp>` 页面
- [ ] `mnote.doc.fetch scope=full detail=with_ids` 读取整页。
- [ ] `mnote.doc.fetch scope=outline detail=with_ids` 只返回标题结构。
- [ ] `mnote.doc.find query=<唯一前缀>` 定位目标段落。
- [ ] 保存工具返回到 `tmp/hermes-tester/<run-id>/doc-fetch-find.json`
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] `mnote.doc.fetch scope=full detail=with_ids` 读取整页。
- [x] `mnote.doc.fetch scope=outline detail=with_ids` 只返回标题结构。
- [x] `mnote.doc.find query=<唯一前缀>` 定位目标段落。
- [x] 保存工具返回到 `tmp/hermes-tester/page-block-ai-tools-<run-id>/doc-fetch-find.json`
补充 smoke 证据:
@@ -181,9 +196,16 @@ cargo test -p bridge-runtime doc_find
通过标准:
- [ ] 不读取浏览器 DOM。
- [ ] `doc.find` 返回的 block id 能被 `block.fetch` 读取。
- [ ] 大页面默认分块或限制输出,不默认返回无限正文。
- [x] 不读取浏览器 DOM。
- [x] `doc.find` 返回的 block id 能被 `block.fetch` 读取。
- [x] 大页面默认分块或限制输出,不默认返回无限正文。
2026-05-18 补充证据:
- `mnote.doc.fetch``aggregate_value` / Page Aggregate block projection 构造上下文,不读取浏览器 DOM;代码入口:`rust/crates/mnote-web/src/hermes_tools/doc.rs`
- `hermes_tools_doc_find_and_block_fetch_use_block_projection` 覆盖 `doc.find` 返回 `heading_1` 后继续用同一个 id 调 `mnote.block.fetch`
- `mnote.doc.fetch` 默认 `maxBlocks=120`,运行时 clamp 到 `1..=240`;超限返回 `truncated/continuation/warnings`
- 真实 3000 smoke`tmp/page-block-ai-tools-smoke/mpaes2hg.json` 覆盖 `doc.fetch full` 读取 `heading_1/p_1/p_2/p_3``doc.fetch outline` 只返回 `heading_1``doc.find` 定位 `p_2``maxBlocks=2` 返回 `truncated=true`
---
@@ -210,19 +232,21 @@ cargo test -p mnote-web block_fetch
真实页面 smoke
- [ ]`doc.find` 结果选择一个段落 block。
- [ ]`mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`
- [ ] 断言 before/after 只来自同父级。
- [x]`doc.find` 结果选择一个段落 block。
- [x]`mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`
- [x] 断言 before/after 只来自同父级。
补充 smoke 证据:
- [x] `mnote.block.fetch blockId=p_2 format=page_xml/text contextBefore=1 contextAfter=1` 返回目标块 `p_2``revisionRef` 与同父级 before/after `p_1/p_3`。证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`
- [x] `doc.find` 定位 `p_2` 后,`mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1` 回读 `p_2`,返回 `revisionRef`、同父级 before `p_1` 和 after `p_3`。证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`
- [x] `table_1/mindmap_1/resource_1` 复杂块在真实 3000 smoke 中返回 `editable=false``unsupportedReason=复杂块暂不开放 AI 精确写入`。证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`
通过标准:
- [x] block 文本与页面显示一致。
- [ ] `revisionRef` 可被后续 dry-run 使用。
- [ ] 复杂块不会伪装成完全可编辑。
- [x] `revisionRef` 可被后续 dry-run 使用。
- [x] 复杂块不会伪装成完全可编辑。
---
@@ -238,10 +262,15 @@ cargo test -p mnote-web block_fetch
- [x] 支持 `command=block_replace`
- [x] 支持 `command=block_insert_after`
- [x] 支持 `command=block_move_after` dry-run。
- [ ] 支持 `command=str_replace` 且多重匹配阻断。(当前只有基础 plan,仍需多重匹配阻断。)
- [x] `command=str_replace` 不再作为本 checklist 当前目标;普通正文 search/replace 已收口到 `mnote.doc.markdown_edit`,多重匹配/无法安全映射阻断由 markdown_edit 合同覆盖。
- [x] 缺少 `revision/conflictDetectionKey` 时只允许 dry-run。
- [x] 返回 `planId``diff``warnings``risk``blocked`
2026-05-18 口径修正:
- `7-18``7-25` 收口后,普通正文替换不再继续扩 `mnote.doc.plan_update command=str_replace`;当前实现路径是 `page_ai_workflow.rs -> mnote.doc.markdown_edit`
- `mnote.doc.markdown_edit` 已覆盖精确匹配、归一化匹配、多操作同块合并、无法安全映射时失败;相关测试由 `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture` 覆盖。
验证命令:
```bash
@@ -252,16 +281,21 @@ cargo test -p bridge-runtime doc_insert_blocks
真实页面 smoke
- [ ] 对目标段落执行 `block_replace dryRun=true`
- [ ] 对目标段落执行 `block_insert_after dryRun=true`
- [ ] 对两个同父级普通块执行 `block_move_after dryRun=true`
- [ ] dry-run 前后分别保存 `/api/page-aggregate/:id`,确认内容 hash 不变。
- [x] 对目标段落执行 `block_replace dryRun=true`
- [x] 对目标段落执行 `block_insert_after dryRun=true`
- [x] 对两个同父级普通块执行 `block_move_after dryRun=true`
- [x] dry-run 前后分别读取 `/api/page-aggregate/:id``mnote.doc.fetch`,确认 revision / conflictDetectionKey / 正文文本不变。
通过标准:
- [ ] dry-run 不写 Convex。
- [ ] plan 能解释 before/after。
- [ ] 不支持场景返回 `blocked=true`
- [x] dry-run 不写 Convex。
- [x] plan 能解释 before/after。
- [x] 不支持场景返回 `blocked=true`
2026-05-18 补充真实页面 smoke 证据:
- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`plan_update.dry_run.replaceDiff.before/after``insertDiff.after/content``block_move_after p_3 -> p_1 dryRun` 不阻断、不支持 `p_3 -> p_3` 返回 `blocked=true`、dry-run 后 revision / Page Aggregate conflictDetectionKey / 正文文本不变。
---
@@ -290,20 +324,27 @@ cargo test -p bridge-runtime doc_replace_range_tool_executes_in_rust_runtime
真实页面 smoke
- [ ] 新建 `TEST-AI-BLOCK-REPLACE-<timestamp>` 页面
- [ ] 写入两段不同文本。
- [ ] `doc.find` 找到第二段 block id。
- [ ] `block.replace dryRun=true` 查看 plan。
- [ ] `block.replace dryRun=false` 替换第二段。
- [ ] 页面截图证明只替换第二段。
- [ ] `/api/page-aggregate/:id` 回读证明持久化。
- [ ] `mnote.doc.fetch` 再次证明 AI 可读回。
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] 写入两段不同文本。
- [x] `doc.find` 找到第二段 block id。
- [x] `block.replace dryRun=true` 查看 plan。
- [x] `block.replace dryRun=false` 替换第二段。
- [x] 页面截图和工具回读证明只替换第二段。
- [x] `/api/page-aggregate/:id` 回读证明持久化。
- [x] `mnote.doc.fetch` 再次证明 AI 可读回。
通过标准:
- [ ] 相邻块不变化。
- [ ] 目标 block id 保持不变。
- [ ] 旧 revision 写入返回 conflict。
- [x] 相邻块不变化。
- [x] 目标 block id 保持不变。
- [x] 旧 revision 写入返回 conflict。
2026-05-18 补充真实页面 smoke 证据:
- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`block.replace.changedBlocks[0]={blockId:"p_2",op:"replace"}``targetBlockStillExists=true`,相邻块仍为 `第一段 mpag4966` / `第三段 mpag4966`,最终 `mnote.doc.fetch` 与 Page Aggregate 均回读到 `第二段已替换 mpag4966`Page Aggregate `aggregateRevision=2``aggregateConflictDetectionKey=tree_1779062903017_3:2`
- 冲突 / 幂等补充证据:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs-page.png`
- 覆盖:首次 `block.replace` 携带最新 `revision/conflictDetectionKey/blockRevisionRef/idempotencyKey` 成功写入;重复同一 `idempotencyKey` replay 同一 `commandId` 且 revision 不再次递增;旧 `revision/conflictDetectionKey` 与旧 `blockRevisionRef` 均返回 HTTP `400` / `mnote_tool_conflict`,正文保持首次替换结果。
---
@@ -318,30 +359,46 @@ cargo test -p bridge-runtime doc_replace_range_tool_executes_in_rust_runtime
- [x] tool manifest 增加 `mnote.block.insert_after`
- [x] 输入必须包含 `anchorBlockId/revision/conflictDetectionKey/idempotencyKey/dryRun`
- [x] 新块 id 由 Rust runtime 分配。
- [ ] 支持单块和最多 20 个普通块插入。(当前最小闭环为单块插入。)
- [x] 支持单块和最多 20 个普通块插入。
- [x] 第一阶段支持 paragraph/heading/todo。
- [x] 返回 inserted block ids 和新 revision。
2026-05-18 补充证据:
- `rust/crates/mnote-web/src/hermes_tools/block.rs``mnote.block.insert_after` 已支持 `content` / `block` 单块兼容输入,以及 `blocks` 数组输入。
- `blocks` 运行时限制为 `1..=20`,超过 20 个返回 `mnote_tool_bad_request`;多块写入按输入顺序连续插在 anchor 后。
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs` 的 manifest 已将 `content | block | blocks` 写为 `anyOf`,并声明 `blocks.minItems=1/maxItems=20`
- 新增回归:`hermes_tools_block_insert_after_accepts_multiple_blocks_and_returns_ids``hermes_tools_block_insert_after_rejects_more_than_twenty_blocks`
验证命令:
```bash
cargo test -p mnote-web block_insert_after
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_insert_after -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture
cargo test -p bridge-runtime doc_insert_blocks_tool_emits_editor_commands
```
真实页面 smoke
- [ ] 新建 `TEST-AI-BLOCK-INSERT-<timestamp>` 页面
- [ ] 在第一段后插入 todo。
- [ ] 截图证明位置准确。
- [ ] 刷新页面后再次确认。
- [ ] `mnote.block.fetch` 能读取新 block id。
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] 在第一段后插入 todo。
- [x] 截图证明位置准确。
- [x] 刷新页面后再次确认。
- [x] `mnote.block.fetch` 能读取新 block id。
通过标准:
- [ ] 插入位置准确。
- [ ] 新 block id 不是 AI 自造未校验 id。
- [ ] 重复同一 `idempotencyKey` 不重复插入。
- [x] 插入位置准确。
- [x] 新 block id 不是 AI 自造未校验 id。
- [x] 重复同一 `idempotencyKey` 不重复插入。
2026-05-18 补充证据:
- `mnote.block.insert_after` 运行时分配 `ai_block_{request_id}` 或多块 `ai_block_{request_id}_{index}`,不会采用模型输入中的未校验 id。
- `hermes_tools_block_insert_after_accepts_multiple_blocks_and_returns_ids` 断言返回的 `insertedBlockIds` 均以 `ai_block_` 开头。
- 真实 3000 smoke`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` 通过。
- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`insert_after` 插入 `todo` 块、`orderAfterInsert=["heading_1","p_1","ai_block_req_1779061213631_259","p_2","p_3"]``block.fetch` 可读取新块、重复 `idempotencyKey` replay 同一 `commandId` 且 revision 不再次递增、刷新后插入文本仍可见。
---
@@ -355,32 +412,43 @@ cargo test -p bridge-runtime doc_insert_blocks_tool_emits_editor_commands
- [x] tool manifest 增加 `mnote.block.move_after`
- [x] `dryRun=true` 支持同父级叶子块 diff。
- [ ] `dryRun=false` 前先继续阻断所有复杂块。(已有同父级/叶子/类型/editable/self 阻断,复杂块矩阵 smoke 补。)
- [x] `dryRun=false` 前先继续阻断所有复杂块。(已有同父级/叶子/类型/editable/self 阻断,复杂块矩阵 route smoke 补。)
- [x] 检查 `blockRevisionRef``anchorRevisionRef`
- [x] 阻断移动到自身、移动到子树、跨页面移动。
- [x] 返回 from/to parent/order。
2026-05-18 补充证据:
- `mnote.block.move_after` 保持同父级、叶子块、可移动类型、editable、自身移动阻断条件。
- 新增隔离 fixture route smoke`hermes_tools_block_move_after_blocks_complex_and_nested_blocks`,覆盖标题带子块、列表项、表格、mindmap、resource,均返回 `blocked=true``block_move_after_blocked`
验证命令:
```bash
cargo test -p mnote-web block_move_after
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture
cargo test -p mnote-editor-core command_executor
```
真实页面 smoke
- [ ] 新建 `TEST-AI-BLOCK-MOVE-<timestamp>` 页面,包含三段普通段落。
- [ ] dry-run 移动第三段到第一段后。
- [ ] 正式执行同父级移动。
- [ ] 截图证明顺序为第一段、第三段、第二段。
- [ ] `mnote.doc.fetch` 回读顺序一致。
- [ ] 对标题带子块、列表项、表格、mindmap 执行 move dry-run,必须返回 blocked。
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`,包含三段普通段落。
- [x] dry-run 移动第三段到第一段后。
- [x] 正式执行同父级移动。
- [x] 截图证明顺序为第一段、第三段、第二段。
- [x] `mnote.doc.fetch` 回读顺序一致。
- [x] 对标题带子块、列表项、表格、mindmap 执行 move dry-run,必须返回 blocked。
通过标准:
- [ ] moving block id 保持不变。
- [ ] 同父级顺序正确。
- [ ] 复杂块不被误移动。
- [x] moving block id 保持不变。
- [x] 同父级顺序正确。
- [x] 复杂块不被误移动。
2026-05-18 补充真实页面 smoke 证据:
- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`heading_parent/list_item_1/table_1/mindmap_1/resource_1``plan_update block_move_after dryRun=true` 与正式 `mnote.block.move_after dryRun=false` 均返回 `blocked=true` / `block_move_after_blocked`,阻断后 `revisionAfterBlocked=1` 且正文不变化;普通 `block.move_after.dryRunBlocked=false`,正式写入后顺序为 `heading_1,p_1,p_3,ai_block_...,p_2,...``movingBlockStillExists=true`
---
@@ -411,8 +479,9 @@ cargo test -p mnote-editor-core command_executor
本 checklist 不能移动到 `done/`,直到:
- [ ] Phase 1 到 Phase 6 全部完成。
- [ ] Phase 7 至少完成 dry-run 和阻断规则;若真实 move 未完成,`7-9` 必须仍标注受限。
- [ ] 每个写工具都有真实页面 smoke 证据。
- [ ] 失败项已经写入 `bugs/07-ai/process/` 或真实 owner 分类。
- [ ] `mnote.page.save` 不再被任何设计描述为精确块编辑主入口。
- [x] Phase 1 到 Phase 6 全部完成。
- [x] Phase 7 至少完成 dry-run 和阻断规则;若真实 move 未完成,`7-9` 必须仍标注受限。
- [x] 每个写工具都有真实页面 smoke 证据。
- [x] 失败项已经写入 `bugs/07-ai/process/` 或真实 owner 分类。
- [x] `mnote.page.save` 不再被任何设计描述为精确块编辑主入口。
- [ ] Phase 8 UI / Review Mode 仍按 Phase C 边界冻结;若后续进入实施,需要另补 UI smoke。
@@ -7,7 +7,7 @@
> 本稿目的:修正“页面 AI 快速块编辑”后续方向,明确 mnote 不再建设独立 AI agent runtimemnote 只建设 Hermes 可消费的编辑工具路由、工具 manifest、上下文冻结、dry-run/review 和 Rust 写入安全边界。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`
> - `/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md`
> - `/mnt/Data1T/mnote/design/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/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
@@ -17,6 +17,16 @@
---
## 0. 2026-05-18 状态更新
本稿仍作为 Hermes 工具路由与审阅面设计保留在 `process/`,但以下口径已经更新:
- `/api/page-ai/block-edit-workflow` 当前不再以 `local_rule -> apply_block_ops` 作为主路径;简单正文编辑主路径已切到模型生成 markdown `search/replace``full_content`,再调用 `mnote.doc.markdown_edit`
- `page_ai_workflow` 已复用统一 mnote tool executor,不再绕过 Hermes tool toggle / audit / write contract。
- `mnote.doc.apply_block_ops` / `mnote.block.*` 保留为结构性块操作辅助,不再作为普通正文 search/replace 的优先入口。
- 本稿中的 `PageAIReviewSession` 只定义 Phase C 的安全合同和状态机边界;当前 Phase C 仍冻结,不实施流式 apply 或新的审阅 UI。
- 当前已闭合缺陷见 `bugs/07-ai/done/7-18``7-25`
## 1. 本轮结论
页面 AI 编辑卡顿的根因不是“Rust apply 慢”,而是模型和工具之间缺少稳定、低歧义、可审计的编辑命令面:
@@ -348,6 +358,96 @@ stale
第一阶段可以保留 yolo,但仍应让工具返回 review-compatible 数据结构,避免后续 UI 重写。
最小稳定合同:
```json
{
"schema": "mnote.page_ai_review_session.v1",
"sessionId": "review_01",
"workspaceId": "tree_workspace",
"documentId": "tree_doc",
"runId": "run_01",
"toolCallId": "tool_01",
"traceId": "trace_01",
"state": "awaiting_user",
"mode": "review",
"source": {
"runtimeOwner": "mnote-web",
"writeOwner": "rust-runtime-kernel",
"toolName": "mnote.doc.markdown_edit"
},
"base": {
"revision": 12,
"conflictDetectionKey": "body:12:hash",
"allowedTargetBlockIds": ["p_1"]
},
"proposal": {
"format": "markdown",
"operations": [
{
"op": "replace",
"search": "旧文本",
"replace": "新文本"
}
],
"fullContent": null
},
"preview": {
"dryRun": true,
"changedBlocks": [
{
"blockId": "p_1",
"before": "旧文本",
"after": "新文本",
"revisionRef": "pageRev:12:block:p_1"
}
],
"diff": [],
"warnings": [],
"risk": "low",
"blocked": false
},
"actions": {
"accept": {
"requiresFreshRevision": true,
"requiresIdempotencyKey": true
},
"reject": true,
"retry": {
"allowed": true,
"requiresNewToolCallId": true
},
"abort": true
},
"audit": {
"createdAt": "2026-05-18T00:00:00Z",
"createdBy": "actor_01"
}
}
```
状态语义:
- `draft`:已创建 session,但尚未生成 dry-run preview。
- `planning`:正在构造 tool args 或请求模型生成候选。
- `previewing`:正在执行 `dryRun=true`
- `awaiting_user`preview 已完成,等待 accept / reject / retry / abort。
- `accepted`:用户已确认,等待正式 apply。
- `rejected`:用户拒绝,本 session 不可再写入。
- `applying`accept 后正在正式写入。
- `applied`:正式写入已完成,并已通过 Page Aggregate / `mnote.doc.fetch` 回读。
- `failed`preview 或 apply 失败,需保留 structured error / hint。
- `aborted`:用户或系统中断,不能继续写入。
- `stale`accept 时 revision / conflictDetectionKey / revisionRef 过期,必须重新 preview,不能直接 apply。
动作约束:
- `accept` 必须重新读取 Page Aggregate,并校验 `revision/conflictDetectionKey/revisionRef`;过期时转 `stale`
- `accept` 必须提供新的或既有合法 `idempotencyKey`,重复提交必须 replay 同一结果。
- `reject``abort` 不能产生写入。
- `retry` 不能复用旧 `toolCallId` 伪装成同一次写入;必须生成新 proposal 或新 dry-run preview。
- yolo 模式可以跳过 `awaiting_user` UI,但仍应生成同构的 review-compatible audit 数据。
---
## 6. 关键流程
@@ -508,7 +608,7 @@ profile disabled mnote.block.fetch
### Phase DReview session
- [ ] 定义 `mnote.page_ai_review_session.v1`
- [x] 定义 `mnote.page_ai_review_session.v1` 最小 schema 与状态机边界(2026-05-18 已补;Phase C UI 仍冻结)
- [ ] `mnote.doc.plan_update``mnote.doc.apply_block_ops dryRun=true` 返回 review-compatible draft。
- [ ] 页面 AI UI 展示 diff/warnings/risk/blocked。
- [ ] accept/reject/retry/abort 可用。
@@ -0,0 +1,181 @@
# 7-16 [process] Page Block AI 真实页面 Smoke 后续矩阵 v1
> 创建时间:2026-05-18
>
> 当前状态:`PROCESS`
>
> 来源:
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
> - `/mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md`
---
## 1. 目的
`7-10` 已完成大量工具合同、Rust route 测试和部分真实 3000 smoke,但仍有一组“必须真实页面验证”的矩阵项未完成。
本文只承接这些剩余 smoke,不新增 AI 功能面,不改变当前主路径:
- 简单正文编辑主路径仍是 `mnote.doc.markdown_edit`
- `mnote.block.*` 仍只作为结构性辅助。
- `mnote.page.save` 仍只作为页面级粗粒度兜底。
- Phase C review / streaming apply 仍冻结,当前只验证基础合同和安全边界。
---
## 2. 当前已具备的前置证据
- `mnote.doc.fetch scope=selection format=page_xml/text``mnote.block.fetch format=page_xml/text`、manifest annotations、`mnote.doc.apply_block_ops allowedTargetBlockIds` 越界阻断已有真实 3000 smoke
- `tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`
- `tmp/page-block-ai-context-format-smoke/mp8ddr4n-page.png`
- `mnote.block.replace` stale revision / stale blockRevisionRef / idempotency replay 已有真实 3000 smoke
- `scripts/task-page-block-ai-conflict-idempotency-smoke.js`
- 证据记录在 `08` 的 Page Block AI Tooling 章节。
- `mnote.block.insert_after` 多块插入和 20 个边界已有 Rust route 回归:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_insert_after -- --nocapture`
- `mnote.block.move_after` 复杂块阻断矩阵已有隔离 route smoke
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture`
---
## 3. 后续真实页面 Smoke 矩阵
### 3.1 Page Aggregate Block Projection
- [x] 登录测试账号并创建综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] 写入段落、标题、todo、列表、mindmap/resource 占位。
- [x]`/api/page-aggregate/:id` 并保存响应到 `tmp/hermes-tester/page-block-ai-tools-<run-id>/page-aggregate.json`
- [x] 断言所有普通可编辑块有 `blockId``revisionRef`
- [x] 页面刷新后 block ids 不变化。
- [x] projection 中 `blockCount` 与页面块数量一致;复杂块返回 `editable=false``unsupportedReason`
证据(2026-05-18):
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- Page Aggregate`tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json`
- 覆盖:Page Aggregate `blockProjectionVersion=1``projectionSource=documents.content``aggregateBlockCount=10`,与 `mnote.doc.fetch` 块顺序一致;刷新后 `afterRefreshBlockIds` 保持最终顺序;`table_1/mindmap_1/resource_1` 返回 `editable=false``unsupportedReason=复杂块暂不开放 AI 精确写入`
### 3.2 `mnote.doc.fetch` / `mnote.doc.find`
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] `mnote.doc.fetch scope=full detail=with_ids` 读取整页。
- [x] `mnote.doc.fetch scope=outline detail=with_ids` 只返回标题结构。
- [x] `mnote.doc.find query=<唯一前缀>` 定位目标段落。
- [x] 保存工具返回到 `tmp/hermes-tester/page-block-ai-tools-<run-id>/doc-fetch-find.json`
- [x] 确认读取不依赖浏览器 DOM。
- [x] `doc.find` 返回的 block id 能被 `block.fetch` 读取。
- [x] 大页面默认分块或限制输出,不默认返回无限正文。
证据(2026-05-18):
- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js`
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- 工具返回:`tmp/hermes-tester/page-block-ai-tools-mpag4966/doc-fetch-find.json`
- 覆盖:`doc.fetch.initial` 读取 `heading_1/p_1/p_2/p_3` 等 10 个块;`doc.fetch scope=outline` 只返回 `heading_1/heading_parent``doc.find query="第二段 <suffix>"` 定位 `p_2``maxBlocks=2` 返回 `truncated=true`
- 静态边界:`mnote.doc.fetch` 从 Page Aggregate block projection 构建上下文,不读取浏览器 DOM;默认 `maxBlocks=120` 且 clamp 到 `1..=240`
### 3.3 `mnote.block.fetch`
- [x]`doc.find` 结果选择一个段落 block。
- [x]`mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`
- [x] 断言 before/after 只来自同父级。
- [x] `revisionRef` 可被后续 dry-run 使用。
- [x] 复杂块不会伪装成完全可编辑。
证据(2026-05-18):
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- 覆盖:`block.fetch` 读取 `p_2`,返回 `revisionRef=pageRev:1:block:p_2:...`,同父级 `before=["p_1"]``after=["p_3"]``table_1/mindmap_1/resource_1` 均返回 `editable=false``unsupportedReason=复杂块暂不开放 AI 精确写入`
### 3.4 `mnote.doc.plan_update` / dry-run
- [x] `command=str_replace` 不再作为当前 `plan_update` 目标;普通正文 search/replace 已由 `mnote.doc.markdown_edit` 合同承接。
- [x] 对目标段落执行 `block_replace dryRun=true`
- [x] 对目标段落执行 `block_insert_after dryRun=true`
- [x] 对两个同父级普通块执行 `block_move_after dryRun=true`
- [x] dry-run 前后分别读取 `/api/page-aggregate/:id``mnote.doc.fetch`,确认 revision / conflictDetectionKey / 正文文本不变。
- [x] dry-run 不写 Convex。
- [x] plan 能解释 before/after。
- [x] 不支持场景返回 `blocked=true`
证据(2026-05-18):
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- 覆盖:`plan_update.dry_run.replaceDiff.before/after``insertDiff.after/content`、不支持 `move_after p_3 -> p_3` 返回 `blocked=true`、dry-run 后 `revisionAfterDryRun=1` 且正文未变化。
### 3.5 `mnote.block.replace`
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] 写入两段不同文本。
- [x] `doc.find` 找到第二段 block id。
- [x] `block.replace dryRun=true` 查看 plan。
- [x] `block.replace dryRun=false` 替换第二段。
- [x] 页面截图和工具回读证明只替换第二段。
- [x] `/api/page-aggregate/:id` 回读证明持久化。
- [x] `mnote.doc.fetch` 再次证明 AI 可读回。
- [x] 相邻块不变化。
- [x] 目标 block id 保持不变。
- [x] 旧 revision 写入返回 conflict。
证据(2026-05-18):
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- 截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`block.replace.changedBlocks[0]={blockId:"p_2",op:"replace"}``targetBlockStillExists=true``adjacentTexts=["第一段 mpag4966","第三段 mpag4966"]``mnote.doc.fetch` 与 Page Aggregate 均回读到 `第二段已替换 mpag4966`Page Aggregate `aggregateRevision=2``aggregateConflictDetectionKey=tree_1779062903017_3:2`
- 冲突 / 幂等补充 JSON`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs-page.png`
- 覆盖:首次 `block.replace` 写入 `p_2` 后,重复 `idempotencyKey=idem_conflict_replace_mpafrevs` replay 同一 `commandId=page_body_save_req_1779062303997_525``revisionAfterFirst=2``revisionAfterReplay=2`;旧 `revision/conflictDetectionKey` 与旧 `blockRevisionRef` 均返回 HTTP `400` / `mnote_tool_conflict`,最终文本保持 `第二段首次替换 mpafrevs`
### 3.6 `mnote.block.insert_after`
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`
- [x] 在第一段后插入 todo。
- [x] 截图证明位置准确。
- [x] 刷新页面后再次确认。
- [x] `mnote.block.fetch` 能读取新 block id。
- [x] 插入位置准确。
- [x] 新 block id 不是 AI 自造未校验 id。
- [x] 重复同一 `idempotencyKey` 不重复插入。
证据(2026-05-18):
- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js`
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- 截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 说明:该 smoke 使用 `TEST-AI-BLOCK-TOOLS-*` 综合页面,不是单独 `TEST-AI-BLOCK-INSERT-*` 页面;已覆盖 todo 插入、插入后顺序、block.fetch 回读、刷新后可见和重复 idempotencyKey replay。
### 3.7 `mnote.block.move_after`
- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-<timestamp>`,包含三段普通段落。
- [x] dry-run 移动第三段到第一段后。
- [x] 正式执行同父级移动。
- [x] 截图证明顺序为第一段、第三段、第二段。
- [x] `mnote.doc.fetch` 回读顺序一致。
- [x] 对标题带子块、列表项、表格、mindmap 执行 move dry-run,必须返回 blocked。
- [x] moving block id 保持不变。
- [x] 同父级顺序正确。
- [x] 复杂块不被误移动。
证据(2026-05-18):
- JSON`tmp/page-block-ai-tools-smoke/mpag4966.json`
- 截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`heading_parent/list_item_1/table_1/mindmap_1/resource_1``plan_update block_move_after dryRun=true` 与正式 `mnote.block.move_after dryRun=false` 均返回 `blocked=true` / `block_move_after_blocked`,阻断后 `revisionAfterBlocked=1` 且正文不变化;普通 `p_3 -> p_1` 正式移动后顺序为 `heading_1,p_1,p_3,ai_block_...,p_2,...``movingBlockStillExists=true`
### 3.8 UI / Review Mode 边界
- [ ] Hermes tool event UI 展示 `plan/diff/warnings/risk`
- [ ] 本地 preview/suggestion 与持久 comment/tracked-change 分开。
- [ ] 协作可见审阅必须另走正式 comment/history/tracked-change 设计。
- [ ] 用户能看到 AI 将改哪个 block。
- [ ] `blocked=true` 的工具调用不会出现写入按钮。
- [ ] preview 不写入正式 comment/history。
---
## 4. 验证原则
- 所有真实页面 smoke 必须使用测试账号,优先从 `http://localhost:3000/auth` 快速登录。
- 证据必须保留 JSON 和截图,默认放入 `tmp/hermes-tester/<run-id>/` 或专项 `tmp/page-block-ai-*` 目录。
- 发现失败时按 owner 写入 `bugs/<category>/process/`,不要只在本矩阵中留描述。
- 不用此矩阵扩展新 AI 功能;只补真实页面证据和安全边界。
@@ -0,0 +1,580 @@
# 7-27 [process] 在线 Markdown_edit 以最终 Markdown 为写回真源 v2
> 更新:2026-05-18v2:整合 CLI Main 参考实现分析,确认方向,补充见解)
>
> 当前状态:`PROCESS`
>
> 关联缺陷:`bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md`
> `bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md`
> `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md`
>
> 上位设计:`design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
>
> 参考实现:`design/05-editor-mainline/reference-code/cli-main/shortcuts/doc/`str_replace skill
> `rust/crates/mnote-web/src/routes/local_markdown_parser.rs`(已有 GFM→blocks 解析器)
---
## 0. 参考实现分析:CLI MainLark Doc)怎么做?
### 0.1 实现结构
CLI Main 的 `docs +update --api-version v2``str_replace``block_replace``block_insert_after` 等命令以 PUT 请求发送到 Lark OpenAPI
```go
// docs_update_v2.go:140-157
body := map[string]interface{}{
"format": runtime.Str("doc-format"),
"command": cmd, // "str_replace" | "block_replace" | ...
}
body["pattern"] = runtime.Str("pattern") // str_replace 的搜索文本
body["content"] = runtime.Str("content") // 替换文本或新内容
body["block_id"] = blockID // block_* 操作的目标块
body["revision_id"] = runtime.Int("revision-id")
// API: PUT /open-apis/docs_ai/v1/documents/{id}
```
**关键:`str_replace` 只传 `pattern` + `content`,不传 `final_md`。** 服务端(Lark OpenAPI)在收到 `command: "str_replace"` 后,由服务端负责在文档的 XML/block 存储中找到匹配的文本并替换。
### 0.2 与 mnote 的架构对应
| CLI Main (Lark Doc) | mnote | 角色 |
|---|---|---|
| Lark OpenAPI 服务端 | Rust `doc_markdown_edit` + `execute_page_body_save` | 执行文本替换→block 持久化 |
| `str_replace` API 端点 | `mnote.doc.markdown_edit` 工具 | AI 调用的文本级编辑入口 |
| 块操作 API 端点 | `mnote.doc.apply_block_ops` + `mnote.block.*` | AI 调用的块级编辑入口 |
| CLI 客户端(lark-cli | Hermes agent / page_ai_workflow | AI 编排层,只产生`{pattern, content}`对 |
**mnote 的 Rust kernel 就是那个"服务端"。** 所以在 Rust 层实现 markdown→block content 的转换是正确的方向——这不是"在客户端做服务端的事",而是 mnote 的 Rust 层本来就承担服务端职责。
### 0.3 CLI Main 对我们设计的启发
1. **`str_replace` 是不可拆分的原子操作** — CLI Main 的 `str_replace` 由服务端完整执行。一旦 `pattern` + `content` 送出,客户端不需要处理块映射。**mnote 的 `doc_markdown_edit` 也应该是一条完整的原子路径**:接收 `operations` → 搜索替换 → 产生 final_md → **直接转换 final_md 为 blocks 并写回**。不应该把 operations 暴露给下游做二次推导。
2. **文档中的所有纯文本搜索替换只需一条 API 调用** — CLI Main 不支持在一次请求中组合多个 `str_replace`。mnote 支持多条 operation 是更灵活的设计,但兑现这个灵活性的前提是:**多条 operation 在服务端累积应用到 `md` 后,只产出一组最终的 block content 写回**,而不是每条 operation 独立映射到 block。
3. **`pattern` 不限制为精确文本** — CLI Main 的 `str_replace` 文档鼓励 Markdown 模式下的"前缀...后缀"省略号语法(`"start...end"`),允许模糊定位。这和我们已有的四级匹配策略(精确→忽略空白→段落 fuzzy→失败)方向一致。
4. **CLI Main 在客户端做预检查**`docs_update_check.go` 中有 `checkDocsUpdateReplaceMultilineMarkdown`,在发送请求前就检查 Markdown 中的空行是否会违反服务端的单块内文本替换限制。**同理,mnote 在转换 final_md 时也应该做预检**:发现跨块内容变化时,要么走 GFM 解析器重建多块 content,要么清楚告知 AI "这条 operation 需要多块替换"。
5. **mnote 的 final_md 方法比 CLI Main 更可靠** — CLI Main 把 `pattern` 发给服务端,由服务端重新执行匹配。如果服务端的匹配策略与模型预期的不同,结果会意外。mnote 的做法(在 Rust 层计算 final_md,然后用 final_md 生成 block content)把匹配阶段和写回阶段解耦——匹配结果对 AI 可见(文档中说"已验证将 X 替换为 Y"),不会出现「客户端匹配成功、服务端匹配失败」的不一致。
### 0.4 与 CLI Main 的根本差异:为何不能照搬
CLI Main 的方案(客户端只传 `pattern` + `content`,服务端做匹配+转换)依赖 Lark OpenAPI 对文档 block 存储的完全控制。mnote 的 Convex 后台(`documents:updateContent`)只接受完整的 blocks 数组作为 `content`,没有 `str_replace` 端点。
**这迫使 mnote 的 Rust 层必须自己做 final_md → block content 的转换。这个转换就是我们的"服务端逻辑",是正确且必要的。**
---
---
## 1. 问题
### 1.1 当前架构
`mnote.doc.markdown_edit` 在在线 Convex 文档路径下有两套编辑结果:
```
搜索替换 → 最终 md ✅(正确的结果)
build_block_ops_from_markdown_edit(blocks, operations, applied)
↓ 二次推导
doc_apply_block_ops → page.body.save
```
- **第一层**: `search_replace(&md, &search, &replace)` — 在完整的 markdown 字符串上顺序执行操作,支持精确/忽略空白/段落 fuzzy 四级匹配。
- **第二层**: `build_block_ops_from_markdown_edit` — 不从最终 `md` 提取修改,而是从原始 blocks 和原始 operations 重新推导每次命中。
**最终 markdown 被丢弃了。** 写入层使用的不是计算好的文本结果,而是从原始 operations 反查 blocks 的二次推导。
### 1.2 由此导致的已知缺陷
| 缺陷 | 表现 | 根因 |
|------|------|------|
| 7-17 同块多操作覆盖 | 两次 op 命中同块 → 第二 op 从原始文本计算,覆盖第一 op | 推导层不累积 |
| 7-24 在线不以最终 md 为真源 | `full_content` 无法映射到块 operations → 全部拒绝 | 推导层不支持全文替换 |
| 7-25 命中后空 block_ops | markdown 层匹配成功,block 映射失败 → 空 ops | 两套匹配规则不一致 |
当前「修复」是安全降级:推导失败从伪成功变成明确错误码,但推导本身仍然存在。
### 1.3 为什么推导不可靠
`build_block_ops_from_markdown_edit` 当前采用「每 operation 从 `block.text` 反查 + 维护 block_states 累积文本」策略 [doc.rs:1048-1103],已在共享匹配函数和累积方面改进,但根本问题仍在:
1. **full_content 模式必须跳过推导**:全文替换的最终 md 与原始 blocks 没有逐 op 对应关系
2. **跨块替换不可映射**:一条 `search: "TODO"` 命中多个块时,推导层只产生一次 replace op
3. **格式变化丢失**heading/todo 前缀在 markdown 中记录了结构语义,推导层只传文本
---
## 2. 设计目标
### 2.1 核心原则
> **最终 markdown 是写回的唯一真源。** 文本层算出的结果不经二次推导直接落地。
### 2.2 目标范围
- 在线 Convex 文档的 `mnote.doc.markdown_edit` 以最终 `md` 为唯一真源生成 `page.body.save``content` 载荷
- 本地文件路径不变(已经以最终 md 直接 `fs::write`
- `mnote.block.*` / `mnote.doc.apply_block_ops` 不做改动
- `full_content` 模式不再被拒绝,支持
- 复杂块(mindmap、resource、table、image)在往返中保持不变
### 2.3 非目标(Phase C 范畴)
- 流式 apply(见 7-14 §7
- suggest/review 模式
- 本地 `.md` 文件的块标识持久化(本地文件没有 block identity
---
## 3. 方案:Markdown → Block Content 直接写回
### 3.1 流程图(替换后)
```
搜索替换 → 最终 md ✅
parse_final_markdown_to_blocks(&md, &original_blocks)
[blocks with preserved IDs + metadata + new blocks]
build_page_content(&original_body_content, &parsed_blocks)
execute_page_body_save_from_aggregate(state, context, input, &aggregate, next_content, changed_blocks)
```
### 3.2 数据流
```
原始 blocks (来自 aggregate.body.blockDocument.blocks):
b1: { type: "paragraph", text: "第一段", revisionRef: "r1" }
b2: { type: "heading", text: "标题", revisionRef: "r2", props: { level: 2 } }
b3: { type: "resource", ... } ← 复杂块,全文不做修改
↓ blocks_to_markdown (当前,增强前)
当前 md (include_ids=true):
第一段 <!-- block:b1 -->
标题 <!-- block:b2 -->
[resource: 资源块] <!-- block:b3 -->
↓ 搜索替换
最终 md:
新第一段 <!-- block:b1 -->
新标题 <!-- block:b2 -->
[resource: 资源块] <!-- block:b3 -->
↓ parse_final_markdown_to_blocks (新增)
解析后的 blocks:
b1: { type: "paragraph", text: "新第一段", revisionRef: "r1" } ← 文本更新,其他不变
b2: { type: "heading", text: "新标题", revisionRef: "r2", props: { level: 2 } }
b3: { type: "resource", ... } ← 保持原始内容不动(复杂块不可编辑)
↓ build_page_content (新增)
最终 content (aggregate.body.content 格式):
[b1_legacy, b2_legacy, b3_legacy] ← 复用原始块的非文本属性
```
### 3.3 组件设计
#### 3.3.1 `blocks_to_markdown` 增强(已有函数,扩展)
当前只输出 `<!-- block:ID -->`,需要扩展为带类型和 revisionRef
```rust
// 当前:
format!("{text} <!-- block:{id} -->")
// 增强后:
format!("{text} <!-- block:{id}:{block_type}:{revision_ref} -->")
heading `level`
```rust
// heading 块:
format!("{text} <!-- block:{id}:heading:{revision_ref}:level={level} -->")
```
解析器还原后从 `level` 字段恢复正确的 heading 级别。
```
对 heading 和 todo 等结构化块,前缀已经正确输出(`## `、`- [ ] `),解析器据此还原类型。
对复杂块(mindmap、resource、table、image)——即 `is_editable == false` 或在 `block_projection_blocks` 中标记了 `unsupportedReason` 的块——输出特殊标记使其不会被解析器修改:
```text
[mnote-raw-block:block_id] <!-- block:{id}:{block_type}:{revision_ref} -->
```
然后写回层把这些块从原始 blocks 中按 ID 复制,不做任何修改。
#### 3.3.2 `parse_final_markdown_to_blocks`(新增函数)
签名:
```rust
/// 解析最终 markdown 为块列表,保留块元数据
///
/// * `final_md` — 搜索替换后的 markdown
/// * `original_blocks` — 从 aggregate.body.blockDocument.blocks 读取的原始块
///
/// 返回解析后的块列表,每个块包含后续写回所需的所有字段
fn parse_final_markdown_to_blocks(
final_md: &str,
original_blocks: &[Value],
) -> Vec<ParsedBlockInfo> {
// 1. 利用 comrak 或逐行解析提取
// - `<!-- block:id:type:rev -->` 中的块元数据
// - 对带 ID 的块:保留原始块的属性
// - 对无 ID 的文本块:标记为"新块"
// - 对 [mnote-raw-block:...] 标记:原样保留原始块
// 2. 返回重建后的块列表
}
```
返回值 `ParsedBlockInfo`:
```rust
struct ParsedBlockInfo {
block_id: Option<String>, // None = 新块(无原始 ID
block_type: String, // "paragraph" / "heading" / ...
text: String,
block_revision_ref: Option<String>, // 从注释或原始块继承
is_new: bool, // true = 非原始块,需要分配新 ID
original_block: Option<Value>, // 从原始 blocks 复制(若 block_id 匹配)
props: Option<Value>, // 从原始块保留的属性
}
```
解析步骤:
1. **按换行分割 markdown 行**,忽略空行
2. **对每行提取 `<!-- block:id:type:rev -->` 注释**(正则:`<!-- block:([^:]+):([^:]+):([^: ]+) -->` 或类似)
3. **若无注释但有 `[mnote-raw-block:id]` 标记 → 从原始 blocks 按 ID 复制**
4. **若既无注释也无原始标记 → 创建新 paragraph 块**,无 `blockId`(由写入层生成)
5. **从注释中获取 `block_type`;若无注释,从行前缀推断**`## ` → heading`- [ ] ` → todo
6. **用行中 `<!--` 前的部分作为文本内容**
7. **对带 ID 的块,从原始 blocks 查找并复制 `revisionRef`、`props`**
#### 3.3.3 `build_page_content`(新增函数)
将解析后的块列表与原始 `body/content` 合并,生成最终的 Convex `content` 数组:
```rust
fn build_page_content(
original_content: &Value, // 原始的 aggregate.body.content
parsed_blocks: &[ParsedBlockInfo],
) -> Value {
// 按 parsed_blocks 顺序遍历
// - 若 block_id 在 original 中存在:
// 复制 original 中该块的完整结构,仅替换 text
// - 若 block_id 为 None(新块):
// 生成新 block_id,作为 paragraph 追加
// - 若 original 中的复杂块不在 parsed 中:
// 保留(markdown_edit 不主动删块)
// 返回 Value::Array
}
```
#### 3.3.4 在 `doc_markdown_edit` 中的集成(替换现有 online 写回)
```rust
// 当前(doc.rs ~line 856:
let block_ops = build_block_ops_from_markdown_edit(&blocks, &operations, applied);
if applied > 0 && block_ops.is_empty() { ... }
let revision = ...;
let conflict_detection_key = ...;
let apply_result = doc_apply_block_ops_with_meta(state, context, input, &aggregate, block_ops, revision, conflict_detection_key).await?;
// 替换为:
let parsed = parse_final_markdown_to_blocks(&md, &blocks);
let next_content = build_page_content(&current_body_content(&aggregate), &parsed);
let changed_blocks = build_changed_blocks_summary(&original_blocks, &parsed);
execute_page_body_save_from_aggregate(state, context, input, &aggregate, next_content, changed_blocks).await?
```
---
## 4. 边界情况处理
### 4.1 原始块 ID 在最终 md 中消失
可能发生在:
- 用户手动删除了 `<!-- block:id -->` 注释(极罕见,通过 AI 输出不可能)
- 模型输出的 search/replace 跨段合并了内容
**策略**:保留该原始块在 `content` 中的位置。`build_page_content` 对在 original 中存在但不在 parsed 中的块,按原样复制到最终 content。
### 4.2 新建块(无原始 ID
用户可能要求"在末尾加一段总结"。模型输出 `full_content` 或 search/replace 产生的新文本带 `<!-- block:new_paragraph -->`。解析器标记为 `is_new=true`,写入层分配新的 `blockId`
**策略**:新块使用临时 ID 格式 `ai_block_{timestamp}_{counter}`,与现有 `mnote.block.insert_after` 的块 ID 风格一致。
### 4.3 复杂块保留
resource/mindmap/table/image 块在 `blocks_to_markdown` 中被序列化为特殊标记 `[mnote-raw-block:id]`。搜索替换通常不会命中这些行(因为它们不含普通文本),但如果意外命中:
**策略**:在 `search_replace` 中,如果 search 路径包含了复杂块的特殊标记,整条 operation 标记为 failed。解析器遇到 `[mnote-raw-block:...]` 标记时,直接从 original blocks 复制。
### 4.4 搜索替换未命中任何块(applied > 0 但所有命中都是新文本)
当模型输出的 full_content 完全不同于原文时可能出现。
**策略**`parse_final_markdown_to_blocks` 对每一行都产生块。如果没有任何 `<!-- block:id -->` 注释,所有块标记为 `is_new=true``build_page_content` 追加新块在后面,同时保留所有原始复杂块。
### 4.5 revision / conflictDetectionKey 一致性
`build_block_ops_from_markdown_edit` 当前依赖 `doc_apply_block_ops_with_meta` 来传递 revision。替换为直接调用 `execute_page_body_save_from_aggregate`,它从 aggregate 读取 revision/conflictDetectionKey [block.rs:1178-1189]。
**策略**:新路径从已经读到的 `aggregate` 中获取 revision/conflictDetectionKey,与现有路径完全一致。
### 4.6 dryRun 模式
当前 online 路径中 dry_run 由 `doc_apply_block_ops` 内部处理(不真正写入)。新路径也需要支持:
**策略**:若 `dryRun == true`,调用 `build_page_content` 但不调用 `execute_page_body_save_from_aggregate`,返回 diff 预览。diff 格式与当前 `docs.md:plan_update` 的 diff 格式一致。
---
## 5. 与已有代码的互动
### 5.1 `build_block_ops_from_markdown_edit` 的删除
该函数不再被 `doc_markdown_edit` 调用。它是一个私有 `fn`(仅 `doc.rs` 内部可见),唯一调用者被移除后成为死代码。
**策略**:直接删除函数体。保留调用处的行作为注释(`// 退役:7-27 改为 final_md→blocks 直接写回`),供后续参考。
### 5.2 `block_projection_blocks` 的继续使用
仍然需要原始 blocks 作为元数据源(提取 revisionRef、props、复杂块)。不改变。
### 5.3 `local_markdown_parser` 的复用
`parse_final_markdown_to_blocks` 可以复用 `local_markdown_parser::markdown_to_blocks` 对 GFM 结构的解析(heading、todo、code block、list),但需要用自己的逻辑提取 `<!-- block:... -->` 注释。不一定要用 comrak 的 `HtmlBlock` 解析;更可靠的方法是正则提取注释,然后从剩余内容中推断块类型。
**策略**:默认使用简单的行级处理(非 comrak),因为 `blocks_to_markdown` 的输出是每行一块的简单格式,不需要完整的 GFM AST。
> **⚠️ 跨行约束**:行级处理仅适用于 `blocks_to_markdown` 产出的单行块格式。以下情况需要 fallback 到 `local_markdown_parser::markdown_to_blocks`
> - 代码块(`block_text` 含 `\n`
> - 用户通过 `full_content` 自由书写的多段 markdown
> - 解析跳过了 `<!-- block:id -->` 的行之间的纯段落
>
> fallback 策略:对无 `<!-- block:... -->` 注释的连续行,收集后一次性通过 GFM 解析器分割为多个常规块。
### 5.4 `changed_blocks` 摘要生成
当前返回的 `applyResult.changedBlocks``doc_apply_block_ops` 产生。新路径需要自己生成:
```rust
fn build_changed_blocks_summary(
original_blocks: &[Value],
parsed_blocks: &[ParsedBlockInfo],
) -> Vec<Value> {
// 对比原始 blocks 和解析后的 blocks
// 对文本改变的块输出 { op: "replace", blockId, content }
}
```
响应体中的 `changedBlocks` 字段格式不变,保持与下游消费者(SSE delta 等)的兼容。
---
## 6. 实施计划
### 步骤 1:增强 `blocks_to_markdown`(小)
**文件**: `doc.rs:574`
改动:
- 注释格式从 `<!-- block:{id} -->` 改为 `<!-- block:{id}:{type}:{rev} -->`
- 对复杂块(`editable==false``unsupportedReason!=null`)输出 `[mnote-raw-block:{id}] <!-- block:{id}:{type}:{rev} -->`
**风险**: 低。纯格式变更,向前兼容——旧注释格式的 md 在解析器看来只是缺类型/rev,可通过 fallback 从原始 blocks 查。
**测试**: 更新现有 `test_blocks_to_markdown_with_ids`,验证新格式。
### 步骤 2:实现 `parse_final_markdown_to_blocks`(中)
**新建模块**`doc_md_to_blocks.rs` 或放在 `doc.rs` 末尾
核心逻辑(~120 行):
```rust
struct ParsedBlock {
block_id: Option<String>,
block_type: String,
text: String,
revision_ref: Option<String>,
original: Option<Value>,
is_new: bool,
}
fn parse_final_markdown_to_blocks(md: &str, originals: &[Value]) -> Vec<ParsedBlock> {
// 1. 为 originals 建立 block_id → Value 的 HashMap
// 2. 将 md 按行分割
// 3. 对每行:
// a. 用正则提取 <!-- block:id:type:rev --> 或 [mnote-raw-block:id]
// b. 若无注释 → is_new=true, type="paragraph"
// c. 若有注释 → 查 originals_map 继承 revision_ref/props
// d. 提取 <!-- 前的纯文本
// 4. 判断块类型:若注释中有 type 则用注释的,否则从行前缀推断
// 5. 返回 ParsedBlock 列表
}
```
**正则示例**
```rust
lazy_static! {
static ref BLOCK_COMMENT_RE: Regex = Regex::new(
r"<!--\s*block:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]*)\s*-->"
).unwrap();
static ref RAW_BLOCK_RE: Regex = Regex::new(
r"\[mnote-raw-block:([a-zA-Z0-9_-]+)\]"
).unwrap();
}
```
**测试**
- `test_parse_empty_md` → 空输入 → 空输出
- `test_parse_with_block_ids` → 包含 `<!-- block:b1:paragraph:r1 -->` → 返回正确 ParsedBlock
- `test_parse_no_ids` → 纯文本 → 全部 is_new
- `test_parse_raw_block``[mnote-raw-block:b3]` → 从 originals 复制
- `test_parse_type_from_prefix``## Title` → type="heading"
- `test_parse_todo_prefix``- [x] Done` → type="todo"
### 步骤 3:实现 `build_page_content`(中)
**文件**: 与步骤 2 同模块(~80 行)
```rust
fn build_page_content(original_content: &Value, parsed: &[ParsedBlock]) -> Value {
// 1. 为 original_content 建立 block_id → full_block 映射
// 2. 遍历 parsed_blocks
// - 有 block_id 且 original 中存在 → 复制 original 块,更新 text
// - 无 block_id → 创建新 paragraph 块
// - 有 block_id 但不在 original 中 → 复制 original 中能找到的(从已处理集合中移除)
// 3. 遍历 original_content 中的复杂块(不在 parsed 中出现的)→ 追加
// 4. 返回 Value::Array
}
```
**测试**
- `test_build_content_preserves_unmodified_blocks`
- `test_build_content_updates_text`
- `test_build_content_preserves_complex_blocks`
- `test_build_content_new_blocks_appended`
### 步骤 4:替换 `doc_markdown_edit` 的 online 写回路径(小)
**文件**: `doc.rs:854-900`
将现有的:
```rust
let aggregate = aggregate_value(state, context, input).await?;
let blocks = block_projection_blocks(&aggregate);
let block_ops = build_block_ops_from_markdown_edit(&blocks, &operations, applied);
if applied > 0 && block_ops.is_empty() { ... }
let apply_result = doc_apply_block_ops_with_meta(...).await?;
```
替换为:
```rust
let aggregate = aggregate_value(state, context, input).await?;
let blocks = block_projection_blocks(&aggregate);
let parsed = parse_final_markdown_to_blocks(&md, &blocks);
let original_content = current_body_content(&aggregate);
let next_content = build_page_content(&original_content, &parsed);
let changed_blocks = build_changed_blocks_summary(&blocks, &parsed);
let apply_result = execute_page_body_save_from_aggregate(
state, context, input, &aggregate, next_content, changed_blocks
).await?;
```
新增 `build_changed_blocks_summary`~30 行)。
**风险**: 中。第一次替换时需要与现有测试对照输出,确保 changed_blocks 格式一致。
**测试**
- 现有 `hermes_tools_markdown_edit_*` 测试全部重新运行——断言现有行为不变
- 新增 `test_markdown_edit_online_single_replacement`:验证单块替换后 content 正确
- 新增 `test_markdown_edit_online_full_content`:验证全文替换不再被拒绝
- 新增 `test_markdown_edit_online_preserves_complex_blocks`:验证 mindmap 块不变
### 步骤 5:更新 full_content 处理(小)
**文件**: `doc.rs:785-800`
当前 `full_content` 构造一个 `search: current_md.trim(), replace: full.trim()` 的 operation。替换后不再需要这个折中——直接解析 full_content 为 blocks。
改动:在 `parse_final_markdown_to_blocks` 中,若没有任何 `<!-- block:id -->` 注释,所有块标记为 is_new。`build_page_content` 只追加新块 + 保留原始复杂块。
**测试**
- `test_markdown_edit_online_full_content_creates_new_blocks`
- `test_markdown_edit_online_full_content_preserves_complex`
### 步骤 6:清理(小)
- 删除 `build_block_ops_from_markdown_edit` 函数体(私有 fn,唯一调用者已移除)
- 在原调用位置保留注释:`// 退役:7-27 改为 final_md→blocks 直接写回`
- 更新 manifest/guidance 文字:full_content 不再被拒绝
---
## 7. 测试矩阵
| 场景 | 输入 | 期望输出 | 优先级 |
|------|------|---------|--------|
| 单块精确 search/replace | `{search:"第一段", replace:"新内容"}` | 文本更新,blockId/revisionRef 不变 | P0 |
| 单块忽略空白 match | `{search:"第 一 段", replace:"新内容"}` | 文本更新(原块中"第一段"→"新内容" | P0 |
| 多 operation 命中同块 | `[{op1},{op2}]` 同块 | 最终文本 = 两次替换累积结果 | P0 |
| full_content | `"新全文"` | 新文本创建新块,原复杂块保留 | P0 |
| 影响 heading | search 命中 heading 文本 | heading 类型不变,文本更新 | P1 |
| 包含复杂块文档的替换 | 只改 paragraph 文本 | resource/mindmap 块不变 | P1 |
| dryRun 不落盘 | `dryRun: true` | 返回 diff,无写入 | P1 |
| revision 过期 | aggregate revision 已过期 | 冲突错误,不写入 | P1 |
| 原始块 ID 消失 | 搜索替换删除了 `<!-- block -->` 注释 | 该原始块保留(不从 content 删除) | P2 |
| 空 operations | `[]` | 错误码 | P2 |
| 纯文本 md(无块注释) | `"摘要\n\n补充"` | 全部 is_new,追加到末尾 | P2 |
---
## 8. 灰度/回滚
不设 feature flag。直接替换 online 写回路径。理由:
- 旧路径(7-24/7-25 修复后)在 `full_content` 等场景是**安全降级(拒绝写入)**,新路径是功能增强。不会出现「旧路径能写入、新路径不能」的退化。
- 现有 `hermes_tools_markdown_edit_*` 测试集 + 新增 5 个测试组合足够兜底。
- 若确实需要回滚,用 `git revert` 回退本次改动即可。
灰度策略:默认开启,观察到新增测试全部 pass 后合入主线。
---
## 9. 关联文件清单
| 文件 | 变更类型 | 变更内容 |
|------|---------|---------|
| `doc.rs` `blocks_to_markdown` | 修改 | 注释格式增强 |
| `doc.rs` `doc_markdown_edit` | 修改 | online 写回路径 |
| 新建 `doc_md_to_blocks.rs` | 新增 | `parse_final_markdown_to_blocks` + `build_page_content` + `build_changed_blocks_summary` |
| `doc.rs` `build_block_ops_from_markdown_edit` | 标记 | `#[deprecated]` |
| `hermes_tools.rs` tests | 新增 | ~5 个新测试 |
| `manifest.rs` / guidance.md | 微调 | full_content 不再标记为拒绝 |
---
## 10. 开放问题
1. **`<!-- block:id:type:rev -->` 注释在行中可能被模型生成的 search/replace 破坏**。例如模型输出 `{search: "第一段 <!-- block:", replace: "新段"}` 前半个注释。这是用户级错误(模型错误地替换了元数据),写入层应在 parse 阶段检测不完整的注释并报错。
2. **`full_content` 场景下的块类型保留**。如果用户要求"把整个文档改写成大纲格式",模型输出不包含 `<!-- block-->` 注释的全部新文本,此时 heading/todo 类型从 GFM 前缀推断。但列表、引用、代码块等格式需确认 `local_markdown_parser``markdown_to_blocks` 返回的结构是否足够完整。
3. **行内格式(bold、italic、link)能否在往返中保持**。当前 `blocks_to_markdown` 只输出文本(`block_text`),丢弃所有 marks。在线文档的 text 存储包含 marksbold/italic/code/link),当前 `doc_markdown_edit` 的整体设计不保证行内格式——这超出了 7-24/7-25 的范围,但建议在 7-14 的 Phase A/B 之后评估。
+6 -5
View File
@@ -1,16 +1,17 @@
# 10-review 审查总览
> 执行状态:`done/01` 到 `done/07` 均已归档;`process/08` 是当前活跃架构收口与下一阶段优先级 review`process/09` 记录页面 AI 快速块编辑 runtime 的阶段性结论与下一步基建方向
> 执行状态:`done/01` 到 `done/11` 均已归档;当前活跃 10-review process 文档
当前活跃审查:
- [Kernel 架构收口与下一阶段优先级 Review / Checklist](./process/08-kernel-architecture-next-priority-review-and-checklist.md)
- [页面 AI 快速块编辑 Runtime Review](./process/09-page-ai-fast-block-edit-runtime-review.md)
- [当前 mnote 项目 AI / Page Aggregate 定向 Review](./process/10-current-mnote-ai-runtime-review-v1.md)
- [当前完整架构 Review](./process/11-current-full-architecture-review-v1.md)
- 暂无。
最新归档审查:
- [Kernel 架构收口与下一阶段优先级 Review / Checklist](./done/08-kernel-architecture-next-priority-review-and-checklist.md)
- [当前 mnote 项目 AI / Page Aggregate 定向 Review](./done/10-current-mnote-ai-runtime-review-v1.md)
- [当前完整架构 Review](./done/11-current-full-architecture-review-v1.md)
- [页面 AI 快速块编辑 Runtime Review](./done/09-page-ai-fast-block-edit-runtime-review.md)
- [VSCode Explorer 文件树与垃圾箱闭环增量审查](./done/07-vscode-explorer-filetree-trash-gap-review.md)
上一轮最终执行入口:
@@ -1,8 +1,8 @@
# 08 [process] Kernel 架构收口与下一阶段优先级 Review / Checklist v1
# 08 [done] Kernel 架构收口与下一阶段优先级 Review / Checklist v1
> 更新时间:2026-05-16
> 更新时间:2026-05-18
>
> 执行状态:`process`
> 执行状态:`done`
>
> 关联文档:
> - `/mnt/Data1T/mnote/AGENTS.md`
@@ -35,14 +35,14 @@
- Page Aggregate 仍是过渡态,block projection 主要从 `documents.content` / local markdown content 投影,不是 EditorBlockDocument 原生落库完成态。
- 标题、正文、页面设置、page tree、AI 写入口尚未完全闭环到同一组 projection / command family。
- `tree.*` 已是 preferred command name,但 `documents.*` 兼容命令面仍未完全降级。
- `/api/tree/events` 已是 tree realtime 主链,但 Sidebar、page subtree、filetree、preferred snapshot 还没有完全统一到一套 live cache
- `/api/realtime/ws` 已是 tree realtime 主链,`/api/tree/events` 已降级为 SSE fallbacklive cache 的后续重点是减少兼容 fallback 与补偿链
- AI 块工具已有最小闭环,但当前编辑路径以块操作(blockId)为中心——AI 被迫理解 UI 层概念,且本地 `.md` 文件无 AI 写入路径。`7-14` 提出应将 AI 编辑主路径修正为 markdown 文本级(search/replace),`mnote.block.*` 降级为结构性辅助,在线 Convex 文档与本地文件共用同一条 markdown 写入路径。
补充 BlockNote / Tiptap AI 参考后的判断:
- BlockNote AI 和 Tiptap AI Toolkit 可以参考的是 AI runtime shape 与 tool contract,不是 mnote 的事实源。
- AI 方向下一步只应补基础底座:`PageAIContextBuilder``scope=selection``page_xml/text`、tool manifest annotations、`PageAIReviewSession`、accept/reject/retry/abort 状态机。
- 2026-05-16 补充( `7-14` 方向修正):AI 编辑主路径应从块级操作调整为 markdown 文本级 search/replace,新增 `mnote.doc.markdown_edit` 作为主要 AI 写入工具;当前 `local_rule` planner`direct_block_edit_operations`是过渡实现,应退役。在线 Convex 文档和本地 `.md` 文件共用同一条写入路径
- 2026-05-18 补充(`7-18` `7-25` 收口后):AI 简单正文编辑主路径已切到 markdown 文本级 search/replace / full_content`page_ai_workflow.rs` 当前通过模型生成 markdown 编辑意图后调用 `mnote.doc.markdown_edit`,并复用统一 mnote tool executor`local_rule` planner`direct_block_edit_operations`不再作为当前 runtime 口径。在线 Convex 文档和本地 `.md` 文件共用同一条 markdown 写入合同
- 在非 AI 主架构和 AI 基础合同打牢之前,不应扩展新的 AI agent 工作流、复杂 AI UI 或跨页面智能功能。
因此,下一阶段不应优先“大改架构”或“大量加新功能”,而应优先做:
@@ -89,7 +89,7 @@
目标:
- `/api/tree/events` 的 snapshot / delta / resync 成为 Sidebar、Page Tree、File Tree、page subtree 的共同 live cache 来源。
- `/api/realtime/ws` 的 snapshot / delta / resync 成为 Sidebar、Page Tree、File Tree、page subtree 的共同 live cache 来源`/api/tree/events` 仅作为 SSE fallback
- 减少 query/refetch/freshness 补偿链和旧快照回闪。
### P0AI 基础工具与审阅底座
@@ -115,13 +115,13 @@
- 不新增复杂 AI 自动化功能面。
- 不把 `@blocknote/xl-ai` 或 BlockNote `AIExtension` 作为 runtime dependency。
- 不让 AI 写入绕过 `dryRun/idempotencyKey/revision/conflictDetectionKey/revisionRef`
- **不把 `direct_block_edit_operations` / `local_rule` planner 当作长期 AI 编辑路径(当前为过渡实现,将被 `markdown_edit` 替代)。**
- **不把 `direct_block_edit_operations` / `local_rule` planner 当作当前或长期 AI 编辑路径;简单正文编辑当前主路径是 `mnote.doc.markdown_edit`,结构性块操作才使用 `apply_block_ops` / `mnote.block.*`。**
- **不新增 `mnote.block.*` 工具;现有保留为结构性辅助。**
**2026-05-16 新增(受 7-14 方向修正):**
**2026-05-18 状态更新(`7-18``7-25` 收口后):**
- **Phase A(当前唯一活跃实施)**`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强search/replace + `format: "markdown"` + local source + Hermes 注册,7-14 v2 Phase A
- **Phase B(下一阶段)**:退役 `direct_block_edit_operations``page_ai_workflow.rs` 改走 `markdown_edit`7-14 v2 Phase B
- **Phase A / B 主路径已完成当前收口**`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强、Hermes manifest / guidance、`page_ai_workflow.rs``markdown_edit`、统一 mnote tool executor、local / online markdown source 合同已同步
- **Phase C 仍冻结**:流式 apply + suggest/review 只保留设计边界,当前不实施、不扩新 AI 功能
- 在线 Convex 文档和本地 `.md` 文件共用同一条 markdown AI 写入路径(`resolve_source` → Convex | LocalFS)。
### P1:定向 Bug Hunt
@@ -144,8 +144,8 @@
### 3.1 Page Aggregate 单一真源
- [x] 盘点 `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 是否仍有 runtime fallback 或 TS builder 读取分支。
- [x] 盘点 `wolai-frontend/src/lib/documents/page-aggregate-builder*` 的引用,确认只剩 test helper / historical adapter。
- [x] 盘点历史 `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 是否仍有 runtime fallback 或 TS builder 读取分支。
- [x] 盘点历史 `wolai-frontend/src/lib/documents/page-aggregate-builder*` 的引用,确认只剩 test helper / historical adapter。
- [x] 检查 `/api/documents/page` 仍返回明确 `410`,不参与 runtime 主链。
- [x] 跑文档页打开 smoke,记录 `/api/page-aggregate/:id` 是首要读链。
- [x] 新建页面后检查 Page Aggregate `identity/head/body/tree/stats` 字段完整。
@@ -156,7 +156,7 @@
- [x] 破坏或暂停 Convex query,检查响应是 degraded/error,不返回伪 fixture。
- [x] 在 `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` 勾选已验证项,并补证据路径。
2026-05-16 静态审计证据:
2026-05-16 静态审计证据(历史 Next 前端退役前快照)
- `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 只通过 `loadPageAggregateFromRustSnapshot` 请求 Rust `/api/page-aggregate/:documentId``loadPageAggregate` 不再回退 TS builder`buildServerBridgeRequest("/documents/page")` 只用于构造转发 header/request,不调用 Next `/api/documents/page` compat route。
- `rg -n "buildPageAggregateFromDocumentPayloads|page-aggregate-builder" wolai-frontend/src rust scripts design --glob '!design/05-editor-mainline/reference-code/**' --glob '!node_modules/**'` 显示 runtime 非测试引用仅剩 builder 定义;代码引用只有 `wolai-frontend/src/lib/documents/page-aggregate-builder.test.ts`
@@ -191,6 +191,8 @@
- 证据:`tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.json`,截图:`tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.png`stdout`tmp/page-aggregate-refresh-persistence-smoke/latest.stdout.json`
- 验证结果:刷新前后 `head.title=Page Aggregate refresh mp88fr6k``body.revision=1``body.conflictDetectionKey=tree_1778929070007_1:1``blockDocument.blocks[0].text=Page Aggregate refresh body mp88fr6k``layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact`;刷新后页头标题、`.ProseMirror` 正文、设置控件、`documentElement``.document-shell`、island root 和 `.editor-surface` 均保持最新值。
- 修复点:`rust/crates/mnote-web/src/ssr/pages/layout.rs``initializePageUiSurfaces` 延后到 `DOMContentLoaded` 后执行,避免 layout 脚本早于嵌入 Page Aggregate JSON / island DOM 完成时把 runtime 属性按默认 pageOptions 应用。
- 2026-05-18 复测并扩展 `scripts/task-page-aggregate-refresh-persistence-smoke.js`,补充 `mnote.doc.fetch` / `tree.pageSubtree` 同组回读证据。证据:`tmp/page-aggregate-refresh-persistence-smoke/mpagfma1.json`,截图:`tmp/page-aggregate-refresh-persistence-smoke/mpagfma1.png`
- 验证结果:刷新前后 `head.title=Page Aggregate refresh mpagfma1``body.revision=1``body.conflictDetectionKey=tree_1779063433221_1:1``blockDocument.blocks[0].text=Page Aggregate refresh body mpagfma1``layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact``tree.pageSubtree.rootNodeId=tree_1779063433221_1` 均一致;`mnote.doc.fetch` 返回 `schema=mnote.page_ai_context.v1``revision/conflictDetectionKey/revisionRef/text` 与同一 Page Aggregate block 完全一致。
- 缺陷记录:`bugs/05-editor-mainline/done/5-12-page-options-refresh-runtime-attrs-v1.md`
- 配套测试已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options`
@@ -216,47 +218,61 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_wi
### 3.2 Page Block AI Tooling
- [ ] 以 `7-10` 为工具执行清单、`7-11` 为 AI runtime 基础设计,逐 phase 检查已勾选项是否都有代码、测试、smoke 证据。
- [x] 以 `7-10` 为工具执行清单、`7-12`当前 AI runtime 基础设计,逐 phase 检查已勾选项是否都有代码、测试、smoke 证据。
- [x] 给 `mnote.doc.fetch scope=selection` 补真实选区上下文输入和返回结构。
- [x] 给 `mnote.doc.fetch format=page_xml` 补最小 PageXML 输出。
- [x] 给 `mnote.block.fetch format=text/page_xml` 补格式分支。
- [x] 给 tool manifest 补 `annotations`,区分 readonly / destructive / requiresApproval / selectionEffect。
- [x] 定义 `mnote.page_ai_context.v1`,明确 context 只来自 Page Aggregate projection。
- [ ] 定义 `mnote.page_ai_review_session.v1`,明确 accept / reject / retry / abort 语义。
- [ ] 给 `mnote.block.insert_after` 补多块插入限制和返回 inserted block ids。
- [ ] 给 `mnote.block.move_after` 补标题带子块阻断 smoke。
- [ ] 给 `mnote.block.move_after` 补列表项阻断 smoke。
- [ ] 给 `mnote.block.move_after` 补表格 / mindmap / resource 阻断 smoke。
- [x] 定义 `mnote.page_ai_review_session.v1`,明确 accept / reject / retry / abort 语义。
- [x] 给 `mnote.block.insert_after` 补多块插入限制和返回 inserted block ids。
- [x] 给 `mnote.block.move_after` 补标题带子块阻断 smoke。
- [x] 给 `mnote.block.move_after` 补列表项阻断 smoke。
- [x] 给 `mnote.block.move_after` 补表格 / mindmap / resource 阻断 smoke。
- [x] 给 `mnote.block.replace` 补 stale revision 失败用例。
- [x] 给 `mnote.block.replace` 补 stale blockRevisionRef 失败用例。
- [x] 给写工具补重复 idempotencyKey 的端到端用例。
- [x] 检查 `mnote.page.save` 在 manifest / UI 中继续标为页面级兜底,不显示为精确块编辑主入口。
- [ ] 检查任何新增 AI surface 是否只是基础 review/context/tooling 验收,不是新功能扩展。
- [ ] 更新 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。
- [ ] 更新 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。
- [x] 检查任何新增 AI surface 是否只是基础 review/context/tooling 验收,不是新功能扩展。
- [x] 更新 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。
- [x] 更新 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。
**新增(2026-05-16,受 7-14 方向修正):**
2026-05-18 `mnote.block.insert_after` 多块插入证据:
**Phase A(当前唯一活跃实施):**
- [ ] `mnote.doc.markdown_edit` 核心实现:resolve_source → search_replace → write_target
- [ ] `mnote.doc.markdown_edit` 搜索替换语义:精确匹配 → fuzzy fallback → operationsFailed7-14 §5
- [ ] `mnote.doc.markdown_edit` Convex 写入 adapter(复用 `page.body.save` 链路)
- [ ] `mnote.doc.markdown_edit` 本地文件写入 adapter`fs.writeFile` + GFM AST markdown 序列化)
- [ ] `mnote.doc.fetch format: "markdown"` 增强(Page Aggregate 产出 PageMarkdown
- [ ] `mnote.doc.fetch` local source 支持(识别本地文件路径并返回 `.md` 原文)
- [ ] Hermes tool manifest 注册 `mnote.doc.markdown_edit`
- [ ] Hermes `mnote` plugin 更新:`mnote_doc_fetch` schema 增加 `format: "markdown"` 和本地文件路径支持
- [ ] 乐观锁:`revision` 冲突检测
- [ ] 单元测试 + smoke(在线 + 本地两种 source
- 代码:`rust/crates/mnote-web/src/hermes_tools/block.rs` 支持 `content` / `block` / `blocks` 输入;`blocks` 限制 `1..=20`;多块写入按输入顺序连续插入,并在结果返回 `insertedBlockIds`
- manifest`rust/crates/mnote-web/src/hermes_tools/manifest.rs` 已声明 `blocks.minItems=1/maxItems=20`,并用 `anyOf` 表达 `content | block | blocks` 三种输入。
- 测试:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_insert_after -- --nocapture` 通过,覆盖多块成功和超过 20 个失败。
- 回归:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture` 通过,覆盖 Hermes tools 38 项。
**Phase B(下一阶段):**
- [ ] `direct_block_edit_operations` 退役(`page_ai_workflow.rs` `local_rule` 分支)
- [ ] `page_ai_workflow.rs` 修正:system prompt 改为产 search/replace 对,底层走 `mnote.doc.markdown_edit`
- [ ] 补全 operation schema`content` 字段)
- [ ] 浏览器 smoke:自然语言编辑可用(不使用「」格式)
2026-05-18 `mnote.block.move_after` 复杂块阻断证据:
- 代码:`rust/crates/mnote-web/src/hermes_tools/block.rs` 继续以同父级、叶子块、可移动类型、editable、自身移动为真实写入前阻断条件。
- 测试:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture` 通过,覆盖标题带子块、列表项、表格、mindmap、resource 均返回 `blocked=true` / `block_move_after_blocked`
- 回归:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture` 通过,当前 Hermes tools 共 39 项。
**新增(2026-05-18`7-18``7-25` 收口状态):**
**Phase A / B(当前主路径已收口):**
- [x] `mnote.doc.markdown_edit` 核心实现:resolve_source → search_replace / full_content → write_target
- [x] `mnote.doc.markdown_edit` 搜索替换语义:精确匹配、归一化匹配、多操作同块合并、无法安全映射时失败
- [x] `mnote.doc.markdown_edit` Convex 在线写入 adapter 以最终 markdown 映射写回
- [x] `mnote.doc.markdown_edit` 本地文件写入 adapter 尊重 dryRun / idempotency
- [x] `mnote.doc.fetch format: "markdown"` 增强
- [x] Hermes tool manifest 注册并描述 `mnote.doc.markdown_edit` 写入合同
- [x] 乐观锁:revision / conflictDetectionKey 前置校验
- [x] `direct_block_edit_operations` / `local_rule` 不再作为 `page_ai_workflow.rs` 当前主路径
- [x] `page_ai_workflow.rs` 改为模型产 markdown search/replace / full_content,底层走 `mnote.doc.markdown_edit`
- [x] 单元测试覆盖 markdown_edit / page_ai_workflow / Hermes guidance / ACP tool contract
**Phase C(设计冻结,不实施):**
- [ ] 流式 apply + suggest/review 设计已冻结,不阻塞 Phase A/B 实施
- [x] 流式 apply + suggest/review 设计已冻结,不阻塞 Phase A/B 实施
2026-05-18 Review Session 合同定义:
- 已在 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 定义 `mnote.page_ai_review_session.v1` 最小 schema。
- 已明确状态:`draft/planning/previewing/awaiting_user/accepted/rejected/applying/applied/failed/aborted/stale`
- 已明确动作约束:`accept` 必须重新读 Page Aggregate 并校验 revision / conflictDetectionKey / revisionRef`reject/abort` 不产生写入,`retry` 必须生成新 proposal 或 dry-run previewyolo 模式也应生成同构 audit 数据。
- 边界:这里只完成合同定义,不实施 Phase C 流式 apply 或新的审阅 UI。
2026-05-16 Page Block AI context / format focused smoke 证据:
@@ -271,15 +287,17 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_wi
- 边界:本轮只验证 `mnote.doc.apply_block_ops` 的 selection scope guard;单个 `mnote.block.replace/insert_after/move_after` 尚未校验 `allowedTargetBlockIds`,不能据此勾选完整 selection 写保护矩阵。
- 配套验证已通过:`node --check scripts/task-page-block-ai-context-format-smoke.js``cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture``MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-context-format-smoke.js`
2026-05-16 Page Block AI stale / idempotency focused smoke 证据:
2026-05-16 / 2026-05-18 Page Block AI stale / idempotency focused smoke 证据:
- 新增并运行 `scripts/task-page-block-ai-conflict-idempotency-smoke.js`,通过真实 3000 + 测试账号创建临时页,用 `mnote.page.save` 初始化 `p_1/p_2`,再围绕 `mnote.block.replace` 验证冲突和幂等安全边界。
- 证据:`tmp/page-block-ai-conflict-idempotency-smoke/mp8dyqiq.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mp8dyqiq-page.png`
- 最新复测证据:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs-page.png`
- 验证结果:
- 首次 `mnote.block.replace` 携带最新 `revision/conflictDetectionKey/blockRevisionRef/idempotencyKey` 成功写入 `p_2`,返回 `commandName=page.body.save``commandId=page_body_save_req_1778938353824_11`
- 使用同一 `idempotencyKey=idem_conflict_replace_mp8dyqiq` 再次调用 `mnote.block.replace`,即使请求 content 改成不同文本,也 replay 同一 `commandId`Page Aggregate 回读 `p_2` 仍为首次写入文本,`revisionAfterFirst=2``revisionAfterReplay=2`,确认不重复写入。
- 使用旧 `revision/conflictDetectionKey` 调用 `mnote.block.replace` 返回 HTTP `400`、错误码 `mnote_tool_conflict`,正文保持首次写入结果。
- 使用最新 `revision/conflictDetectionKey` 但旧 `blockRevisionRef` 调用 `mnote.block.replace` 返回 HTTP `400`、错误码 `mnote_tool_conflict`,正文保持首次写入结果。
- 2026-05-18 复测中,重复 `idempotencyKey=idem_conflict_replace_mpafrevs` replay 同一 `commandId=page_body_save_req_1779062303997_525``revisionAfterFirst=2``revisionAfterReplay=2`;旧 revision 与旧 blockRevisionRef 仍均返回 `mnote_tool_conflict`
- 边界:本轮只覆盖 `mnote.block.replace` 直接 tool executor 的 stale revision / stale blockRevisionRef / idempotency replay;不代表 `mnote.block.insert_after` 幂等矩阵、所有写工具幂等矩阵、review session accept stale 或 accept/reject/retry/abort 已完成。
- 配套验证已通过:`node --check scripts/task-page-block-ai-conflict-idempotency-smoke.js``cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture``MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-conflict-idempotency-smoke.js`
@@ -302,11 +320,17 @@ node scripts/task-page-block-ai-tools-smoke.js
- [x] 对 compat alias 返回增加 owner / deprecated 标识,避免被当作主链。
- [x] 更新 `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md` 或新增后续 process checklist。
2026-05-16 Tree Command Cutover 静态盘点与 alias 标识证据
2026-05-18 当前 Tree Command Cutover 状态
- `3000` 当前主壳由 Rust `mnote-web` SSR 承载;主要页面生命周期动作通过 Rust `/api/tree/commands` route 生成 `tree.node.create``tree.node.rename``tree.subtree.move``tree.node.archive``tree.node.restore``tree.node.purge``tree.subtree.copy`
- `bridge-runtime` 仍接受历史 `documents.*` alias,但 execution plan / artifact 会标记 `preferredCommandName``compatCommandName``deprecatedAlias`,避免兼容命令被当成当前主链。
- `wolai-frontend` / Next tree command route 只作为退役前历史审计材料,不再作为当前运行时主链依据。
2026-05-16 Tree Command Cutover 静态盘点与 alias 标识证据(历史 Next 前端退役前快照):
- 只读盘点命令:`rg -n "documents\\.(create|title\\.update|move|archive|restore|delete|purge|copy_tree)" rust wolai-frontend scripts --glob '!node_modules/**'`
- 前端主链:`wolai-frontend/src/lib/documents/tree-command-client.ts` 的新建、重命名、移动、归档、恢复、永久删除和复制均 POST `/api/tree/commands`,返回 meta 使用 `TREE_COMMAND_PROTOCOL.*.preferredCommandName``TREE_COMMAND_PROTOCOL` 中的 `documents.*` 只保留为 `compatCommandName`
- Next tree command route`wolai-frontend/src/app/api/tree/commands/route.ts` 在 create / move / rename / archive / restore 分支分别构造 `tree.node.create``tree.subtree.move``tree.node.rename``tree.node.archive``tree.node.restore`
- 退役前 Next 前端主链:`wolai-frontend/src/lib/documents/tree-command-client.ts` 的新建、重命名、移动、归档、恢复、永久删除和复制均 POST `/api/tree/commands`,返回 meta 使用 `TREE_COMMAND_PROTOCOL.*.preferredCommandName``TREE_COMMAND_PROTOCOL` 中的 `documents.*` 只保留为 `compatCommandName`
- 退役前 Next tree command route`wolai-frontend/src/app/api/tree/commands/route.ts` 在 create / move / rename / archive / restore 分支分别构造 `tree.node.create``tree.subtree.move``tree.node.rename``tree.node.archive``tree.node.restore`
- Rust Web tree route`rust/crates/mnote-web/src/routes/tree.rs``create_command_wire` 输出 `tree.node.create``tree.node.rename``tree.subtree.move``tree.node.archive``tree.node.restore``tree.node.purge``tree.subtree.copy``tree_command` route 只接收 action,不接收旧 `documents.*` command name 作为主链输入。
- 兼容保留:`bridge-runtime` 仍接受 `documents.create/title.update/move/delete/restore/purge/copy_tree`,但本轮已在对应 execution plan 的 `args_json.commandProtocol` 增加 `family=tree``owner=rust-runtime-kernel``preferredCommandName``compatCommandName``deprecatedAlias`;旧 `documents.*` alias 会标记 `deprecatedAlias=true`
- Transport 边界:`rust/crates/mnote-web/src/transport/convex.rs` 发送给 Convex legacy mutation 前会剥离 `commandProtocol``streamDeltaHint``domainEventHint``domainEventPlan(s)`,避免 legacy validator 把审计字段当写入参数。
@@ -337,14 +361,20 @@ node scripts/task122-rust-web-create-page-ui-smoke.js
- [x] 重命名页面后,双浏览器 A/B 检查 Sidebar / Breadcrumb / File Tree 一致更新。
- [x] 移动页面后,双浏览器 A/B 检查 tree order 不回闪。
- [x] 删除 / 恢复后检查 trash 与主树事件一致。
- [x] 断开 SSE 后恢复,检查 resync 能把 UI 拉回正确状态。
- [x] 断开 WS / SSE fallback 后恢复,检查 resync 能把 UI 拉回正确状态。
- [x] 更新 `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md` 的已验证项。
2026-05-16 Tree Realtime Live Cache 静态盘点与 payload 覆盖证据
2026-05-18 当前状态更新
- 只读审查确认 React `AppLayoutShell` 同时接入 `useSidebarData``useSidebarTreeStream``usePreferredSidebarSnapshot` 负责 freshness 仲裁;Page Tree / File Tree 通过 preferred snapshot 消费 `kernelSidebarTree``kernelFileTreeProjection`
- 仍未统一的补偿链:`useSidebarData` 还保留 Convex query、HTTP `/api/sidebar` fallback 与手动 `refetch`mutation 后仍有 `refreshTree` / `sidebarQuery.refetch`Rust SSR shell 与 React hook 目前各自可建立 EventSourcepage subtree 仍从 Page Aggregate client state 派生,不应误标为已统一 live cache
- Rust stream 盘点确认 workspace snapshot 已同时加载 `KernelProjectionKind::SidebarTree``KernelProjectionKind::FileTree`SSE payload 的 `data.dataset.kernel_sidebar_projection` / `data.dataset.kernel_file_tree_projection` 覆盖 page/file rowsubtree snapshot 仍只覆盖 `page_tree`,不含 file tree projection
- `/api/realtime/ws` 已成为 Rust SSR 主壳默认 transport`convex-command-log-ws`),`/api/tree/events` 仅作为 SSE fallback
- `CURRENT_ARCHITECTURE.md``bugs/03-rust-web/done/3-16` `3-18` 已同步:原“WS 文档口径 / 前端 SSE 实现”冲突、SSE push 跳过 polling safety net、WS/SSE delta 合同分裂均已修复
- 2026-05-16 下面的 SSE smoke 证据保留为迁移前历史验证材料,不再代表当前主链
2026-05-16 Tree Realtime Live Cache 静态盘点与 payload 覆盖证据(WS 迁移前历史快照):
- 退役前只读审查确认 React `AppLayoutShell` 同时接入 `useSidebarData``useSidebarTreeStream``usePreferredSidebarSnapshot` 负责 freshness 仲裁;Page Tree / File Tree 通过 preferred snapshot 消费 `kernelSidebarTree``kernelFileTreeProjection`
- 退役前未统一的补偿链:`useSidebarData` 还保留 Convex query、HTTP `/api/sidebar` fallback 与手动 `refetch`mutation 后仍有 `refreshTree` / `sidebarQuery.refetch`Rust SSR shell 与 React hook 当时各自可建立 EventSourcepage subtree 当时仍从 Page Aggregate client state 派生。
- Rust stream 历史盘点确认 workspace snapshot 已同时加载 `KernelProjectionKind::SidebarTree``KernelProjectionKind::FileTree`SSE payload 的 `data.dataset.kernel_sidebar_projection` / `data.dataset.kernel_file_tree_projection` 覆盖 page/file rowsubtree snapshot 当时只覆盖 `page_tree`,不含 file tree projection。
- 本轮新增 `stream_change_preserves_remove_asset_delta_fields`,确认 `tree.resource.delete``remove_asset` delta 字段保真;`structural_delta_requires_projection_snapshot` 已覆盖 `remove_asset` 需要 projection snapshot,避免资源删除类事件只靠局部 patch。
- 本轮强化 `scripts/task123-rust-web-tree-live-stream-consumer-smoke.js`:解析 SSE `snapshot` data,断言 `kind=snapshot``stream=workspace``projection=sidebar_tree``x-mnote-tree-stream-owner=rust-web`,并确认 workspace snapshot 中有 `kernel_sidebar_projection``kernel_file_tree_projection` 和临时页 `doc:<documentId>` file tree row。
@@ -425,21 +455,26 @@ node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js
### 3.5 定向 Bug Hunt
- [ ] 建立 bugs 分类:Page Aggregate 问题归 `bugs/05-editor-mainline/process/`
- [ ] Tree command / realtime / File Tree 问题归 `bugs/04-tree-domain/process/`
- [ ] AI tools 问题归 `bugs/07-ai/process/`
- [ ] 每个 bug 必须包含复现步骤、期望、实际、证据截图或 JSON、owner 判断。
- [ ] 先补最小 failing smoke,再修实现。
- [ ] 修复后移动到对应 `done/`,并记录验证命令。
- [x] 建立 bugs 分类:Page Aggregate 问题归 `bugs/05-editor-mainline/process/`
- [x] Tree command / realtime / File Tree 问题归 `bugs/04-tree-domain/process/`
- [x] AI tools 问题归 `bugs/07-ai/process/`
- [x] 每个 bug 必须包含复现步骤、期望、实际、证据截图或 JSON、owner 判断。
- [x] 先补最小 failing smoke 或定向单测,再修实现。
- [x] 修复后移动到对应 `done/`,并记录验证命令。
重点 bug 方向:
- [ ] Page Aggregate 与页头标题不一致。
- [ ] Page Aggregate 与 File Tree `{title}.md` 不一致。
- [ ] AI 写入成功但 Page Aggregate 回读旧内容。
- [ ] stale revision 未阻断写入。
- [ ] tree stream 断线恢复后 UI 停在旧快照。
- [ ] 旧 compat / debug route 在 3000 首屏被误用。
- [x] Page Aggregate 与页头标题不一致。
- [x] Page Aggregate 与 File Tree `{title}.md` 不一致。
- [x] AI 写入成功但 Page Aggregate 回读旧内容。
- [x] stale revision 未阻断写入。
- [x] tree stream 断线恢复后 UI 停在旧快照。
- [x] 旧 compat / debug route 在 3000 首屏被误用。
2026-05-18 定向 bug hunt 收口:
- `find bugs -path '*/process/*' -type f | wc -l` 结果为 `0`
- 本轮 P0 / 内核优先缺陷已迁入 `bugs/*/done/`,并在 `CURRENT_ARCHITECTURE.md``design/10-review/done/10-current-mnote-ai-runtime-review-v1.md``design/10-review/done/11-current-full-architecture-review-v1.md` 中同步验证证据。
### 3.6 2026-05-16 暂停交接进展摘要
@@ -497,16 +532,55 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:300
## 4. 当前不优先做
- [ ] 不优先增加新的编辑器 UI 大功能。
- [ ] 不优先新增 AI Agent 工作流功能。
- [ ] 不优先扩展 AI 功能面;AI 只做基础上下文、工具合同、审阅会话、冲突与回滚。
- [ ] 不优先大规模替换 Convex。
- [ ] 不优先重写 `leptos-tiptap` 输入层。
- [ ] 不把 `mnote.page.save` 包装成精确块编辑长期方案。
- [ ] 不在 compat route 继续扩写长期业务语义。
- [ ] 不把 BlockNote / Tiptap AI runtime 作为 mnote runtime 依赖。
- [ ] 不把 `direct_block_edit_operations` / `local_rule` planner 当作长期 AI 编辑路径(当前为过渡实现,将被 `mnote.doc.markdown_edit` 替代)
- [ ] 不新增 `mnote.block.*` 工具;`mnote.block.*` 保留为结构性辅助,不做块操作向新工具的扩张。
- [x] 不优先增加新的编辑器 UI 大功能。
- [x] 不优先新增 AI Agent 工作流功能。
- [x] 不优先扩展 AI 功能面;AI 只做基础上下文、工具合同、审阅会话、冲突与回滚。
- [x] 不优先大规模替换 Convex。
- [x] 不优先重写 `leptos-tiptap` 输入层。
- [x] 不把 `mnote.page.save` 包装成精确块编辑长期方案。
- [x] 不在 compat route 继续扩写长期业务语义。
- [x] 不把 BlockNote / Tiptap AI runtime 作为 mnote runtime 依赖。
- [x] 不把 `direct_block_edit_operations` / `local_rule` planner 当作当前或长期 AI 编辑路径;简单正文编辑当前主路径是 `mnote.doc.markdown_edit`
- [x] 不新增 `mnote.block.*` 工具;`mnote.block.*` 保留为结构性辅助,不做块操作向新工具的扩张。
2026-05-18 静态边界核验证据:
- `rg -n "mnote\\.block\\.[a-zA-Z0-9_]+" rust/crates/mnote-web/src/hermes_tools rust/crates/mnote-web/src/routes/hermes_tools.rs scripts/reasonix-acp-wrapper.mjs | ... | sort -u` 仅发现既有 `mnote.block.fetch/delete/replace/insert_after/move_after`,本轮未新增 `mnote.block.*` 工具。
- `page_ai_workflow.rs` 注释与当前实现仍指向 `mnote.doc.markdown_edit``direct_block_edit_operations` 只保留为历史/测试辅助,不作为当前主路径。
- `mnote.page.save` 在 manifest / 7-10 / 7-12 / 本 checklist 中继续描述为页面级粗粒度兜底,不是精确块编辑主入口。
- 未引入 `@blocknote/xl-ai`、BlockNote `AIExtension` 或新的 AI Agent workflow;本轮改动仅限 Hermes tools 合同、阻断矩阵测试和 checklist 证据。
2026-05-18 `7-12` runtime 口径闭合证据:
- `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 已明确 `7-11` 旧“自有 AI runtime”口径移入 `design/old/07-ai/process/`,当前执行口径以 `7-12` 为准。
- `7-12` 已定义 `PageAIContextBuilder``MnoteAIToolManifestProvider``PageAICommandRouter``PageAIReviewSession` 的边界,并明确 Phase C review / streaming apply 只做设计冻结,当前不实施。
- `7-12` Phase B 到 Phase F 保留 manifest / router / review session / event / smoke 的后续 checklist;其中 selection 外写入 blocked 已有真实 smoke 证据,其余未实施项继续保留未勾。
2026-05-18 `7-10` 剩余真实页面矩阵拆分:
- 新增 `design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md`,专门承接 `7-10` 中仍未完成的真实 3000 smoke 和 UI/review 边界矩阵。
- `7-10` 继续保留为页面块 AI 工具执行总 checklist`7-16` 只用于后续真实页面证据补齐,不扩新 AI 功能。
2026-05-18 真实页面 insert_after smoke 补充:
- `scripts/task-page-block-ai-tools-smoke.js` 增加 `PLAYWRIGHT_CHROME_EXECUTABLE` 支持,用系统 Chromium 运行,避免当前系统无法下载 Playwright 官方 chromium 的阻断。
- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js`
- 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- Page Aggregate`tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json`doc/fetch/find`tmp/hermes-tester/page-block-ai-tools-mpag4966/doc-fetch-find.json`
- 覆盖:`mnote.block.insert_after` 插入 `todo` 块、插入顺序、`mnote.block.fetch` 回读新块、重复 `idempotencyKey` 不二次插入、刷新后文本可见。
- 覆盖:`table_1/mindmap_1/resource_1` 在 AI block projection 中返回 `editable=false``unsupportedReason=复杂块暂不开放 AI 精确写入``heading_parent/list_item_1/table_1/mindmap_1/resource_1``plan_update block_move_after` dry-run 与正式 `mnote.block.move_after` 均阻断,阻断后 revision / 正文不变化。
2026-05-18 真实页面 doc.fetch / doc.find / block.fetch smoke 补充:
- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js`
- 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`doc.fetch full``doc.fetch outline``doc.find` 定位 `p_2``maxBlocks=2` 截断、`block.fetch includeChildren=true contextBefore=1 contextAfter=1` 返回 `revisionRef` 与同父级 before/after;复杂块投影不伪装完全可编辑。
2026-05-18 真实页面 plan_update / replace / move smoke 补充:
- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js`
- 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`
- 覆盖:`block_replace/block_insert_after/block_move_after` dry-run、dry-run 不写入、blocked 场景、replace 相邻块不变与 id 保持、replace 后 Page Aggregate 回读持久化、复杂块 move 阻断、普通块 move dry-run 与正式移动、moving block id 保持、刷新后 block ids 稳定。
---
@@ -514,18 +588,18 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:300
本 checklist 不能迁入 `done/`,直到:
- [ ] Page Aggregate 的标题 / 正文 / 页面设置 / page tree / AI fetch 回读形成同一组可验证真相。
- [ ] `7-10` 的 Page Block AI Tooling 剩余矩阵完成或明确拆出后续 process 文档。
- [ ] `7-11` 的 AI 基础 runtime 口径完成:context、format、manifest annotations、review session、状态机边界都有代码或明确后续 checklist。
- [ ] 7-14 v2 Phase A + B 全部完成(`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强 + `page_ai_workflow.rs` 收口);Phase C 设计已冻结。设计稿修正已由 7-14 v2 自行完成。
- [ ] `tree.*` command 面对主要页面生命周期动作成为唯一 preferred runtime 主链。
- [ ] tree realtime live cache 覆盖 Sidebar、Page Tree、File Tree、page subtree 的关键写后更新。
- [ ] 至少一轮定向 bug hunt 完成,所有 P0/P1 blocker 已归档到 `bugs/*/done/` 或明确保留为后续 process。
- [x] Page Aggregate 的标题 / 正文 / 页面设置 / page tree / AI fetch 回读形成同一组可验证真相。
- [x] `7-10` 的 Page Block AI Tooling 剩余矩阵完成或明确拆出后续 process 文档。
- [x] `7-12` 的 AI 基础 runtime 口径完成:context、format、manifest annotations、review session、状态机边界都有代码或明确后续 checklist。
- [x] 7-14 v2 Phase A + B 当前主路径完成(`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强 + `page_ai_workflow.rs` 收口);Phase C 设计已冻结。设计稿修正已由 7-14 v2 `7-18``7-25` bug 修复共同完成。
- [x] `tree.*` command 面对主要页面生命周期动作成为唯一 preferred runtime 主链。
- [x] tree realtime live cache 覆盖 Sidebar、Page Tree、File Tree、page subtree 的关键写后更新。
- [x] 至少一轮定向 bug hunt 完成,所有 P0/P1 blocker 已归档到 `bugs/*/done/` 或明确保留为后续 process。
---
## 6. 给后续 /goal 的持续执行 Prompt
```text
/goal objective: 持续执行 /mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md,按 P0 -> P1 顺序推进 MNOTE 架构收口、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md、design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md、design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源;2) tree command cutover3) tree realtime live cache4) AI 基础工具与审阅底座,`7-14` v2 方向优先做 Phase A`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强),Phase B 退役 `local_rule` planner + `page_ai_workflow.rs` 收口;Phase C(流式 apply + suggest/review)仅设计冻结不实施。同时补 context/selection/page_xml/tool annotations,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新本 checklist 和对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。
/goal objective: 持续核验 /mnt/Data1T/mnote/design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md 的已归档结论,按 P0 -> P1 顺序推进 MNOTE 架构防回归、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md、design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md、design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源防回归2) tree command cutover 防回归3) tree realtime live cache 防回归4) AI 基础工具与审阅底座,当前简单正文编辑主路径已是 `mnote.doc.markdown_edit` + `mnote.doc.fetch` + `page_ai_workflow.rs` 统一 tool executorPhase C(流式 apply + suggest/review)仅设计冻结不实施;继续补 context/selection/page_xml/tool annotations,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 scoped git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。
```
@@ -1,6 +1,6 @@
# 09 页面 AI 快速块编辑 Runtime Review
> 状态:`process`
> 状态:`done`
>
> 日期:2026-05-16
>
@@ -12,6 +12,19 @@
---
## 0. 归档说明
本文件是 2026-05-16 的页面 AI fast block edit 历史审查快照。其记录的 `local_rule` / `doc_apply_block_ops` 快路径已被后续 `mnote.doc.markdown_edit` 主路径替代,不再作为当前 runtime 口径。
当前有效口径见:
- [10-current-mnote-ai-runtime-review-v1](./10-current-mnote-ai-runtime-review-v1.md)
- [11-current-full-architecture-review-v1](./11-current-full-architecture-review-v1.md)
- [7-18 AI markdown_edit 阶段状态合同漂移](../../../bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md)
- [7-20 page_ai_workflow 绕过 Hermes tool executor / audit / toggle](../../../bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md)
归档后的结论:`page-ai/block-edit-workflow` 仍是简单编辑 fast-path 入口,但当前实现应通过模型生成 markdown search/replace / full_content 后调用 `mnote.doc.markdown_edit`,并复用统一 mnote tool executor`mnote.doc.apply_block_ops` / `mnote.block.*` 只作为结构性块操作辅助。
## 1. 本轮结论
页面 AI 块写入的慢点不在 Rust 块工具本身,也不在 Convex 持久化本身。
@@ -1,22 +1,24 @@
# 10 [process] 当前 mnote 项目 AI / Page Aggregate 定向 Review v1
# 10 [done] 当前 mnote 项目 AI / Page Aggregate 定向 Review v1
> 更新时间:2026-05-17
>
> 执行状态:`process`
> 执行状态:`done`
>
> 范围:当前主线中 Page Aggregate 单一真源、页面 AI 快速编辑、`mnote.doc.markdown_edit` 与 ACP / Hermes runtime 的源码级定向审查。
## 1. 本轮结论
本轮没有发现 Page Aggregate 读取主链重新回退到 Next compat builder 的证据`wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 仍只走 Rust `/api/page-aggregate/:documentId``/api/documents/page` compat 读链保持 `410` 退场口径
本轮没有发现 Page Aggregate 读取主链重新回退到 Next compat builder 的证据。后续修复已进一步收口:AI context 的 page subtree 只读 Rust Page Aggregate projection,不再由前端本地构造第二份 page tree 真相
新的风险集中在 `07-ai` markdown 编辑收敛实现
本轮识别出的 `07-ai` markdown 编辑 P0 风险已修复
- `mnote.doc.markdown_edit` 已进入实际写入主线,并被 `/api/page-ai/block-edit-workflow` 调用。
- 该工具当前先在 markdown 字符串上顺序执行 search/replace,再把原始 operations 重新转换成 `mnote.doc.apply_block_ops`
- 转换层仍按“单块文本包含 search”查找目标块,无法忠实表达 markdown 层已经算出的最终结果
- `mnote.doc.markdown_edit` 是简单正文编辑主路径,并被 `/api/page-ai/block-edit-workflow` 通过统一 mnote tool executor 调用。
- 中文归一化匹配不再把 UTF-8 byte index 当 char index
- 同一块内多次 markdown_edit 会合并到最终 markdown 结果后再映射写回,避免后续 block op 覆盖前序修改
- 在线写回在无法安全映射最终 markdown 时明确拒绝,避免 `ok=true` 伪成功。
- Hermes guidance、manifest、tool toggle、dryRun / idempotency、revision / conflictDetectionKey 与 Reasonix ACP payload 已同步到当前工具合同。
因此,当前 AI 编辑主线不能只看 `operationsApplied > 0` 或 route 返回 `ok=true`。需要优先补充 `markdown_edit` 的中文、多操作、同块、多块、full_content 与失败原子性测试
剩余长期方向仍是 Phase C review session / streaming apply;当前设计冻结,不属于本轮 bug 修复范围
## 2. 发现的问题
@@ -24,7 +26,7 @@
关联缺陷:
- `bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md`
- `bugs/07-ai/done/7-16-markdown-edit-normalized-search-byte-index-v1.md`
证据:
@@ -41,7 +43,7 @@
关联缺陷:
- `bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md`
- `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md`
证据:
@@ -58,18 +60,20 @@
- AI 一次请求中常见的“把同一段里的 A 改成 B,同时把 C 改成 D”可能只保留最后一次修改。
- 前端快路径仍可能显示“已通过页面 markdown 编辑快路径完成写入”,但正文只部分生效。
## 3. 次级风险
## 3. 修复状态
- `doc_markdown_edit` 当前允许部分 operation 失败后继续写入已成功的子集,并返回 `ok=true`。如果这是有意设计,需要在 manifest / UI 中明确“非原子”;如果不是,应改为任一 operation 失败时不写入
- `changedText` 通过 `operations.iter().take(applied)` 生成摘要;当前如果第一个 operation 失败、第二个成功,摘要会错误地展示第一个失败 operation
- `/api/page-ai/block-edit-workflow` 的系统 prompt 已改成 search/replace,但仍强依赖模型精确复制 `pageText`;一旦模型输出跨块片段,Convex 写回层不能表达该编辑
- `mnote.doc.markdown_edit` manifest 已补齐 required / write contract
- 本地 `.md` 写入已尊重 dryRun / idempotency
- 在线写回已使用最终 markdown 作为真源进行映射;无法安全映射时返回明确错误
- `mnote.doc.apply_block_ops` 批量写入已补 revision / conflictDetectionKey 前置校验。
- `/api/page-ai/block-edit-workflow` 已从绕过 executor 改为复用统一 Hermes mnote tool executor。
- ACP Reasonix wrapper 已透传 actor/session/run/toolCall/trace/dryRun/idempotency/workspace/document 等上下文字段。
## 4. 建议下一步
## 4. 后续边界
1. 先给 `search_replace` 补中文归一化替换单测,再修正 byte / char offset 映射
2. `doc_markdown_edit` 补同一 block 多 operation 的失败用例,明确应以最终 markdown 生成落库内容,或在转换层合并同块操作
3. 明确 `markdown_edit` 的失败原子性:默认建议任一 operation 失败时不写入,除非请求显式允许 partial apply
4. 将 `/api/page-ai/block-edit-workflow` 的验收从“route 成功”提升为“Page Aggregate 回读与预期最终 markdown 一致”。
1. Phase C 的 review session / streaming apply 仍冻结,当前不扩新 AI 功能
2. 复杂结构编辑继续走 `apply_block_ops` / `mnote.block.*`,不要把正文 search/replace 回退到结构性块工具
3. Page Aggregate 仍需后续推进 EditorBlockDocument 原生落库真相
## 5. 本轮验证
@@ -80,9 +84,15 @@ rg -n "markdown_edit|apply_block_ops|block-edit-workflow|local_rule|page_ai_work
rg -n "page-aggregate|pageAggregate|PageAggregate|blockDocument|projectionSource|documents/save|documents/options|/api/page-aggregate" rust/crates/mnote-web rust/crates/bridge-runtime wolai-frontend/src scripts --glob '!rust/target/**' --glob '!node_modules/**'
```
本轮是 review / bug hunt,没有修改 runtime 源码。后续修复应至少补
修复后定向验证
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow
cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_client -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_session_manager -- --nocapture
node --check scripts/reasonix-acp-wrapper.mjs
MNOTE_REASONIX_ACP_SELFTEST=1 node scripts/reasonix-acp-wrapper.mjs
```
结果:上述验证均通过。
@@ -0,0 +1,53 @@
# 11 [done] 当前完整架构 Review v1
> 更新时间:2026-05-17
>
> 状态:`done`
>
> 参考总文档:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)
## 1. 结论
当前 mnote 的主线架构已经成形。本轮 review 识别出的 P0 / 内核优先缺陷已完成代码修复、文档迁移和定向验证。
已修复的四个关键冲突:
1. Tree realtime 的发布口径与前端消费口径不一致。
2. FileTree 资源行误走页面命令。
3. Page Aggregate / AI context 保留前端第二真相。
4. AI 写入链路在 `markdown_edit``apply_block_ops`、Hermes / ACP / Reasonix 之间的合同漂移。
仍需后续主线继续推进的不是本轮 P0 bug,而是长期架构收口:Page Aggregate 原生 EditorBlockDocument 落库、正文保存主命令面、`tree.resource.*` 资源生命周期和兼容路由瘦身。
## 2. 关键判断
- Rust kernel 与 bridge-runtime 已经承担语义主导权。
- Rust SSR 主壳已默认以 `/api/realtime/ws` 为实时主链,SSE 是 fallback。
- FileTree 资源行的 owner document 与页面命令目标已分离。
- AI context 的 page subtree 已只读 Rust Page Aggregate projection。
- `mnote.doc.markdown_edit` 已是简单正文编辑主路径,`page_ai_workflow` 复用统一 mnote tool executor`apply_block_ops` / `mnote.block.*` 仅作为结构性辅助。
- ACP / Reasonix 的 mnote tool payload、response、run context 与幂等字段已同步到当前工具合同。
## 3. 已闭合 bug
- Rust Web / realtime / ACP`bugs/03-rust-web/done/3-16``3-21`
- Tree domain`bugs/04-tree-domain/done/4-46`
- Editor mainline`bugs/05-editor-mainline/done/5-15``5-18`
- AI`bugs/07-ai/done/7-18``7-25`
## 4. 验证
- `find bugs -path '*/process/*' -type f | wc -l`
- 结果:`0`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture`
- 结果:9 passed
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture`
- 结果:3 passed
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_client -- --nocapture`
- 结果:6 passed
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests -- --nocapture`
- 结果:14 passed
## 5. 备注
本次审查的详细架构说明已统一写入根目录:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)。
@@ -1,34 +0,0 @@
# 11 [process] 当前完整架构 Review v1
> 更新时间:2026-05-17
>
> 状态:`process`
>
> 参考总文档:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)
## 1. 结论
当前 mnote 的主线架构已经成形,但还没有完成单一真源收口。最关键的未闭合点有四个:
1. Tree realtime 的发布口径与前端消费口径不一致。
2. FileTree 资源行仍可能误走页面命令。
3. Page Aggregate 仍保留前端第二真相。
4. AI 写入链路在 `markdown_edit``apply_block_ops`、Hermes / ACP / Reasonix 之间存在合同漂移。
## 2. 关键判断
- Rust kernel 与 bridge-runtime 已经承担语义主导权。
- 前端仍保留若干本地派生状态,适合过渡,不适合作为长期真相层。
- `mnote.doc.markdown_edit` 已是主推路径,但工具级 contract 还没收口。
- `page_ai_workflow` 与 ACP / Reasonix 已接入主链,但运行时 owner 与写入字段不统一。
## 3. 需要继续跟踪的 bug
- Rust Web / realtime / ACP`bugs/03-rust-web/process/3-16``3-21`
- Tree domain`bugs/04-tree-domain/process/4-46`
- Editor mainline`bugs/05-editor-mainline/process/5-15``5-18`
- AI`bugs/07-ai/process/7-18``7-25`
## 4. 备注
本次审查的详细架构说明已统一写入根目录:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)。