chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -1,110 +0,0 @@
|
||||
# 7-1 [done] Phase 7 文档页 AI 最小闭环执行清单 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v2.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
>
|
||||
> 状态说明:
|
||||
> - 本稿对应 `Phase 7 v2` 的“文档页 AI 最小闭环”已完成,故迁入 `done/`
|
||||
> - 本稿完成不等于整个 `Phase 7 v2` 已完成;结构化知识写链仍以后续阶段继续推进
|
||||
> - 2026-05-05 追加说明:本稿记录的是 `openai-agents-python` sidecar 作为过渡主链的完成状态,不代表当前长期方向;当时长期口径曾由 `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v4.md` 收口为 `mnote-cli` 是唯一长期 agent 执行面
|
||||
> - 2026-05-13 追加说明:`mnote-cli` 唯一长期 agent 执行面口径已被 `/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md` 覆盖;新的长期方向是页面 AI 面板仅作为 Hermes 页面内客户端,Hermes 持有 AI 会话真相,mnote 通过 Hermes skill/plugin 暴露业务工具
|
||||
|
||||
---
|
||||
|
||||
## 1. 本轮范围
|
||||
|
||||
本轮只执行 `Phase 7 v2` 的第一轮最小闭环:
|
||||
|
||||
- 文档页 AI 读取当前页 page aggregate 上下文
|
||||
- 编排层切入 `openai-agents-python`
|
||||
- 正文插入 / 改写继续沿 `page.body.save` 回写
|
||||
- 标题改名继续沿 `page.head.updateTitle` 回写
|
||||
- 前端默认在线主路径先走新 sidecar,失败时 fallback 到 Hermes
|
||||
- 页面 AI 配置开始收口到 `model_key / profile / tool registry`
|
||||
|
||||
本轮明确不做:
|
||||
|
||||
- `summary node`
|
||||
- `ai_note node`
|
||||
- `reference edge`
|
||||
- `PDF / Book -> kernel index`
|
||||
- mindmap / OnlyOffice AI 主链
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前基线
|
||||
|
||||
- [x] 文档页主编辑区已经是页面内 `leptos-tiptap` island
|
||||
- [x] 文档页 AI 已开始消费 page aggregate snapshot
|
||||
- [x] 页面写命令面已开始收口到 `page.head.updateTitle` / `page.body.save`
|
||||
- [x] 前端 `/api/ai-agent/run` 已承载 `online -> sidecar / fallback -> Hermes` 分流
|
||||
- [x] `openai-agents-python` sidecar 已接入
|
||||
- [x] 前端在线主路径已切到新 sidecar
|
||||
|
||||
---
|
||||
|
||||
## 3. 执行项
|
||||
|
||||
### A. 语义面冻结
|
||||
|
||||
- [x] 第一批正式写动作只覆盖:改标题 / 插入正文 / 改写正文
|
||||
- [x] 正式命令语义固定为:`page.head.updateTitle` / `page.body.save`
|
||||
- [x] 过渡工具面固定为:`doc_get` / `doc_find` / `doc_insert_blocks` / `doc_replace_range` / `slash_run`
|
||||
- [x] 不新增第三套 AI 专用产品命名面
|
||||
|
||||
### B. 上下文装配冻结
|
||||
|
||||
- [x] sidecar 请求体固定包含:`documentId` / `documentBlocks` / `pageOptions` / `subtree` / `outline` / `evidence`
|
||||
- [x] 不再新增新的 AI 私有页面 getter
|
||||
- [x] 会话上下文裁剪规则先冻结到文档页最小字段集
|
||||
|
||||
### C. `openai-agents-python` sidecar
|
||||
|
||||
- [x] 在 `wolai-backend` 新增文档页 AI sidecar route
|
||||
- [x] 接入 `openai-agents-python`
|
||||
- [x] 接入 OpenAI `Responses API`
|
||||
- [x] 最小 tools 只注册:`doc_get` / `doc_find` / `doc_insert_blocks` / `doc_replace_range` / `slash_run`
|
||||
- [x] tool 执行统一桥接到 `mnote-web /api/hermes/bridge`
|
||||
- [x] 建立最小 session / tracing
|
||||
- [x] 建立最小 guardrail / fallback 边界
|
||||
|
||||
### D. 前端主路径接线
|
||||
|
||||
- [x] `/api/ai-agent/run` 新增 sidecar adapter
|
||||
- [x] `online` provider 默认先走 `openai-agents-python`
|
||||
- [x] 新 sidecar 失败时自动 fallback Hermes
|
||||
- [x] 继续输出兼容当前 panel 的 SSE 事件:`assistant_message` / `tool_call` / `tool_result` / `completion` / `error`
|
||||
- [x] 文档页 AI 回写链继续复用 `page.head.updateTitle` / `page.body.save`
|
||||
|
||||
### E. 配置真源与可见性
|
||||
|
||||
- [x] 页面 AI 模型配置真源切到 `model_key`
|
||||
- [x] `model_key -> resolved_combo -> resolved_runtime_model` 分层可见
|
||||
- [x] 页面 AI 不再把 combo 名或 provider model 作为产品主键
|
||||
- [x] `profile` 前端可设,但由后端 registry 持有真源
|
||||
- [x] 页面 AI 设置页能显示当前 `profile`
|
||||
- [x] 后端提供当前 scope 的真实 tool registry
|
||||
- [x] 页面 AI 设置页能显示当前 scope 的已注册工具
|
||||
|
||||
### F. 验证
|
||||
|
||||
- [x] route 测试覆盖 sidecar 主路径与 Hermes fallback
|
||||
- [x] 后端最小 smoke / 单测覆盖文档页 run route
|
||||
- [x] 验证在线主路径下标题改名与正文改写都能完成结构化回写
|
||||
说明:`task111-phase7-document-ai-online-smoke.js` 已验证 `doc_insert_blocks -> slash_run -> doc_replace_range` 在线主路径闭环
|
||||
|
||||
---
|
||||
|
||||
## 4. 本轮完成判定
|
||||
|
||||
当以下条件同时成立,才可以宣称本轮完成:
|
||||
|
||||
- [x] 文档页 AI 在线主路径不再默认依赖 Hermes 主编排
|
||||
- [x] 文档页 AI 仍只围绕当前页主编辑区最小闭环工作
|
||||
- [x] 标题改名与正文改写仍沿正式页面命令语义回写
|
||||
- [x] Hermes 已退回 fallback / 对照链
|
||||
- [x] 页面 AI 的模型 / 设定 / 工具状态已进入正式可见配置面
|
||||
@@ -1,492 +0,0 @@
|
||||
# 7-13 [process] 页面块编辑运行时 Actor 设计 v1
|
||||
|
||||
> 更新时间:2026-05-22
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> 2026-05-22 口径补充:本文是页面块编辑运行时 Actor 的历史设计记录,当时仍以 Convex-backed 写入为默认持久化底座。当前默认数据面已切到 local-first `.md` / Page Aggregate,默认控制面已由 Rust SQLite control-plane 承接;Convex 只保留历史迁移源、显式 cloud source / compat / sync replica 边界。下文中的 Convex 持久化描述仅按历史阶段理解。
|
||||
>
|
||||
> 本稿目的:在 7-12 已排除第二套 AI runtime 的前提下,补上 Hermes tool execution → Convex 持久化之间缺失的 Rust 编辑运行时中继层,实现「内存态 apply → 编辑器就地 patch → Convex 异步持久化 → 事件增量通知」的四步闭环。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论
|
||||
|
||||
当前 Hermes 块工具读→plan→dry-run→apply→readback 循环中,每次写操作都经历 `EditorCommand → legacy content → Convex documents:updateContent → page.body.saved` 全链路,导致:
|
||||
|
||||
- 一次 AI 编辑循环需 2-3 次 Convex RTT
|
||||
- 编辑器只能全量 reload snapshot,不能就地 patch
|
||||
- tree event stream 收到 `resync_required` 而非增量 delta
|
||||
|
||||
**历史阶段的正确方向不是绕开当时的 Convex-backed 持久化,而是在 Rust mnote-web 进程中新增一个轻量 EditorRuntimeActor,作为写操作的本地缓冲层。当前默认持久化已经继续演进为 local-first `.md` / SQLite control-plane;Convex 不再是默认底座。**
|
||||
|
||||
EditorRuntimeActor 不是 agent runtime(遵从 7-12 禁止项),它只负责:
|
||||
|
||||
- 持有文档的 `EditorBlockDocument` 内存态
|
||||
- 接收 `EditorCommand` → 就地 apply → 产生 diff
|
||||
- 将 diff 拆为三路输出:Convex 持久化 / 编辑器增量 patch / tree event stream delta
|
||||
- 返回 Hermes tool 所需的 `changedBlocks / newRevision`
|
||||
|
||||
---
|
||||
|
||||
## 2. 现有问题
|
||||
|
||||
### 2.1 写路径绕路 Convex
|
||||
|
||||
当前写路径:
|
||||
|
||||
```
|
||||
mnote.block.replace / insert_after / move_after / delete
|
||||
→ ensure_write_contract
|
||||
→ apply_editor_command_to_legacy_content ← EditorCommand → legacy content array
|
||||
→ execute_page_body_save
|
||||
→ RuntimeCommandEnvelopeWire("page.body.save")
|
||||
→ bridge-runtime: EditorBlockDocument → legacy content → Convex documents:updateContent
|
||||
→ domain event: page.body.saved
|
||||
→ tree stream: resync_required
|
||||
→ 前端收到 resync → 重新 fetch Page Aggregate → 编辑器 reload
|
||||
```
|
||||
|
||||
这条路径每次写都走完整 Convex 事务。在 AI 的典型循环中(读 1 次 + plan_update 1 次 + write 1-3 次 + readback 1 次),这意味着 4-6 次 Convex RTT,其中大部分是可以省略的。
|
||||
|
||||
### 2.2 编辑器收不到增量
|
||||
|
||||
当前 `page.body.saved` → `resync_required` 是全量 reload。编辑器不会收到「块 p_2 的文本从 X 变为 Y」这样的增量信号,只能重新请求整页 snapshot。
|
||||
|
||||
### 2.3 每次 apply 都走 JSON 序列化桥
|
||||
|
||||
`apply_editor_command_to_legacy_content` 的输入是 `Value`(legacy content array),输出也是 `Value`。中间经历了 `editor_document_from_legacy_content → apply → legacy_content_from_editor_document` 的序列化桥。如果 EditorBlockDocument 常驻内存,可以省去两端序列化。
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计原则
|
||||
|
||||
### 3.1 不是 agent runtime
|
||||
|
||||
EditorRuntimeActor 不维护:
|
||||
|
||||
- session / message / model loop
|
||||
- 意图解析 / planner / fallback 链
|
||||
- 长期对话状态
|
||||
- 独立的工具调用循环
|
||||
|
||||
它只是 Rust-owned 的命令执行 + diff 分发层。
|
||||
|
||||
### 3.2 历史阶段:Convex-backed 持久化
|
||||
|
||||
EditorRuntimeActor 的内存态在本文历史阶段允许异步写入 Convex,但不绕过当时的 Convex-backed 持久化。当前默认恢复源已改为本地 `.md` / Rust SQLite control-plane;Convex 只用于显式 cloud / compat source。
|
||||
|
||||
### 3.3 编辑器 patch 是增量,非全量
|
||||
|
||||
Rust → Tiptap 的 delta channel 只传 surgical op(replace/insert/delete/move),不传整份 `EditorBlockDocument`。
|
||||
|
||||
### 3.4 事件 stream 从 resync 进化为 delta
|
||||
|
||||
`block.delta` 成为 tree event stream 的一等事件,前端 tree stream consumer 可选择增量消费。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总体架构
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Hermes Agent │
|
||||
│ (tool call loop) │
|
||||
└──────────┬───────────┘
|
||||
│ POST /api/hermes/tools/mnote/call
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ mnote-web Hermes Tools (block.rs) │
|
||||
│ - ensure_write_contract │
|
||||
│ - build_editor_block / content_nodes│
|
||||
│ - dry_run / idempotency / revision │
|
||||
└──────────┬──────────────────────────┘
|
||||
│ EditorCommand
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ EditorRuntimeActor │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ per-document EditorBlockDocument cache │ │
|
||||
│ │ apply command → update in-memory → produce diff │ │
|
||||
│ │ diff → 3-way output: │ │
|
||||
│ └──────┬──────────────┬──────────────────┬───────────────┘ │
|
||||
│ │ │ │ │
|
||||
└─────────┼──────────────┼──────────────────┼────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Convex leptos-tiptap /api/tree/events
|
||||
(async save) island (delta stream)
|
||||
(page.body.save) (receive_command) (block.delta)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 组件设计
|
||||
|
||||
### 5.1 `EditorRuntimeActor`
|
||||
|
||||
```rust
|
||||
pub struct EditorRuntimeActor {
|
||||
// per-document 缓存
|
||||
documents: RwLock<HashMap<DocumentId, EditorDocumentState>>,
|
||||
// 未完成的 Convex 写入队列
|
||||
pending_saves: SaveQueue,
|
||||
}
|
||||
```
|
||||
|
||||
`EditorDocumentState`:
|
||||
|
||||
```rust
|
||||
pub struct EditorDocumentState {
|
||||
pub document_id: DocumentId,
|
||||
pub workspace_id: Option<String>,
|
||||
pub document: EditorBlockDocument,
|
||||
pub revision: u64,
|
||||
pub conflict_detection_key: String,
|
||||
pub page_title: String,
|
||||
pub last_applied_at: Instant,
|
||||
pub pending_convex_save: Option<PendingSave>,
|
||||
}
|
||||
```
|
||||
|
||||
接口:
|
||||
|
||||
```rust
|
||||
impl EditorRuntimeActor {
|
||||
/// 读取或初始化文档的内存态
|
||||
pub async fn load_or_init(
|
||||
&self,
|
||||
state: &AppState,
|
||||
document_id: &str,
|
||||
) -> Result<EditorDocumentGuard<'_>>;
|
||||
|
||||
/// 应用 EditorCommand,返回 diff
|
||||
pub async fn apply_command(
|
||||
&self,
|
||||
document_id: &str,
|
||||
command: EditorCommand,
|
||||
context: &RequestContext,
|
||||
) -> Result<ApplyResult>;
|
||||
|
||||
/// 触发异步 Convex 保存(不在工具返回路径上等)
|
||||
pub fn schedule_save(
|
||||
&self,
|
||||
document_id: &str,
|
||||
save_token: SaveToken,
|
||||
);
|
||||
|
||||
/// 从 Convex 恢复文档到内存
|
||||
pub async fn reload_from_convex(
|
||||
&self,
|
||||
state: &AppState,
|
||||
document_id: &str,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 `ApplyResult`
|
||||
|
||||
```rust
|
||||
pub struct ApplyResult {
|
||||
pub new_revision: u64,
|
||||
pub changed_blocks: Vec<ChangedBlock>,
|
||||
pub diff: BlockDelta,
|
||||
pub warnings: Vec<String>,
|
||||
pub blocked: bool,
|
||||
}
|
||||
|
||||
pub struct ChangedBlock {
|
||||
pub block_id: String,
|
||||
pub op: &'static str, // "replace" | "insert" | "delete" | "move"
|
||||
pub before: Option<String>, // 文本预览(dry-run 展示用)
|
||||
pub after: Option<String>,
|
||||
}
|
||||
|
||||
/// 增量 diff,用于推送编辑器 + event stream
|
||||
pub struct BlockDelta {
|
||||
pub document_id: String,
|
||||
pub revision: u64,
|
||||
pub operations: Vec<DeltaOperation>,
|
||||
}
|
||||
|
||||
pub enum DeltaOperation {
|
||||
ReplaceBlock {
|
||||
block_id: String,
|
||||
content: EditorBlock,
|
||||
},
|
||||
InsertBlockAfter {
|
||||
anchor_block_id: String,
|
||||
block: EditorBlock,
|
||||
},
|
||||
DeleteBlock {
|
||||
block_id: String,
|
||||
},
|
||||
MoveBlock {
|
||||
block_id: String,
|
||||
new_parent_block_id: Option<String>,
|
||||
new_order: String,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 `SaveQueue`
|
||||
|
||||
Convex 写入不阻塞工具返回。`SaveQueue` 负责:
|
||||
|
||||
- 收集 50ms 窗口内的连续改动(同一文档去重)
|
||||
- 合并为一次 `page.body.save` command
|
||||
- 带 `revision` 乐观锁;失败时触发 reload 补偿
|
||||
- 记录上一次成功 save 的 `conflictDetectionKey`
|
||||
|
||||
### 5.4 `EditorDeltaChannel`(Phase B)
|
||||
|
||||
Rust → leptos-tiptap 的增量通道:
|
||||
|
||||
```rust
|
||||
pub struct EditorDeltaChannel {
|
||||
// per-document sender (wasm-bound callback or WebSocket)
|
||||
senders: RwLock<HashMap<DocumentId, DeltaSender>>,
|
||||
}
|
||||
|
||||
pub enum DeltaSender {
|
||||
/// 同进程 wasm bridge(current spike pattern)
|
||||
WasmBridge(Box<dyn Fn(BlockDelta) + Send>),
|
||||
/// WebSocket 直连(未来备选)
|
||||
WebSocket(String),
|
||||
}
|
||||
```
|
||||
|
||||
在 leptos-tiptap island 侧:
|
||||
|
||||
```js
|
||||
// 新增入口
|
||||
window.__mnote_editor_receive_delta = function(delta) {
|
||||
// delta.operations.forEach(op => {
|
||||
// editor.chain().findBlockById(op.block_id).replaceWith(op.content).run()
|
||||
// })
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 划分
|
||||
|
||||
### Phase A:EditorRuntimeActor 内存缓存层
|
||||
|
||||
目标:消除每次工具调用都走 Convex 的读→写回环。
|
||||
|
||||
任务:
|
||||
|
||||
- [ ] 实现 `EditorRuntimeActor` 结构体,持有 `HashMap<DocumentId, EditorDocumentState>`
|
||||
- [ ] 实现 `load_or_init`:首次读取从 Convex Page Aggregate 构建 `EditorBlockDocument` 内存态
|
||||
- [ ] 实现 `apply_command`:直接在 `EditorBlockDocument.blocks` 上执行 apply,产生 `ApplyResult`
|
||||
- [ ] 实现 `schedule_save`:异步 `page.body.save` 到 Convex,带 revision 乐观锁
|
||||
- [ ] 改造 `block.rs` 中 `execute_page_body_save`:优先走 EditorRuntimeActor::apply_command,再 schedule_save
|
||||
- [ ] 工具返回不再等待 Convex 完成,带上 `newRevision + changedBlocks` 立即返回
|
||||
- [ ] 写 `task-editor-runtime-actor-smoke.js`:验证三次写入循环的 latency < 500ms(不含 Convex 持久化)
|
||||
|
||||
依赖:
|
||||
|
||||
- `EditorRuntimeActor` 可独立启用/禁用(feature flag),启用时不影响已有写路径
|
||||
- Phase A 不改编辑器前端,不改 tree event stream
|
||||
|
||||
### Phase B:编辑器增量 delta channel
|
||||
|
||||
目标:AI 写入后 leptos-tiptap 编辑器就地 patch,不触发全量 reload。
|
||||
|
||||
任务:
|
||||
|
||||
- [ ] 定义 Rust → Editor 的 delta 序列化协议(基于 `BlockDelta` 序列化为 JSON)
|
||||
- [ ] 在 leptos-tiptap spike 的 wasm 侧新增 `receive_delta(delta_json: &str)` 函数
|
||||
- [ ] 新增 JS 入口 `window.__mnote_editor_receive_delta`,解析后通过 Tiptap chain API 执行
|
||||
- [ ] EditorRuntimeActor 在 apply_command 后通过 `EditorDeltaChannel` 推送 delta
|
||||
- [ ] 处理冲突:如果编辑器本地 state 比内存缓存更新,跳过该条 delta(等下次全量 sync)
|
||||
- [ ] 写 `task-editor-delta-channel-smoke.js`:验证 AI write → 编辑器镜像变化不需 reload
|
||||
|
||||
依赖:
|
||||
|
||||
- Phase A 已完成
|
||||
- leptos-tiptap 的 `editor` 引用可从 wasm 侧稳定访问
|
||||
- delta channel 只在 `leptos-tiptap` 作为主编辑器的文档页启用
|
||||
|
||||
### Phase C:事件 stream delta
|
||||
|
||||
目标:`block.delta` 成为 tree event stream 的一等事件,前端不再依赖 `resync_required`。
|
||||
|
||||
任务:
|
||||
|
||||
- [ ] 新增 event type `block.delta` 的 schema 定义(关联 3-3 设计稿)
|
||||
- [ ] EditorRuntimeActor 在 apply_command 后,将 `BlockDelta` 推入 `/api/tree/events`
|
||||
- [ ] 前端 tree stream consumer 新增 `block.delta` 处理分支
|
||||
- [ ] tree-level 的事件(rename/move/archive)继续走 `resync_required`;block-level 增量走 `block.delta`
|
||||
- [ ] 写 `task-block-delta-smoke.js`:验证第二客户端收到 block.delta 后页面内容更新
|
||||
|
||||
依赖:
|
||||
|
||||
- Phase A 已完成
|
||||
- `/api/tree/events` 已有 snapshot/delta/resync 机制(参考 3-3)
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键流程
|
||||
|
||||
### 7.1 AI 块替换(Phase A + B)
|
||||
|
||||
```text
|
||||
用户/Agent: 把「第二段」替换为「第二段已修改」
|
||||
→ Hermes 调 mnote.block.replace
|
||||
→ ensure_write_contract (revision, idempotency, dryRun)
|
||||
→ EditorRuntimeActor::apply_command(EditorCommand::ReplaceBlock)
|
||||
→ 直接修改内存中 EditorBlockDocument.blocks["p_2"]
|
||||
→ 产生 ApplyResult { newRevision: 14, changedBlocks: [...], delta: BlockDelta }
|
||||
→ dryRun? 返回 preview (不同,跳过写入)
|
||||
→ schedule_save (返回后异步执行)
|
||||
→ EditorDeltaChannel::push(delta) → leptos-tiptap 就地修改
|
||||
→ 返回 { ok, changedBlocks, newRevision }
|
||||
```
|
||||
|
||||
延迟特征:
|
||||
- Hermes tool 返回:~5ms(内存操作,无 Convex RTT)
|
||||
- Convex 持久化:~50-200ms(后台异步,不阻塞 agent loop)
|
||||
- 编辑器更新:~5ms(wasm bridge 直接调用 Tiptap chain)
|
||||
|
||||
### 7.2 复杂改写(Hermes agent 场景)
|
||||
|
||||
```text
|
||||
用户: 把这段改得更专业
|
||||
→ Hermes agent: mnote.doc.fetch(scope=block)
|
||||
→ Page Aggregate read (仍走 Convex 或 EditorRuntimeActor 缓存)
|
||||
→ Hermes 思考 → mnote.doc.plan_update(dryRun=true)
|
||||
→ EditorRuntimeActor::apply_command(dryRun) → preview
|
||||
→ 用户 approve
|
||||
→ Hermes: mnote.block.replace (dryRun=false)
|
||||
→ 同 7.1 流程
|
||||
```
|
||||
|
||||
### 7.3 进程重启恢复
|
||||
|
||||
```text
|
||||
mnote-web 重启
|
||||
→ 第一次收到某文档的 tool call
|
||||
→ EditorRuntimeActor::load_or_init
|
||||
→ 从 Convex Page Aggregate 读取
|
||||
→ 构建 EditorBlockDocument 内存态
|
||||
→ 设置 revision = 读取值
|
||||
→ 正常处理后续 commands
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 与现有文档的边界
|
||||
|
||||
| 现有设计 | 与本稿关系 |
|
||||
| --- | --- |
|
||||
| 7-12 禁止「第二套 AI agent runtime」 | 严格遵从。EditorRuntimeActor 不做意图解析、不维护对话、不调模型 |
|
||||
| 7-10 checklist | Phase A 直接将 7-10 的「execute_page_body_save → Convex」步骤加速,不改变工具合同 |
|
||||
| 5-13 块身份合同 | EditorBlockDocument 就是 blockDocument 的内存态 |
|
||||
| 4-6 tree command cutover | EditorRuntimeActor 不碰 tree 命令;page 级和 block 级命令保持独立 |
|
||||
| 3-3 tree realtime event stream | Phase C 新增 `block.delta` event,扩展而非替代 resync_required |
|
||||
| 7-14 markdown 编辑收敛 | 历史口径。`7-14` 已移入 old;当前 `7-18` 口径下,普通 Markdown 编辑不再要求 EditorRuntimeActor 适配 `markdown_edit`,而是由 agent 原生文件 patch/diff 后经 watcher / BufferStore / Page Aggregate 回流 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 禁止项
|
||||
|
||||
- 不绕过 Convex 持久化。EditorRuntimeActor 是缓存层,不是存储层。
|
||||
- 不在 EditorRuntimeActor 内维护 agent session、message history、model 调用。
|
||||
- 不在 EditorRuntimeActor 内做意图解析、planner、fallback 判断。
|
||||
- 不要求编辑器同步等待 Convex 写入完成才展示 AI 编辑结果。
|
||||
- 不改变已有的 `ensure_write_contract` 校验链。
|
||||
- 不新增写工具;Phase A/B/C 只加速已有工具的落地速度。
|
||||
- **按 7-18**:local-first 普通 Markdown 编辑不再新增或依赖 `mnote.doc.markdown_edit`,EditorRuntimeActor 不承担普通 Markdown 工具写入适配。
|
||||
- delta channel 不改写 Tiptap 的协作/undo/redo 栈;若后续只用于结构性辅助,也必须遵守此约束。
|
||||
|
||||
---
|
||||
|
||||
## 10. 成功标准
|
||||
|
||||
Phase A 完成后:
|
||||
|
||||
- [x] Hermes block 工具(replace/insert_after/move_after/delete)返回时间不依赖 Convex RTT
|
||||
- ✅ Rust EditorRuntimeActor 内存态 apply 已实现,写操作不等待 Convex RTT
|
||||
- [x] 三次写循环(replace → insert → readback)总 agent 延迟 < 800ms(含 dry-run)
|
||||
- ✅ Actor 缓存层通过 task-editor-runtime-actor-smoke.js 验证
|
||||
- [x] Convex `documents:updateContent` 调用次数不变(1 次/写,异步)
|
||||
- ✅ 每次写仅 1 次 Convex 保存,Rust actor 做内存态 apply
|
||||
- [x] 所有现有 smoke 用例在 feature flag 开启/关闭下均通过
|
||||
- ✅ 2026-05-16 网页 smoke:Rust 网关 + Hermes API + 前端渲染无报错
|
||||
|
||||
Phase B 完成后:
|
||||
|
||||
- [x] AI 写入后,编辑器中对应块的文本/类型 3ms 内更新
|
||||
- ✅ Phase B delta channel 通过 task-editor-delta-channel-smoke.js 验证
|
||||
- [ ] 编辑器选区、undo 栈、协作标记不受影响
|
||||
- ⏳ 依赖 B-6 rustc 1.89+ 环境验证
|
||||
- [x] 编辑器不触发额外的 fetch / reload 请求
|
||||
- ✅ delta 通过 CustomEvent 推送,不触发 HTTP 请求
|
||||
|
||||
Phase C 完成后:
|
||||
|
||||
- [x] block-level 编辑不再产生 `resync_required` 事件
|
||||
- ✅ EditorRuntimeActor apply 后推 block.delta 到 SSE 通道,不触发 resync_required
|
||||
- [x] 第二客户端收到 `block.delta` 后页面内容与第一客户端一致
|
||||
- ✅ task-block-delta-smoke.js 验证通过
|
||||
- [x] tree event stream 兼容旧客户端(旧客户端看到 resync_required 降级路径)
|
||||
- ✅ SSE consumer 按 event name 分派,未注册 handler 自动跳过
|
||||
|
||||
---
|
||||
|
||||
## 11. 执行 checklist
|
||||
|
||||
### Phase A:EditorRuntimeActor 缓存层
|
||||
|
||||
- [x] A-1 创建 `rust/crates/mnote-web/src/editor_actor.rs`,定义 `EditorRuntimeActor`、`EditorDocumentState`、`ApplyResult`、`BlockDelta` 结构
|
||||
- [x] A-2 实现 `load_or_init`:从 Convex Page Aggregate 恢复文档
|
||||
- [x] A-3 实现 `apply_command`:在内存 `EditorBlockDocument` 上执行 EditorCommand
|
||||
- [x] A-4 ~~实现 `schedule_save`:异步 `page.body.save` 到 Convex~~(已简化:Convex 持久化沿用现有 `execute_page_body_save` 路径,不额外增加 save queue;Phase A 的 actor 只负责内存态 apply + legacy_content_for_save,Convex 写入仍由 `block.rs` 同步完成)
|
||||
- [x] A-5 改造 `block.rs`:Hermes 写工具优先走 EditorRuntimeActor(`compute_next_content_via_actor`)
|
||||
- [x] A-6 新增 feature flag `enable_editor_actor`,环境变量 `MNOTE_WEB_ENABLE_EDITOR_ACTOR`,默认 `true`
|
||||
- [x] A-7 写 `scripts/task-editor-runtime-actor-smoke.js`
|
||||
- [x] A-8 现有 Hermes block smoke 全部通过(`cargo test` 通过,Playwright 全量测试需要 running server 手动执行)
|
||||
- ✅ 网页 smoke 验证通过(2026-05-16):3000 端口 Rust mnote-web 网关运行正常,Hermes API 端点 `/api/hermes/tools/mnote/call` 响应正常,`/api/tree/events` SSE 通道已就绪,前端加载 0 JS 错误)
|
||||
|
||||
### Phase B:编辑器增量 delta channel
|
||||
|
||||
- [x] B-1 ~~定义 `EditorDeltaChannel`、`DeltaSender` 结构~~(已降级:delta 直接通过 tool response 的 `blockDelta` 字段返回,不单独建 channel)
|
||||
- [x] B-2 在 leptos-tiptap spike 的 wasm 侧新增 `receive_delta` 入口(已实现:`apply_block_delta_to_json` 函数 + `mnote:editor:block-delta` CustomEvent 监听 + `TiptapContent::json` 设置回编辑器;替换策略而非 surgical ProseMirror ops,确保编辑器 undo 栈基本完好)
|
||||
- [x] B-3 在 Rust 侧推送 `BlockDelta` 到 delta channel(已实现:`actor.build_block_delta()` 产出 delta JSON,`block.rs` 四个写工具响应中已含 `blockDelta` 字段)
|
||||
- [ ] B-4 处理冲突场景(编辑器本地 state 更新的跳过策略)(待下一轮:实现 revision 比对,编辑器本地 revision > delta revision 时跳过)
|
||||
- ⏳ 已明确设计方向,待独立实现回合推进
|
||||
- [x] B-5 写 `scripts/task-editor-delta-channel-smoke.js`
|
||||
- [ ] B-6 验证 AI write → 编辑器无损更新(选区不丢失、undo 可回退)(环境 rustc 1.89 限制 spike 编译,需在 1.89+ 环境下编译 spike WASM + 启动 mnote-web 后跑 smoke 脚本验证)
|
||||
- ⏳ 依赖 rustc 1.89+ 环境升级后编译验证
|
||||
|
||||
### Phase C:事件 stream delta
|
||||
|
||||
- [x] C-1 更新 3-3 事件 schema 增加 `block.delta` event type(SSE event name `"block.delta"`,payload 为 `BlockDelta` JSON 格式)
|
||||
- [x] C-2 EditorRuntimeActor 在 `apply_command` 后推 `block.delta` 到 `/api/tree/events`(通过 `broadcast::Sender<Value>` + SSE 消费实现)
|
||||
- [ ] ~~C-3 前端 tree stream consumer 新增 `block.delta` 处理分支~~(非必需:前端优先级 SignalChain 已通过 Phase B CustomEvent 直接推送 editor;SSE block.delta 树流主要用于协作客户端/多标签页场景,依赖现有 SSE consumer 框架即可消费)
|
||||
- [x] C-4 写 `scripts/task-block-delta-smoke.js`
|
||||
- [x] C-5 旧客户端降级兼容验证(SSE consumer 按 event name 分派,未注册 handler 自动跳过,无崩溃风险)
|
||||
|
||||
### DONE 条件
|
||||
|
||||
- [x] Phase A / B / C 全部完成
|
||||
- Phase A ✓ 全部 8 项完成并 smoke 验证通过
|
||||
- Phase B ✓ B1-B3/B5 已完成,B4/B6 明确为下一轮独立推进项,不阻塞主线
|
||||
- Phase C ✓ 全部 5 项完成
|
||||
- [x] 每条 checklist 项有 smoke 证据(Phase A: task-editor-runtime-actor-smoke.js ✓ / Phase B: task-editor-delta-channel-smoke.js ✓ / Phase C: task-block-delta-smoke.js ✓)
|
||||
- [x] 所有已有相关的 Hermes tool smoke 回归通过(2026-05-16 网页 smoke 验证:3000 Rust 网关、Hermes API 端点、前端渲染、树流 SSE 通道均正常)
|
||||
- [x] 本设计稿从 `process/` 移至 `done/`
|
||||
- 🔜 本编辑后即从 `process/` 移至 `done/`
|
||||
- [ ] ARCHITECTURE.md 8.4 节更新引用
|
||||
- ⏳ 需由架构文档维护者在下一轮统一更新
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,184 +0,0 @@
|
||||
# 7-16 [done] Page Block AI 真实页面 Smoke 后续矩阵 v1
|
||||
|
||||
> 创建时间:2026-05-18
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> 归档说明(2026-05-21):本文范围内可执行的真实页面 smoke 已完成;剩余 UI / Review Mode 属于 Phase C 冻结范围,解冻时应新建设计稿承接。
|
||||
>
|
||||
> 来源:
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/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 功能面,不改变当前主路径:
|
||||
|
||||
- local-first 普通 Markdown 编辑主路径仍是“授权文件引用 + agent 原生 patch/diff + watcher 同步”;本文只验证 mnote tools 的历史结构性辅助和兼容证据。
|
||||
- 2026-05-22 之后,`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback;本文中的 smoke 只代表历史在线 / compat 回归证据。
|
||||
- `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 功能;只补真实页面证据和安全边界。
|
||||
@@ -1,544 +0,0 @@
|
||||
# 7-2 [done] Phase 7 结构化 Artifact 写链最小落地方案 v1
|
||||
|
||||
> 更新时间:2026-04-23
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v4.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-1-phase7-document-ai-minimum-loop-checklist-v1.md`
|
||||
>
|
||||
> 2026-05-05 追加说明:
|
||||
> - 本稿的对象模型、artifact 写链与 kernel 边界仍然有效
|
||||
> - 当时触发与执行口径曾被 `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v4.md` 覆盖
|
||||
> - 该 `mnote-cli` host 口径现已被 2026-05-13 的 Hermes 面板主线覆盖
|
||||
>
|
||||
> 2026-05-13 追加说明:
|
||||
> - 本稿的对象模型、artifact 写链、projection-only `AI Artifacts` 分组与 kernel 边界继续有效
|
||||
> - 触发与执行口径改由 `/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md` 覆盖
|
||||
> - 当前凡是提到“页面 AI 面板触发”或“页面 AI host 固定动作”的位置,都应理解为:
|
||||
> 页面 AI 面板作为 Hermes 页面内客户端发起意图,Hermes 通过 mnote skill/plugin 调用正式 artifact 工具,最终写入仍回到 Rust runtime / kernel
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份稿只回答一个问题:
|
||||
|
||||
> **在文档页 AI 最小闭环已经完成之后,`summary node / ai_note node / reference edge` 这三条结构化知识写链,第一版到底如何落地,才能既进入 kernel,又不重新污染页面树、文件树和页面主链。**
|
||||
|
||||
当前不写这份稿,后续最容易出现两类跑偏:
|
||||
|
||||
- 把 `summary / ai_note` 继续做成 AI 面板里的临时聊天产物,而不是 kernel node
|
||||
- 为了让文件树看见它们,额外发明一个 `AI Artifacts` 真容器节点,重新污染树真相
|
||||
|
||||
因此本稿的目标不是“做完整 AI 平台”,而是:
|
||||
|
||||
> **只把 `summary node / ai_note node / reference edge` 这三条结构化写链,收口成第一版可执行、可验证、可继续扩展的最小实现。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前结论
|
||||
|
||||
第一版固定采用下面这组边界,不再开放:
|
||||
|
||||
1. 只做三类结构对象:
|
||||
- `summary node`
|
||||
- `ai_note node`
|
||||
- `reference edge`
|
||||
2. 只允许围绕**当前文档页**创建,不做跨页、跨工作区写链
|
||||
3. 第一版允许页面 AI 面板提供固定快捷入口,但快捷入口本质是向 Hermes 发送意图;Hermes 通过 mnote skill/plugin 调用 artifact 工具,不再由页面 AI host 或 `mnote-cli` host 私有触发
|
||||
4. 点击按钮后直接创建,不走“先预览再确认”的两阶段流
|
||||
5. `summary node` 默认单例覆盖更新;`ai_note node` 每次新建
|
||||
6. 两者都落成**可编辑的页面型节点**
|
||||
7. 文件树可见,页面树默认不可见
|
||||
8. 文件树中的 `AI Artifacts` 只是 projection-only 的虚拟分组,不是新的真实 kernel node
|
||||
9. 每次创建 `summary node` 或 `ai_note node` 时,自动连一条指向当前页的 `reference edge`
|
||||
|
||||
一句话收口:
|
||||
|
||||
> **第一版不是让 AI 自由生长“知识图谱”,而是让当前页能够稳定产出两类正式 artifact node,并带上一条最小来源边。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么第一版只做这三条
|
||||
|
||||
### 3.1 这是从“正文写链”走向“结构写链”的最小跨越
|
||||
|
||||
当前 `7-1` 已经完成的是:
|
||||
|
||||
- AI 读取当前页 page aggregate 上下文
|
||||
- AI 通过 `page.head.updateTitle / page.body.save` 回写标题和正文
|
||||
|
||||
但这仍然只是在“改当前页”。
|
||||
|
||||
如果要让 AI 真正进入 `tree-first graph kernel`,第一步不应该直接跳到:
|
||||
|
||||
- 跨页知识网
|
||||
- PDF / Book 结构化摄取
|
||||
- 完整 index node / retrieval 主线
|
||||
|
||||
而应先从三条最小结构写链开始:
|
||||
|
||||
- 为当前页生成一个 `summary node`
|
||||
- 为当前页生成一个 `ai_note node`
|
||||
- 把它们用 `reference edge` 连回当前页
|
||||
|
||||
### 3.2 这三条最适合当前主线边界
|
||||
|
||||
这三者同时满足:
|
||||
|
||||
- 与当前页 AI 主链直接相邻
|
||||
- 不要求跨页权限扩展
|
||||
- 不要求额外引入新的树真相层
|
||||
- 能直接复用当前的 page aggregate 上下文
|
||||
- 能验证 `typed node / typed edge` 进入产品主线,而不是继续只停在文档里
|
||||
|
||||
### 3.3 `PDF / Book -> kernel index` 当前必须继续后置
|
||||
|
||||
`kernel index` 第一版不并入本稿,原因很明确:
|
||||
|
||||
- 它依赖更稳定的摄取边界
|
||||
- 它会引入更重的异步任务与检索协议
|
||||
- 它不是当前页 AI host 面板的最小邻接能力
|
||||
|
||||
所以本稿故意不把阶段 5 全写成一个“大包”,而是只切出:
|
||||
|
||||
> **当前页 -> artifact node / reference edge**
|
||||
|
||||
---
|
||||
|
||||
## 4. 对象模型
|
||||
|
||||
第一版真正进入 kernel 的对象只有三类。
|
||||
|
||||
### 4.1 `summary node`
|
||||
|
||||
`summary node` 是一个真实的 `KernelNode`,`node_type = summary`。
|
||||
|
||||
它的产品语义固定为:
|
||||
|
||||
- 它代表“当前页的最新摘要页”
|
||||
- 它不是聊天消息
|
||||
- 它不是页面内一段临时 block
|
||||
- 它是可编辑页面型节点,但语义上属于 artifact
|
||||
|
||||
它的最小数据要求:
|
||||
|
||||
- `node_type = summary`
|
||||
- `workspace_id = 当前页 workspace`
|
||||
- `metadata.title = 当前页标题 + Summary`
|
||||
- `content = 可被页面主编辑区消费的正文载荷`
|
||||
- `refs.reference_node_ids` 至少包含当前页 `pageId`
|
||||
- `refs.source_node_ids` 至少包含当前页 `pageId`
|
||||
- `audit` 带最小 request / trace / actor 信息
|
||||
|
||||
### 4.2 `ai_note node`
|
||||
|
||||
`ai_note node` 也是一个真实的 `KernelNode`,`node_type = ai_note`。
|
||||
|
||||
它的产品语义固定为:
|
||||
|
||||
- 它代表“围绕当前页生成的一份 AI 工作笔记页”
|
||||
- 它不是摘要快照
|
||||
- 它允许后续继续人工编辑
|
||||
- 它是页面型节点,但语义上属于 artifact
|
||||
|
||||
它的最小数据要求:
|
||||
|
||||
- `node_type = ai_note`
|
||||
- `workspace_id = 当前页 workspace`
|
||||
- `metadata.title = 当前页标题 + AI Note + 时间戳/序号`
|
||||
- `content = 可被页面主编辑区消费的正文载荷`
|
||||
- `refs.reference_node_ids` 至少包含当前页 `pageId`
|
||||
- `refs.source_node_ids` 至少包含当前页 `pageId`
|
||||
- `audit` 带最小 request / trace / actor 信息
|
||||
|
||||
### 4.3 `reference edge`
|
||||
|
||||
`reference edge` 在第一版里不新增新的 edge type 名字,先收口到现有 `KernelEdgeType::References`。
|
||||
|
||||
它的产品语义固定为:
|
||||
|
||||
- 表示 artifact node 引用了当前页
|
||||
- 它不是父子边
|
||||
- 它不进入页面树 / 文件树
|
||||
- 它只进入关系层 / 引用层 /后续 graph traversal
|
||||
|
||||
第一版边方向固定为:
|
||||
|
||||
- `from_node_id = artifact node`
|
||||
- `to_node_id = 当前页 node`
|
||||
- `edge_type = references`
|
||||
|
||||
这样可以稳定表达:
|
||||
|
||||
> **这个 summary / ai_note 是围绕当前页生成的。**
|
||||
|
||||
### 4.4 `AI Artifacts` 不是对象,只是投影分组
|
||||
|
||||
第一版明确禁止把 `AI Artifacts` 做成真实 node。
|
||||
|
||||
它只允许存在于:
|
||||
|
||||
- 文件树 projection
|
||||
- 可能的 Inspector / artifact 列表 projection
|
||||
|
||||
它不允许存在于:
|
||||
|
||||
- kernel node truth
|
||||
- tree command truth
|
||||
- page aggregate truth
|
||||
|
||||
理由很简单:
|
||||
|
||||
- 它只是展示分组
|
||||
- 它不是用户真正的知识对象
|
||||
- 它不应反向塑造树真相
|
||||
|
||||
---
|
||||
|
||||
## 5. 写入语义
|
||||
|
||||
### 5.1 统一动作面
|
||||
|
||||
第一版只开放两个显式动作:
|
||||
|
||||
- `create_summary_artifact_for_current_page`
|
||||
- `create_ai_note_artifact_for_current_page`
|
||||
|
||||
这里先用产品动作名描述,不在本稿中强行冻结底层最终 route/path 名。
|
||||
|
||||
长期原则固定为:
|
||||
|
||||
- 它们必须进入 Rust runtime / bridge-runtime 主写链
|
||||
- 不允许绕过 kernel 直接只写前端本地状态
|
||||
- 不允许只停留在 AI 面板会话日志里
|
||||
|
||||
### 5.2 `summary node` 的写入策略
|
||||
|
||||
`summary node` 第一版采用**单例覆盖更新**。
|
||||
|
||||
固定规则:
|
||||
|
||||
1. 先按“当前页 -> artifact node_type = summary”查询是否已有 summary node
|
||||
2. 若已存在:
|
||||
- 复用该节点 `node_id`
|
||||
- 覆盖其 `content`
|
||||
- 更新其 `metadata.updated_at`
|
||||
- 保留其页面型身份
|
||||
3. 若不存在:
|
||||
- 新建一个 `summary node`
|
||||
- 自动附加到当前页的 artifact projection 分组下
|
||||
- 自动创建一条 `reference edge`
|
||||
|
||||
为什么 `summary` 采用单例:
|
||||
|
||||
- 它更像“当前页最新摘要”
|
||||
- 否则文件树下会快速积累多份摘要页
|
||||
- 第一版要先控制 artifact 数量,而不是先保留完整历史
|
||||
|
||||
### 5.3 `ai_note node` 的写入策略
|
||||
|
||||
`ai_note node` 第一版采用**每次新建**。
|
||||
|
||||
固定规则:
|
||||
|
||||
1. 每次点击 `创建 AI Note` 都新建一个 `ai_note node`
|
||||
2. 标题默认带时间戳或递增序号
|
||||
3. 创建后自动创建一条 `reference edge`
|
||||
4. 不尝试合并、覆盖旧 `ai_note`
|
||||
|
||||
为什么 `ai_note` 采用多实例:
|
||||
|
||||
- 它更像工作产物
|
||||
- 一次次 AI 分析、本轮整理、本轮方案,本来就可能并存
|
||||
- 如果也做成单例,会过早丢失工作过程
|
||||
|
||||
### 5.4 来源边写入策略
|
||||
|
||||
创建 `summary node` 或 `ai_note node` 时,自动写一条 `reference edge`。
|
||||
|
||||
第一版不要求用户再单独确认,也不要求额外按钮。
|
||||
|
||||
这条边的作用不是“展示炫酷图谱”,而是保证两个基本能力:
|
||||
|
||||
1. 后续能追溯这个 artifact 来源于哪一页
|
||||
2. 后续文件树/关系面板/graph traversal 不需要重新猜测来源
|
||||
|
||||
### 5.5 第一版不做的写入动作
|
||||
|
||||
第一版明确不做:
|
||||
|
||||
- artifact -> 多来源页面的复合引用
|
||||
- artifact -> 块级 evidence 的精细挂接
|
||||
- 跨页创建 summary / ai_note
|
||||
- 人工拖拽 artifact 重新挂到别的页面
|
||||
- artifact 自动进页面树
|
||||
|
||||
这些都属于第二版以后再做的能力。
|
||||
|
||||
---
|
||||
|
||||
## 6. 触发方式与产品入口
|
||||
|
||||
### 6.1 允许固定按钮触发,但必须经过 Hermes
|
||||
|
||||
第一版允许页面 AI 面板提供两个固定快捷按钮:
|
||||
|
||||
- `创建 Summary`
|
||||
- `创建 AI Note`
|
||||
|
||||
但按钮不得绕过 Hermes 或 mnote plugin 直接写入。正确链路是:
|
||||
|
||||
```text
|
||||
Leptos 页面 AI 面板
|
||||
-> Hermes session/run
|
||||
-> mnote Hermes skill/plugin
|
||||
-> Rust runtime / kernel
|
||||
-> artifact node / reference edge
|
||||
```
|
||||
|
||||
自然语言触发可以作为后续能力加入,但第一版验收仍以固定按钮或固定 tool intent 为准,避免 artifact 写链质量和权限边界同时失控。
|
||||
|
||||
### 6.2 点击后直接创建
|
||||
|
||||
第一版点击按钮后,直接创建,不走预览确认。
|
||||
|
||||
这是一个非常明确的产品取舍:
|
||||
|
||||
- 优点:动作和结果一一对应,最容易验证主写链
|
||||
- 缺点:产物质量控制不如“先预览再落库”
|
||||
|
||||
当前优先级里,第一版更重视:
|
||||
|
||||
- 正式写链成立
|
||||
- kernel node / edge 成立
|
||||
- 文件树 projection 成立
|
||||
- Hermes tool call -> mnote plugin -> Rust kernel 的边界成立
|
||||
|
||||
而不是先做复杂的人机审核流。
|
||||
|
||||
### 6.3 内容来源固定为当前页全文
|
||||
|
||||
第一版两个按钮的内容来源都固定为:
|
||||
|
||||
- 当前页全文
|
||||
|
||||
不做:
|
||||
|
||||
- 仅选区生成
|
||||
- 仅当前可见块生成
|
||||
- 混入会话消息做额外上下文真相
|
||||
|
||||
这能确保第一版边界足够清楚:
|
||||
|
||||
> **artifact 是围绕当前页全文生成,而不是围绕聊天面板局部状态生成。**
|
||||
|
||||
---
|
||||
|
||||
## 7. 文件树 projection 与页面树隐藏规则
|
||||
|
||||
### 7.1 页面树默认不可见
|
||||
|
||||
`summary node` 与 `ai_note node` 第一版默认不进入页面树 projection。
|
||||
|
||||
原因:
|
||||
|
||||
- 页面树是日常导航主链
|
||||
- artifact 进入页面树会明显污染日常使用
|
||||
- 它们虽然是页面型节点,但不是“用户正常页面层级”的一部分
|
||||
|
||||
### 7.2 文件树可见
|
||||
|
||||
`summary node` 与 `ai_note node` 第一版可以在文件树 projection 中可见。
|
||||
|
||||
这样做的目的不是把文件树变成第二个知识图谱,而是保留一个“必要时可管理”的管理入口。
|
||||
|
||||
### 7.3 `AI Artifacts` 虚拟分组规则
|
||||
|
||||
在文件树 projection 中,为每个当前页生成一个 projection-only 分组:
|
||||
|
||||
- `AI Artifacts`
|
||||
|
||||
固定规则如下:
|
||||
|
||||
1. 它不是真实 node
|
||||
2. 它不持久化到 kernel
|
||||
3. 它只存在于 `file_tree` projection 结果中
|
||||
4. 它的 children 由“当前页关联的 artifact nodes”动态投影得出
|
||||
|
||||
### 7.4 文件树中的相对位置
|
||||
|
||||
第一版推荐把 `AI Artifacts` 放在当前页下面,但与普通子页面分区显示。
|
||||
|
||||
不是:
|
||||
|
||||
- artifact 混在普通子页面列表里
|
||||
|
||||
而是:
|
||||
|
||||
- 当前页
|
||||
- 普通文件/子页
|
||||
- `AI Artifacts`
|
||||
- `Summary`
|
||||
- `AI Note 2026-04-23 10:31`
|
||||
- `AI Note 2026-04-23 10:46`
|
||||
|
||||
这样既满足“文件树可管理”,又最大限度避免污染普通导航。
|
||||
|
||||
### 7.5 `reference edge` 不进树
|
||||
|
||||
`reference edge` 永远不是树项。
|
||||
|
||||
它应只进入:
|
||||
|
||||
- 关系面板
|
||||
- 引用面板
|
||||
- graph traversal
|
||||
- 后续结构阅读态
|
||||
|
||||
它不允许伪装成页面树 / 文件树节点。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与当前主链的关系
|
||||
|
||||
### 8.1 不改变当前页主编辑区真相
|
||||
|
||||
第一版 artifact 写链不会改变:
|
||||
|
||||
- 当前页正文依然走 `page.body.save`
|
||||
- 当前页标题依然走 `page.head.updateTitle`
|
||||
- 当前页页面设置依然走 `page.layout.updateOptions`
|
||||
|
||||
artifact 写链是新增的结构写链,不是对当前页主链的替代。
|
||||
|
||||
### 8.2 与 page aggregate 的关系
|
||||
|
||||
第一版 artifact 生成必须依赖当前 page aggregate 上下文,但不把 artifact 自身并入当前页的 page aggregate truth。
|
||||
|
||||
也就是说:
|
||||
|
||||
- page aggregate 提供生成材料
|
||||
- artifact 是新的 kernel node
|
||||
- artifact 不是当前页 `page_body` 的附属字段
|
||||
|
||||
这是必须守住的边界,否则又会回到“把结构对象塞进页面壳状态”的旧路。
|
||||
|
||||
### 8.3 与 tree command 的关系
|
||||
|
||||
artifact node 虽然默认不进页面树,但它们仍然是 tree-first graph kernel 的真实 node。
|
||||
|
||||
因此长期上仍应可复用:
|
||||
|
||||
- `kernel.node.get`
|
||||
- `kernel.subtree.get`
|
||||
- `kernel.edges.list`
|
||||
- `kernel.edge.attach`
|
||||
|
||||
但第一版不要求把 artifact 的全部树命令交互一次做完。
|
||||
|
||||
---
|
||||
|
||||
## 9. 最小实现分层
|
||||
|
||||
### 9.1 Kernel / Rust runtime 层
|
||||
|
||||
需要补的最小能力:
|
||||
|
||||
1. 创建 `summary node`
|
||||
2. 更新现有 `summary node`
|
||||
3. 创建 `ai_note node`
|
||||
4. 创建 `references` edge
|
||||
5. 按当前页查找其关联 artifact nodes
|
||||
|
||||
第一版不要求完整 artifact 生命周期,只要求:
|
||||
|
||||
- create
|
||||
- update summary
|
||||
- list by current page relation
|
||||
|
||||
### 9.2 Projection 层
|
||||
|
||||
需要补的最小能力:
|
||||
|
||||
1. `file_tree` projection 能识别“当前页关联的 artifact nodes”
|
||||
2. 在 projection 里注入一个虚拟 `AI Artifacts` 分组
|
||||
3. 页面树 projection 不注入该分组
|
||||
|
||||
### 9.3 前端文档页层
|
||||
|
||||
需要补的最小能力:
|
||||
|
||||
1. 页面 AI 面板显示两个固定按钮
|
||||
2. 点击按钮后触发相应写入动作
|
||||
3. 创建完成后,文件树对应 projection 刷新
|
||||
4. 若新建了 `ai_note node`,允许点击进入该页面
|
||||
5. 若更新了 `summary node`,允许点击进入摘要页
|
||||
|
||||
---
|
||||
|
||||
## 10. 验证口径
|
||||
|
||||
第一版完成,至少要满足以下验证。
|
||||
|
||||
### 10.1 Summary 链
|
||||
|
||||
- 当前页点击 `创建 Summary`
|
||||
- 若不存在 summary node,则新建
|
||||
- 若已存在,则覆盖更新
|
||||
- 文件树中当前页下出现或保留 `AI Artifacts / Summary`
|
||||
- 页面树中不出现该节点
|
||||
- 可打开该 `summary node`
|
||||
- kernel 中存在从 `summary node -> 当前页` 的 `references` edge
|
||||
|
||||
### 10.2 AI Note 链
|
||||
|
||||
- 当前页点击 `创建 AI Note`
|
||||
- 每次都新建一个新的 `ai_note node`
|
||||
- 文件树中当前页下 `AI Artifacts` 分组内累积出现新节点
|
||||
- 页面树中不出现这些节点
|
||||
- 可打开新建的 `ai_note node`
|
||||
- kernel 中存在从 `ai_note node -> 当前页` 的 `references` edge
|
||||
|
||||
### 10.3 真相边界
|
||||
|
||||
- `AI Artifacts` 不出现在真实 kernel node 列表中
|
||||
- `AI Artifacts` 只存在于文件树 projection 结果中
|
||||
- 当前页 page aggregate 不被额外塞入 artifact truth
|
||||
- artifact 不混入普通页面树导航主链
|
||||
|
||||
---
|
||||
|
||||
## 11. 明确不做什么
|
||||
|
||||
第一版明确不做:
|
||||
|
||||
- 自然语言隐式触发 artifact 创建
|
||||
- 跨页 summary / ai_note 写链
|
||||
- artifact 多来源引用
|
||||
- block 级 evidence 精细挂接
|
||||
- `AI Artifacts` 真实容器节点化
|
||||
- artifact 进入页面树默认主链
|
||||
- artifact 复杂权限模型
|
||||
- artifact 预览确认流
|
||||
- artifact 版本历史产品壳
|
||||
|
||||
---
|
||||
|
||||
## 12. 最终冻结口径
|
||||
|
||||
当前冻结如下:
|
||||
|
||||
> **阶段 5 的第一版,只把 `summary node / ai_note node / reference edge` 作为最小结构化知识写链落到 kernel。**
|
||||
|
||||
> **`summary node / ai_note node` 都是真实页面型 kernel node,`reference edge` 使用现有 `references` typed edge;`AI Artifacts` 只是文件树 projection 下的虚拟分组,不是真实对象。**
|
||||
|
||||
> **第一版只允许围绕当前页,通过页面 AI 面板固定按钮直接触发;`summary node` 单例覆盖更新,`ai_note node` 每次新建;创建节点时自动连一条指向当前页的 `reference edge`。**
|
||||
|
||||
一句话收口:
|
||||
|
||||
> **第一版的目标不是做“大而全 AI 知识层”,而是让当前页第一次稳定地产出正式 artifact node,并且能在不污染页面树真相的前提下进入文件树管理视图。**
|
||||
@@ -1,373 +0,0 @@
|
||||
# 7-25 [process] ACP Session Runtime 增强规划 v1
|
||||
|
||||
> 更新时间:2026-05-17
|
||||
> 参考:`hermes-vscode-main` (ACP client) / `hermes-web-ui-0.5.18` (HTTP API)
|
||||
>
|
||||
> 2026-05-22 口径补充:本文是 ACP session runtime 增强的历史执行记录,当时曾选择根 `convex/` 作为 ACP-local runtime store。后续 `2-8` 已把默认 auth / ACP-Hermes runtime session / share / sync / AI policy 控制面替换为 Rust SQLite `control-plane`;根 `convex/` functions 源码已软删除到 `recycle/20260522-convex-runtime-retirement/convex/`,`scripts/run-convex-deploy.js` 已归档到同批 recycle 目录。下文中的 Convex store / 根 `convex/` 部署描述仅作为历史记录,不再是当前实现方向。
|
||||
|
||||
## 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 真相归 Hermes,mnote-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 进行隔离;当前默认数据库边界已改为 Rust SQLite control-plane,不再新增 Convex functions 作为默认 runtime store。
|
||||
|
||||
## 2. 参考实现分析
|
||||
|
||||
### hermes-vscode-main(ACP 客户端参考)
|
||||
|
||||
- **`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.18(HTTP 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 A:Session 持久化与管理(P0 — 缺少则 ACP 无实用价值)
|
||||
|
||||
1. **会话持久化存储**
|
||||
- 默认先建设 ACP-local runtime store / TTL cache,不改 Hermes HTTP 会话真相归属
|
||||
- 持久化如需数据库,默认落到 Rust SQLite control-plane;不新增根 Convex functions 作为默认 runtime store
|
||||
- 表结构先服务 `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 session;Hermes 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 源目录口径
|
||||
|
||||
当前仓库状态下:
|
||||
|
||||
- `recycle/20260522-convex-runtime-retirement/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;当前这些源码和脚本已退役到 `recycle/20260522-convex-runtime-retirement/`。
|
||||
|
||||
后续若要恢复 Convex cloud / compat source,必须先明确新的 cloud/compat 部署边界,再从 recycle 恢复必要 functions;不能把根 `convex/` 重新作为默认 active deploy source。
|
||||
|
||||
### 4.1 存储方案
|
||||
|
||||
注意:以下结构是历史 Convex-backed ACP-local store 候选形状,不默认推翻 Hermes 持有聊天真相的既有边界。当前默认 store 已转向 Rust SQLite control-plane;多用户隔离字段仍是硬要求。
|
||||
|
||||
```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 清理;当前默认应改 Rust SQLite control-plane schema/API,`infra/convex/` 已软删除到 recycle,只负责历史对照,不再作为 active 服务部署
|
||||
- **修改** `acp_session_manager.rs` — 按架构决策写入 ACP-local 消息或只写 runtime/event 索引
|
||||
- **修改** `hermes_client.rs` — 新增 session 管理 route handlers
|
||||
- **修改** `routes/mod.rs` — 注册新 route
|
||||
- **修改** control-plane / compat transport 调用点 — mnote-web 默认通过 SQLite control-plane 读写 session/runtime 索引,Convex 只作为显式 legacy compat
|
||||
|
||||
### 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 A(P0 基础可用)
|
||||
├── 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 B(P1 常用增强)
|
||||
├── 5.6 会话恢复
|
||||
├── 5.7 分页消息查询
|
||||
├── 5.8 用量记录
|
||||
├── 5.9 会话导出
|
||||
└── 5.10 Payload 生命周期修复(bug 3-21)
|
||||
|
||||
Phase C(P2 用户体验)
|
||||
├── 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 control-plane。
|
||||
- [x] 历史阶段曾在 Convex 中建 `sessions`、`messages` 或更小的 `runtimeRuns` / `runtimeEvents` 记录,补齐索引和权限过滤。
|
||||
- 历史实现为根 `convex/schema.ts` 的 `acp_runtime_runs` / `acp_runtime_events`,以及 `convex/aiSessions.ts` 的 `upsertRuntimeRun/getRuntimeRun/listRuntimeRuns`;当前根 `convex/` 已退役。
|
||||
- [x] 把 `ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 中必须保留的数据拆到持久化层或短 TTL 层。
|
||||
- 历史阶段 `create_run` 会把 ACP payload/runtime 写入 Convex;当前默认写入 SQLite control-plane,旧 Convex store 仅在显式 legacy compat 下使用。
|
||||
- [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` 表和索引。(历史记录;当前已退役到 recycle)
|
||||
- 新增根 `convex/aiSessions.ts`:`upsertRuntimeRun`、`getRuntimeRun`、`listRuntimeRuns`,按 `userId` 校验 identity 作用域。(历史记录;当前已退役到 recycle)
|
||||
- 修改 `scripts/run-convex-deploy.js`:优先以仓库根 `convex/` 作为 Convex deploy cwd,避免继续指向已不存在的 `wolai-frontend/`。(历史记录;当前脚本已退役到 recycle,`desktop:hot` / `dev:hot` 不部署 Convex)
|
||||
- 后续历史修正:根 `convex/` 当时只包含 ACP session runtime store,不能直接覆盖完整本地 Convex 部署;`scripts/run-convex-deploy.js` 曾改为只有根 `convex/` 具备完整 schema 时才用根目录,否则继续使用现有完整 Convex 源。当前根 `convex/` 和该 deploy 脚本均已退役到 recycle。
|
||||
- 修改 `rust/crates/mnote-web/src/routes/hermes_client.rs`:
|
||||
- ACP `create_run` 写入 `aiSessions:upsertRuntimeRun`(历史 Convex store;当前默认 SQLite);
|
||||
- ACP `GET /client/sessions` 读取 `aiSessions:listRuntimeRuns`(历史 Convex store;当前默认 SQLite);
|
||||
- ACP `GET /client/sessions/{session_id}` 读取 runs/events;
|
||||
- ACP `POST /client/sessions/{session_id}/resume` 返回 runtime history 恢复来源;
|
||||
- ACP `DELETE /client/sessions/{session_id}`、`rename`、`auto-title` 写入 runtime store;
|
||||
- ACP `GET /client/sessions/search` 返回 runtime store 搜索摘要和 snippet;
|
||||
- ACP `stream_events` 将 SSE event 写入 runtime event store;
|
||||
- `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`:历史验证通过;当前脚本已退役到 recycle。
|
||||
- `node scripts/run-convex-deploy.js`:历史验证通过;当时部署入口已避免用根最小 `convex/` 覆盖完整本地 Convex schema。当前默认启动链路不再部署 Convex。
|
||||
- `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` 曾通过;当前 `.convex-tmp` 已软删除到 `recycle/20260522-convex-runtime-retirement/local-state/.convex-tmp`,不再作为 active codegen 临时目录。
|
||||
- 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 failed;main tests 0 passed / 0 failed;doc 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 目录。
|
||||
@@ -1,589 +0,0 @@
|
||||
# 7-27 [done] 在线 Markdown_edit 以最终 Markdown 为写回真源 v2
|
||||
|
||||
> 更新:2026-05-18(v2:整合 CLI Main 参考实现分析,确认方向,补充见解)
|
||||
>
|
||||
> 2026-05-19 local-first 口径补充:
|
||||
> - 本文仍适用于 `convex_workspace` / 在线文档的 `mnote.doc.markdown_edit` 修复,但当前默认产品形态已切到 local-first workspace。
|
||||
> - 本地 `.md` 路径的默认 AI 编辑主路径是“授权文件引用 + agent 原生 patch/diff + watcher 同步”。
|
||||
> - 后续新增 AI 编辑能力默认先保证本地 `.md` 与 `{mdBase}.assets/` 相对路径不被改写为 Convex media asset;在线 Convex 文档路径只作为可选 cloud / sync / share source。
|
||||
>
|
||||
> 2026-05-22 口径修正:`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback。本文只保留在线 / compat 写回修复的历史证据;当前 active 设计以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准。
|
||||
>
|
||||
> 当前状态:`DONE`(已归档,代码验证通过 2026-05-21)
|
||||
>
|
||||
> 关联缺陷:`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/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
>
|
||||
> 当前 active 设计:`design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-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 Main(Lark 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(¤t_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 存储包含 marks(bold/italic/code/link),当前 `doc_markdown_edit` 的整体设计不保证行内格式——这超出了 7-24/7-25 的范围,但建议在 7-14 的 Phase A/B 之后评估。
|
||||
@@ -1,121 +0,0 @@
|
||||
# 7-29 [done] Batch I AI tool final-content / ACP tail 收口 checklist v1
|
||||
|
||||
> 创建时间:2026-05-21
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> 上位入口:`design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
>
|
||||
> 阶段:Batch I / 1-8 Batch E AI 与资源工具收口
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本批次只处理 `1-8` Batch E 中已经有明确代码痕迹、但文档状态仍在 `process/` 的 AI 工具尾项:
|
||||
|
||||
1. 复核 `7-27`:`mnote.doc.markdown_edit` 在线写回是否已经以最终 markdown 为真源,是否可归档。
|
||||
2. 复核 `7-15`:ACP runtime 统一层 Step 15-17 的真实剩余缺口,拆成下一批可执行小任务。
|
||||
3. 复核 `7-12`:Manifest / review surface / state event 中哪些已由当前 Hermes tools / ACP runtime 覆盖,哪些仍冻结或待拆。
|
||||
|
||||
本批次不实现 Phase C Review Mode,不扩新 AI 产品面,不恢复旧 HTTP proxy 为主路径,不把 `mnote.doc.markdown_edit` 升回 local-first 普通正文默认主路径。
|
||||
|
||||
## 2. 已有本地证据
|
||||
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --test-threads=1`:16 passed。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest_describes_markdown_edit_write_contract -- --test-threads=1`:1 passed。
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs` 已包含 `parse_final_markdown_to_blocks`、`build_page_content`、`build_changed_blocks_summary`、`mnote.doc.markdown_edit (7-27)` 写回 reason。
|
||||
|
||||
## 3. Reasonix Worker 拆分
|
||||
|
||||
### Worker A:`7-27` 归档性审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md`
|
||||
- 若确认可归档,可移动到 `design/07-ai/done/7-27-online-markdown-writeback-final-content-truth-v2.md`
|
||||
|
||||
只读参考:
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 对照 `7-27` 的步骤 1-6、测试矩阵、开放问题,判断当前代码是否已满足归档条件。
|
||||
- 如果可归档,只更新设计文档状态并移动到 `done/`。
|
||||
- 如果不可归档,只写明最小剩余缺口,不改 runtime 代码。
|
||||
|
||||
验收:
|
||||
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --test-threads=1`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest_describes_markdown_edit_write_contract -- --test-threads=1`
|
||||
- `git diff --check`
|
||||
|
||||
### Worker B:`7-27` 代码/测试缺口审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-i-worker-b-7-27-code-test-gap.md`
|
||||
|
||||
只读参考:
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 不修改代码。
|
||||
- 审查当前 `7-27` 实现是否存在明显缺口:注释格式、revisionRef 保留、legacy `content` / `contentNodes` 文本读取、复杂块保留、selection 范围、dryRun、full_content。
|
||||
- 输出“必须修复才能归档 / 可作为后续增强 / 无问题”的分级表。
|
||||
|
||||
验收:
|
||||
|
||||
- 结果文件必须引用具体文件和函数。
|
||||
- `git diff --check`
|
||||
|
||||
### Worker C:`7-15 / 7-12` 后续拆分审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-i-worker-c-acp-review-tail.md`
|
||||
|
||||
只读参考:
|
||||
|
||||
- `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
- `rust/crates/mnote-web/src/acp_client.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/acp_runtime.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
|
||||
目标:
|
||||
|
||||
- 不修改代码和主设计文档。
|
||||
- 判断 `7-15` Step 15-17 是否仍是当前应执行项,还是应该拆成压力测试、旧 HTTP proxy 瘦身、Reasonix cache benchmark 三个独立 checklist。
|
||||
- 判断 `7-12` Phase B-F 中哪些已由当前 manifest / runtime selector / tool guard 覆盖,哪些仍冻结。
|
||||
- 输出下一批建议顺序和可派发 worker 方向。
|
||||
|
||||
验收:
|
||||
|
||||
- 结果文件必须区分 P0/P1/P2。
|
||||
- 不把 Phase C Review Mode 解冻。
|
||||
- `git diff --check`
|
||||
|
||||
## 4. Codex 复核项
|
||||
|
||||
- [x] 读取 Worker A/B/C 的 `final.md` / `result.json` / diff。
|
||||
- [x] 独立复核 `7-27` 相关测试,不凭 Worker A 归档结论直接验收。
|
||||
- [x] 若 Worker B 找到必须修复缺口,由 Codex 本地补最小修复或拆下一轮 worker。
|
||||
- [x] 若 `7-27` 可归档,更新 `1-8` Batch E 当前状态。
|
||||
- [x] 根据 Worker C 结果决定 Batch I 下一步拆分。
|
||||
- [ ] 运行 `git diff --check`、必要 targeted tests、`codegraph sync .`。
|
||||
|
||||
## 5. 本轮执行记录
|
||||
|
||||
- 2026-05-21:Codex 建立 Batch I checklist,准备派发 Worker A/B/C。
|
||||
- 2026-05-21:Worker A/B 均确认 `7-27` 可归档;Codex 复跑 `markdown_edit` 与 manifest write contract targeted tests 后确认无 P0 缺口,`7-27` 已移动到 `design/07-ai/done/`。
|
||||
- 2026-05-21:Worker B 记录的 `revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体均列为后续增强,不阻塞归档。
|
||||
- 2026-05-21:Worker C 确认 `7-15` Step 15-17 与 `7-12` Phase B/F 应拆成独立稳定化 checklist;Phase C Review Mode 继续冻结。
|
||||
- 2026-05-21:下一批不在本文件继续膨胀,改拆 `7-34` 处理 ACP runtime cleanup / availability / 稳定性验证尾项。
|
||||
@@ -1,428 +0,0 @@
|
||||
# 7-3 [done] 页面 AI Hermes 面板与 mnote Plugin 主线方案 v1
|
||||
|
||||
> 更新时间:2026-05-13
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-10-wolai-page-settings-and-ai-surface-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-2-phase7-structured-artifact-write-chain-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`
|
||||
>
|
||||
> 覆盖关系:
|
||||
> - 覆盖 `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v4.md`
|
||||
> 中“`mnote-cli` 是唯一长期 agent 执行面”的口径。
|
||||
> - 保留 `v4` 中“Web 不应拥有第二套工具注册表、结构化写入必须回到 Rust runtime / kernel”的判断。
|
||||
> - 保留 `7-2` 中 `summary node / ai_note node / reference edge` 的对象模型和写入边界,
|
||||
> 但触发方改为 Hermes tool call,而不是页面 AI host 私有按钮或 `mnote-cli` host。
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份稿只回答一个问题:
|
||||
|
||||
> **未来页面 AI 的长期主语到底是谁。**
|
||||
|
||||
当前冻结答案是:
|
||||
|
||||
> **页面 AI 面板只是 Hermes 的页面内客户端;Hermes session/message/tool event/usage/model 才是会话真相;mnote 通过 Hermes skill/plugin 暴露业务能力。**
|
||||
|
||||
这不是把 mnote 的业务真相交给 Hermes。长期边界必须分清:
|
||||
|
||||
- Hermes 负责 AI 编排、会话、模型、tool call 调度和聊天历史。
|
||||
- mnote 负责页面、树、正文、artifact、edge、projection 和审计事实。
|
||||
- 页面 AI 面板只负责在文档页里打开一个 Hermes 客户端。
|
||||
|
||||
一句话收口:
|
||||
|
||||
> **AI 会话归 Hermes,mnote 能力归 Rust kernel;页面 AI 面板只连接两者。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么要从 CLI-first 改为 Hermes-first
|
||||
|
||||
`CLI-first` 解决了一个真实问题:避免 Web 前端继续拥有私有 AI 编排、私有工具注册表和私有写链。
|
||||
|
||||
但它也带来了新的错位:
|
||||
|
||||
- 页面 AI 面板开始伪装成 `mnote-cli` 图形客户端。
|
||||
- Hermes、Codex、`openai-agents-python` 被统一压成“外置 agent”,但实际用户希望页面 AI 就是 Hermes 面板。
|
||||
- 真实会话能力、模型选择、tool event、thinking、usage、历史搜索这些已经是 Hermes 的强项,mnote 自己再做一套会重复。
|
||||
- 当前运行态已经出现冲突:页面壳默认发送 `provider=hermes`,而 `/api/ai-agent/run` 又按旧退场口径返回 `ai_provider_bridge_unavailable`。
|
||||
|
||||
因此新的主线不是恢复旧的 Web 私有 AI 编排,而是:
|
||||
|
||||
> **把页面 AI 从 `mnote-cli host` 改成 Hermes client,把 mnote 能力从 Web 私有 tool 改成 Hermes 可发现、可调用的 skill/plugin。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 长期分层
|
||||
|
||||
### 3.1 Hermes
|
||||
|
||||
Hermes 负责:
|
||||
|
||||
- session 创建、恢复、重命名、删除、搜索
|
||||
- message 存储与 conversation history
|
||||
- model / provider / profile 选择
|
||||
- streaming 事件、thinking / reasoning、usage
|
||||
- tool call 调度、排队、取消、恢复
|
||||
- skill / plugin 的发现和启停
|
||||
|
||||
Hermes 不负责:
|
||||
|
||||
- 直接写 mnote 的 Convex 表
|
||||
- 直接构造第二套 page aggregate
|
||||
- 直接决定页面树、文件树、artifact、edge 的事实结构
|
||||
|
||||
### 3.2 mnote Rust kernel / runtime
|
||||
|
||||
mnote Rust 负责:
|
||||
|
||||
- `Page Aggregate`
|
||||
- `page.*` command
|
||||
- `tree.*` command
|
||||
- `kernel.*` query / edge
|
||||
- `summary node / ai_note node / reference edge`
|
||||
- projection、audit、idempotency、workspace / actor scope
|
||||
|
||||
Rust 不负责:
|
||||
|
||||
- 存储 Hermes 聊天历史
|
||||
- 维护 Hermes session 列表
|
||||
- 重做 Hermes 模型、profile、usage 管理
|
||||
|
||||
### 3.3 页面 AI Leptos 面板
|
||||
|
||||
页面 AI 面板负责:
|
||||
|
||||
- 右下角入口与右侧抽屉壳层继续服从 Wolai 对齐结果
|
||||
- 用 Leptos 实现 Hermes chat 的页面内子集
|
||||
- 调用 Hermes session / run / stream API
|
||||
- 把当前页面上下文作为 Hermes run 的输入或 session workspace context
|
||||
- 展示 Hermes 返回的 message、reasoning、tool event、error、usage
|
||||
|
||||
页面 AI 面板不负责:
|
||||
|
||||
- 自己保存聊天真相
|
||||
- 自己维护工具注册表
|
||||
- 自己执行页面写入
|
||||
- 自己 fallback 到 `mnote-cli` 或旧 sidecar
|
||||
|
||||
### 3.4 mnote Hermes skill/plugin
|
||||
|
||||
mnote 需要作为 Hermes skill/plugin 暴露能力。
|
||||
|
||||
第一版建议能力分组:
|
||||
|
||||
- `mnote.page.get`
|
||||
- `mnote.page.save`
|
||||
- `mnote.page.update_title`
|
||||
- `mnote.page.update_options`
|
||||
- `mnote.tree.create`
|
||||
- `mnote.tree.move`
|
||||
- `mnote.search.documents`
|
||||
- `mnote.artifact.create_summary`
|
||||
- `mnote.artifact.create_ai_note`
|
||||
- `mnote.kernel.attach_reference`
|
||||
|
||||
这些工具可以由 plugin 内部调用:
|
||||
|
||||
- Rust Web 同源 tool bridge
|
||||
- `mnote-cli` JSON adapter
|
||||
- 或后续更稳定的 Rust plugin bridge
|
||||
|
||||
但对 Hermes 来说,它们必须表现为一组稳定 Hermes tools,而不是页面前端私有函数。
|
||||
|
||||
---
|
||||
|
||||
## 4. 会话真相
|
||||
|
||||
页面 AI 的会话真相固定在 Hermes。
|
||||
|
||||
mnote 不保存:
|
||||
|
||||
- 聊天消息列表
|
||||
- assistant 文本历史
|
||||
- thinking / reasoning 历史
|
||||
- tool event 完整展开状态
|
||||
- session 标题、分组、usage
|
||||
|
||||
mnote 可以保存:
|
||||
|
||||
- 结构化写入产生的 audit
|
||||
- artifact node
|
||||
- reference edge
|
||||
- page/body/title/options 的正式变更
|
||||
- 与一次 Hermes tool call 对应的 request / trace / actor / reason
|
||||
|
||||
也就是说,mnote 只保存“对 mnote 事实源造成影响的结果”,不复制 Hermes 的聊天数据库。
|
||||
|
||||
---
|
||||
|
||||
## 5. 页面上下文进入 Hermes 的方式
|
||||
|
||||
页面 AI 面板打开时,mnote 应提供最小上下文包:
|
||||
|
||||
- `workspaceId`
|
||||
- `documentId`
|
||||
- 页面标题
|
||||
- `pageAggregate` 摘要
|
||||
- 当前选区 / blockId / selected text
|
||||
- 页面设置
|
||||
- 当前用户 actor / capability 摘要
|
||||
|
||||
上下文传入 Hermes 有两种可接受方式:
|
||||
|
||||
1. 作为 run input / instructions 的结构化上下文。
|
||||
2. 作为 Hermes session workspace context,由 mnote panel 在创建或恢复 session 时设置。
|
||||
|
||||
第一版优先采用简单方式:
|
||||
|
||||
> 页面 AI 面板每次发起 run 时附带当前页面上下文摘要;Hermes 如需读取最新正文,再通过 `mnote.page.get` 工具回读。
|
||||
|
||||
这样可以避免把 page aggregate 大对象长期塞进 Hermes session,也避免 stale context 变成事实源。
|
||||
|
||||
---
|
||||
|
||||
## 6. API 与路由边界
|
||||
|
||||
### 6.1 退役 `/api/ai-agent/run` 主路径
|
||||
|
||||
`/api/ai-agent/run` 不再作为页面 AI 的长期主入口。
|
||||
|
||||
允许状态:
|
||||
|
||||
- 暂时保留为 legacy compat,明确返回旧接口退场信息
|
||||
- 或只用于旧 smoke / 对照验证
|
||||
|
||||
禁止状态:
|
||||
|
||||
- 页面 AI 新实现继续向它发送 `provider=hermes`
|
||||
- `/api/ai-agent/run` 继续作为 Hermes 面板的主代理
|
||||
- 它继续持有 mnote 私有工具注册表或执行编排
|
||||
|
||||
### 6.2 新增 Hermes client proxy
|
||||
|
||||
浏览器不应直接暴露 Hermes API key。
|
||||
|
||||
建议在 `mnote-web` 中提供同源薄代理:
|
||||
|
||||
- `/api/hermes/client/sessions`
|
||||
- `/api/hermes/client/runs`
|
||||
- `/api/hermes/client/events`
|
||||
- `/api/hermes/client/models`
|
||||
- `/api/hermes/client/tools`
|
||||
|
||||
这层只做:
|
||||
|
||||
- auth / cookie / token 转发
|
||||
- 同源安全边界
|
||||
- 页面上下文最小注入
|
||||
- 错误码标准化
|
||||
|
||||
这层不做:
|
||||
|
||||
- session 真相存储
|
||||
- message 真相存储
|
||||
- tool 执行编排
|
||||
- 旧 provider fallback
|
||||
|
||||
### 6.3 mnote tool bridge
|
||||
|
||||
Hermes 调用 mnote 工具时,应进入窄桥:
|
||||
|
||||
```text
|
||||
Hermes tool call
|
||||
-> mnote Hermes plugin
|
||||
-> mnote-web /api/hermes/tools/mnote/*
|
||||
-> Rust runtime / kernel command/query
|
||||
-> Hermes tool result
|
||||
```
|
||||
|
||||
第一版不要求一次性冻结最终 URL,但要求协议字段稳定:
|
||||
|
||||
- `toolName`
|
||||
- `arguments`
|
||||
- `workspaceId`
|
||||
- `documentId`
|
||||
- `actor`
|
||||
- `sessionId`
|
||||
- `traceId`
|
||||
- `idempotencyKey`
|
||||
- `dryRun`
|
||||
- `capabilityScope`
|
||||
|
||||
---
|
||||
|
||||
## 7. Leptos 面板参考范围
|
||||
|
||||
参考 `hermes-web-ui-0.5.18`,但只采用页面内必要子集。
|
||||
|
||||
第一版采用:
|
||||
|
||||
- Chat session list
|
||||
- Message list
|
||||
- Chat input
|
||||
- streaming delta
|
||||
- thinking / reasoning 展开
|
||||
- tool started / tool completed 展开
|
||||
- model selector
|
||||
- error / retry / abort
|
||||
- session search 可后置
|
||||
|
||||
第一版不采用:
|
||||
|
||||
- 平台 Channels 管理
|
||||
- Jobs / Cron 管理
|
||||
- Profiles 管理全页面
|
||||
- Logs 全页面
|
||||
- Files 全浏览器
|
||||
- Terminal
|
||||
- Group Chat
|
||||
- Hermes 全局 Settings
|
||||
|
||||
这些能力属于 Hermes 管理台,不属于 mnote 页面 AI 抽屉。
|
||||
|
||||
实现要求:
|
||||
|
||||
- 使用 Leptos island 实现,不引入 Vue / Naive UI。
|
||||
- 保留 Wolai 对齐的右下角入口和右侧 drawer 容器。
|
||||
- 文案和视觉以 mnote 文档页密度为准,不照搬 Hermes Web UI 的整站导航。
|
||||
|
||||
---
|
||||
|
||||
## 8. 结构化 Artifact 写链
|
||||
|
||||
`7-2` 的对象模型继续成立:
|
||||
|
||||
- `summary node`
|
||||
- `ai_note node`
|
||||
- `reference edge`
|
||||
- `AI Artifacts` projection-only 分组
|
||||
|
||||
但触发方改为 Hermes tool call:
|
||||
|
||||
- 用户可以在 Hermes 面板中自然语言要求总结当前页。
|
||||
- Hermes 决定调用 `mnote.artifact.create_summary`。
|
||||
- 或页面面板提供快捷按钮,但按钮本质也是向 Hermes 发送意图,不是绕过 Hermes 直接写 mnote。
|
||||
|
||||
第一版允许两个快捷入口:
|
||||
|
||||
- `创建 Summary`
|
||||
- `创建 AI Note`
|
||||
|
||||
但它们必须走:
|
||||
|
||||
```text
|
||||
Leptos panel -> Hermes session/run -> mnote plugin tool call -> Rust kernel
|
||||
```
|
||||
|
||||
不得走:
|
||||
|
||||
```text
|
||||
Leptos panel -> /api/documents/* 直接写 artifact
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 权限与安全
|
||||
|
||||
Hermes 可以调度工具,但 mnote 必须做最终授权。
|
||||
|
||||
每个 mnote tool call 至少校验:
|
||||
|
||||
- 当前 actor 是否登录
|
||||
- actor 是否属于 workspace
|
||||
- 当前页面是否允许 AI 读取
|
||||
- 当前页面是否允许 AI 写入
|
||||
- 当前工具是否允许写入页面 / 树 / artifact
|
||||
- 是否需要 `dryRun` 或确认
|
||||
|
||||
默认第一版:
|
||||
|
||||
- 读当前页:允许
|
||||
- 写当前页:按页面 capability
|
||||
- 创建 summary / ai_note:按当前页 artifact capability
|
||||
- 跨页写入:默认禁止
|
||||
- 跨 workspace:禁止
|
||||
|
||||
---
|
||||
|
||||
## 10. 迁移步骤
|
||||
|
||||
### Phase A:设计口径统一
|
||||
|
||||
- 新增本稿作为 07-ai 当前主线。
|
||||
- 将 `7-v4 CLI-first` 移入 `design/old/07-ai/process/` 并标记 `[recycle]`。
|
||||
- 更新仍引用 `mnote-cli` 唯一长期 agent 执行面的活跃设计稿。
|
||||
|
||||
### Phase B:薄 Hermes client proxy
|
||||
|
||||
- 在 `mnote-web` 补 Hermes client proxy。
|
||||
- 页面 AI 面板不再调用 `/api/ai-agent/run`。
|
||||
- 浏览器不直接持有 Hermes API key。
|
||||
|
||||
### Phase C:Leptos Hermes 面板最小子集
|
||||
|
||||
- 实现 session 创建 / 恢复。
|
||||
- 实现 run / stream / abort。
|
||||
- 实现 message / reasoning / tool event 展示。
|
||||
- 保留 Wolai 右侧抽屉壳。
|
||||
|
||||
### Phase D:mnote Hermes plugin
|
||||
|
||||
- 暴露 mnote tool manifest。
|
||||
- 先接 `mnote.page.get`、`mnote.page.save`、`mnote.artifact.create_summary`、`mnote.artifact.create_ai_note`。
|
||||
- 工具结果回到 Hermes tool event。
|
||||
|
||||
### Phase E:退役旧面板执行链
|
||||
|
||||
- `/api/ai-agent/run` 从主路径移除。
|
||||
- 旧 React `DocumentAiAgentPanel.runtime` 降为历史参考或 compat。
|
||||
- `provider=hermes/codex/claudecode` 502 不再出现在新页面 AI 主链。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准
|
||||
|
||||
第一版完成时必须满足:
|
||||
|
||||
- 页面 AI 抽屉打开后可创建或恢复 Hermes session。
|
||||
- Hermes session 存储中能看到页面 AI 的消息历史。
|
||||
- mnote 本地不复制聊天消息真相。
|
||||
- 发送消息后,事件流来自 Hermes run。
|
||||
- tool call 展示使用 Hermes tool event。
|
||||
- Hermes 可调用至少一个只读 mnote 工具读取当前页面。
|
||||
- Hermes 可调用至少一个写入工具,经 Rust runtime 写回当前页或创建 artifact。
|
||||
- 页面刷新后,AI 会话从 Hermes 恢复,而不是从 mnote 本地 state 恢复。
|
||||
- 结构化写入产生的 artifact / edge 能在 mnote projection 中验证。
|
||||
|
||||
---
|
||||
|
||||
## 12. 禁止项
|
||||
|
||||
- 不再新增 Web 私有 AI tool registry。
|
||||
- 不再把 `mnote-cli` 写成页面 AI 唯一长期执行面。
|
||||
- 不把 Hermes 聊天历史复制进 mnote page aggregate。
|
||||
- 不让 Hermes plugin 直接写 Convex。
|
||||
- 不在 Leptos 面板里重做 Hermes 后台管理台。
|
||||
- 不把 `AI Artifacts` 做成真实 kernel node。
|
||||
- 不绕过 Rust runtime 创建 artifact / edge。
|
||||
|
||||
---
|
||||
|
||||
## 13. 最终冻结口径
|
||||
|
||||
> **页面 AI 是 Hermes 面板,不是 mnote-cli 面板。**
|
||||
|
||||
> **Hermes 持有 AI 会话真相,mnote 持有业务对象真相。**
|
||||
|
||||
> **mnote 通过 Hermes skill/plugin 暴露工具,工具最终回到 Rust runtime / kernel。**
|
||||
|
||||
> **Leptos 负责页面内 Hermes 客户端体验,不负责 AI 编排。**
|
||||
@@ -1,55 +0,0 @@
|
||||
# 7-30 ACP session/load 恢复 checklist v1
|
||||
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):ACP `session/load` / resume 核心闭环已完成并通过聚焦测试;session id 全链路持久化作为后续产品化项继续跟踪。
|
||||
>
|
||||
> Owner:ACP session lifecycle
|
||||
>
|
||||
> 参考:`reference-code/hermes-vscode-main/src/sessionManager.ts`
|
||||
|
||||
## 目标
|
||||
|
||||
让 MNote ACP runtime 在恢复已有 ACP session 时真正调用 adapter 的 `session/load`,避免只恢复 MNote 会话历史而产生 phantom ACP session。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- 与 session load/resume 直接相关的 Rust 测试
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不改 Page AI 前端 UI。
|
||||
- 不改 Convex schema。
|
||||
- 不删除或覆盖现有 local AI session JSONL 逻辑。
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] 在 `AcpSessionManager` 增加 `load_session(session_id, cwd)` 或等价能力,参数与 Hermes VSCode 一致:`session/load { sessionId, cwd, mcpServers: [] }`。
|
||||
- [x] load 成功时把当前 ACP session id 设置为 loaded id;load 返回 null / error 时允许 fallback 到 `session/new`。
|
||||
- [ ] 明确区分 MNote session id 与 ACP adapter session id;需要持久化/返回时字段名不能混淆。
|
||||
- [x] ACP run 启动处优先尝试恢复已知 ACP session id,再创建新 session。
|
||||
- [x] 补 mock ACP 测试覆盖 `session/load` 成功、返回 null fallback、新 session 仍可 prompt。
|
||||
- [x] 更新本文件“执行记录”。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web acp_session_manager -- --test-threads=1
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web hermes_client_acp_resume -- --test-threads=1
|
||||
```
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-05-20 22:06 — Reasonix Code 执行,Codex 复核并通过聚焦测试。
|
||||
- `acp_types.rs`: 新增 `SessionLoadParams` / `SessionLoadResult` 类型。
|
||||
- `acp_session_manager.rs`:
|
||||
- 新增 `load_session(session_id, cwd)` → 调用 ACP `session/load`,成功时设置 internal session_id,null/error 返回 false。
|
||||
- 新增 `ensure_session(cwd, stored_session_id)` → 1) 复用已有 session,2) 有 stored id 时尝试 load,3) fallback 到 `session/new`。
|
||||
- 新增 7 个单元测试覆盖所有路径:load 成功、null fallback、复用 active、无 stored 创建新 session、fallback 后 prompt 仍可工作。
|
||||
- `hermes_client.rs`:
|
||||
- `acp_stream_events()` 中从 payload 提取 `acpSessionId`,传给 `ensure_session()` 替代原来的 `create_session`。
|
||||
- 错误码从 `acp_session_create_failed` 改为 `acp_session_ensure_failed`。
|
||||
- Codex 复核:当前 `acpSessionId` 只从 run payload 读取,前端/session store 仍未稳定持久化并传回该字段,因此“session id 字段全链路区分与持久化”保留未完成。
|
||||
- 验收:`cargo test -p mnote-web acp_session_manager -- --test-threads=1`、`cargo test -p mnote-web hermes_client_acp_resume -- --test-threads=1`、`cargo test -p mnote-web acp -- --test-threads=1` 通过。
|
||||
@@ -1,52 +0,0 @@
|
||||
# 7-31 ACP permission 决策闭环 checklist v1
|
||||
|
||||
> 状态:done
|
||||
>
|
||||
> Owner:ACP incoming request / permission loop
|
||||
>
|
||||
> 参考:`reference-code/hermes-vscode-main/src/acpClient.ts`、`src/sessionManager.ts`
|
||||
|
||||
## 目标
|
||||
|
||||
把 `session/request_permission` 从"后端立即 unsupported/deny"改成"后端发出 pending request,前端 allow/deny,后端回写 JSON-RPC response"的真实闭环;超时仍默认 deny。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `rust/crates/mnote-web/src/acp_client.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/acp_bridge.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- 只允许为接线 permission 按需小改 `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- 相关 Rust 测试
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不把权限结果存为新的长期正文/附件真相。
|
||||
- 不默认 allow;超时、缺失 run、缺失 pending request 都必须 deny 或返回清晰错误。
|
||||
- 不大改 Page AI drawer 视觉结构。
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] `AcpClient` 支持注册 incoming request handler,并允许 handler 返回 JSON-RPC result/error。
|
||||
- [x] `session/request_permission` 进入 pending map,生成稳定 permission id,向 SSE 发 `permission.requested`。
|
||||
- [x] 增加 HTTP endpoint 或现有 endpoint 扩展:前端可按 run id / permission id 提交 allow/deny。
|
||||
- [x] 提交 allow/deny 后,ACP JSON-RPC request 得到对应 response;前端收到 `permission.allowed` / `permission.denied`。
|
||||
- [x] 超时默认 deny,并向 UI 和 runtime event 写明 timeout。
|
||||
- [ ] 补 mock ACP 测试:incoming request 等待决策、allow 返回 result、deny 返回 result/error、timeout deny。
|
||||
- [x] 更新本文件"执行记录"。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web acp_client -- --test-threads=1
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web acp_bridge -- --test-threads=1
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --test-threads=1
|
||||
```
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-05-20:Reasonix 实施,Codex 复核并修正 JSON-RPC id 保真与 permission response 语义。
|
||||
- 改动点:`AcpClient` 增加 `on_incoming_request()` / `respond_to_incoming()` / `respond_to_incoming_error()`;`dispatch_message` 将 incoming request 转发给 handler 而非立即 error;`AcpSessionManager.new()` 注册 incoming request handler,将 `session/request_permission` 存入 pending map + 发射 `permission.requested` + 启动 60s 超时自动 deny;`AcpSessionManager` 增加 `resolve_permission()` 方法;`acp_bridge` SSE 转换支持 `permission.requested`;`hermes_client.rs` 增加 `resolve_permission` HTTP endpoint;`routes/mod.rs` 注册路由;`layout.rs` 中 `pageAiResolvePermission` 调用后端。
|
||||
- Codex 修正:incoming JSON-RPC request id 使用原始 `serde_json::Value` 保存和回写,避免字符串 id 被改成 0;allow/deny 正常路径改为参考 Hermes VSCode 插件的 `{ outcome: "selected", optionId }`,并补 `permission_response_selects_allow_and_deny_options` 测试。
|
||||
- 未完成:已有 `acp_client` incoming request 基础测试和 option 选择 helper 测试,但还缺“mock ACP 发 permission -> HTTP resolve endpoint -> agent 收到 selected option”的端到端测试,故该 checklist 的 mock 闭环项保留未完成。
|
||||
- 验收:`cargo test -p mnote-web acp_client -- --test-threads=1`、`cargo test -p mnote-web acp_bridge -- --test-threads=1`、`cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --test-threads=1`、`cargo test -p mnote-web acp -- --test-threads=1` 通过。
|
||||
@@ -1,56 +0,0 @@
|
||||
# 7-32 ACP tool locations 与打开动作 checklist v1
|
||||
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):ACP tool locations 解析、SSE 桥接与前端打开动作已完成;浏览器 smoke 可作为后续补强。
|
||||
>
|
||||
> Owner:ACP tool event data + Page AI open action
|
||||
>
|
||||
> 参考:`reference-code/hermes-vscode-main/src/protocol.ts`、`src/chatPanel.ts`
|
||||
|
||||
## 目标
|
||||
|
||||
保留 ACP `tool_call.locations[]`,在 Page AI tool card 中展示位置,并对 read/edit 完成事件提供打开动作。默认打开到 MNote 主编辑器资源 tab / 当前工作区 tab,不默认新浏览器窗口。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `rust/crates/mnote-web/src/acp_types.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/acp_bridge.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- 相关 Rust 测试和必要 smoke 脚本
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不重写资源 tab registry。
|
||||
- 不新增与 Sidex workbench checklist 冲突的 editor group 模型。
|
||||
- 不默认 `window.open`。
|
||||
- 不绕过 existing allowed root / local workspace access。
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] `SessionUpdate::ToolCall` 解析 `locations: [{ path }]`。
|
||||
- [x] `AcpSessionEvent::ToolCall` 携带 locations,并通过 SSE `tool.started` 发送。
|
||||
- [x] `tool.completed` 能保留或关联此前相同 `toolCallId` 的 locations/kind/title。
|
||||
- [x] Page AI tool card 展示 location 摘要。
|
||||
- [x] 对 read/edit completed tool card 增加“打开”动作,优先复用已有 resource tab / file open resolver。
|
||||
- [ ] 补测试覆盖 locations parse、SSE 数据、前端字符串断言或 smoke。
|
||||
- [x] 更新本文件“执行记录”。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web acp_tool -- --test-threads=1
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --test-threads=1
|
||||
```
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-05-20 Reasonix Code 实现:
|
||||
- `acp_types.rs`: 新增 `ToolLocation` 结构体(含 `path` 字段),`SessionUpdate::ToolCall` 新增 `locations: Vec<ToolLocation>`(含反序列化支持)。
|
||||
- `acp_session_manager.rs`: `AcpSessionEvent::ToolCall` 新增 `locations: Vec<String>`,`session_update_to_event` 中映射 locations。
|
||||
- `acp_bridge.rs`: SSE `tool.started` 事件数据包含 `locations` 数组,桥测试中验证。
|
||||
- `layout.rs` 前端:SSE 消息解析提取 `locations`;`pageAiApplyToolEvent` 存储 `locations`;tool card 渲染显示位置路径与"打开"按钮;点击按钮调用 `openLocalResourceInActiveTab` 优先复用主编辑器资源 tab,失败则 fallback 为 `window.open`。
|
||||
- 测试覆盖:`test_session_update_tool_call_with_locations`(parse)、`acp_tool_events_keep_detail_for_collapsible_ui` 验证 SSE locations 字段、`page_ai_uses_backend_acp_session_runtime_store` 验证布局完整性。
|
||||
- Codex 复核:代码层已通过,但尚未做真实浏览器 smoke 点击 tool location 打开动作;因此“前端 smoke”保留未完成。
|
||||
- 验收通过:`cargo test -p mnote-web acp_bridge -- --test-threads=1`、`cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --test-threads=1`、`cargo test -p mnote-web acp -- --test-threads=1`。
|
||||
@@ -1,51 +0,0 @@
|
||||
# 7-33 ACP session info / plan UI checklist v1
|
||||
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):ACP session info / plan SSE 与 Page AI 轻量 UI 已完成;todo JSON 细化显示作为后续格式约定项。
|
||||
>
|
||||
> Owner:ACP metadata SSE + Page AI lightweight UI
|
||||
>
|
||||
> 参考:`reference-code/hermes-vscode-main/src/sessionManager.ts`、`src/webview/renderers.ts`
|
||||
|
||||
## 目标
|
||||
|
||||
把已解析但被丢弃的 `session_info_update` 和 `plan` 事件送到 Page AI UI:自动标题更新当前会话;plan/todo 以轻量状态面板或系统消息展示,不混入正文真相。
|
||||
|
||||
## 允许修改范围
|
||||
|
||||
- `rust/crates/mnote-web/src/acp_bridge.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- 相关 Rust 测试
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不改 ACP parser 类型,除非发现现有 PlanEntry 无法表达必要字段。
|
||||
- 不实现完整 Phase C review session。
|
||||
- 不把 plan/todo 写入页面正文。
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] `AcpSessionEvent::SessionInfoUpdate` 转成 `session.info.updated` SSE。
|
||||
- [x] `AcpSessionEvent::PlanUpdate` 转成 `plan.updated` SSE,data 包含 entries。
|
||||
- [x] Page AI 收到 session info 后更新当前 session title,并持久化 localStorage/local session view。
|
||||
- [x] Page AI 收到 plan 后渲染轻量 plan 状态(collapsible details/summary + 编号列表),不当作普通 assistant 正文。
|
||||
- [ ] tool raw output 中包含 todo JSON 时,可以复用 plan 区域或 tool card 状态显示。(风险可控,todo JSON 解析需额外确认格式约定;建议后续补)
|
||||
- [x] 补 bridge 单测和 `SIDEBAR_TREE_JS` 字符串断言。
|
||||
- [x] 更新本文件“执行记录”。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web acp_bridge -- --test-threads=1
|
||||
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --test-threads=1
|
||||
```
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-05-20 Reasonix:桥接 SessionInfoUpdate → `session.info.updated` SSE、PlanUpdate → `plan.updated` SSE。
|
||||
- 2026-05-20 Reasonix:前端 SSE handler 解析 session.info.updated 更新当前 session title 并持久化;plan.updated 在 pageAiMessages 末尾创建/更新 kind=plan 消息。
|
||||
- 2026-05-20 Reasonix:renderPageAiConversation 添加 kind===plan 渲染分支:collapsible details/summary 面板显示编号步骤列表。
|
||||
- 2026-05-20 Reasonix:acp_bridge 添加两个单测(session_info_update / plan_update);SIDEBAR_TREE_JS 添加 4 个断言。
|
||||
- 2026-05-20 Codex 复核:todo JSON 格式尚未确认,保留为未完成项;其余已通过 `cargo test -p mnote-web acp_bridge -- --test-threads=1`、`cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --test-threads=1`、`cargo test -p mnote-web acp -- --test-threads=1`。
|
||||
@@ -1,138 +0,0 @@
|
||||
# 7-34 [done] ACP runtime cleanup / availability / stability 尾项 checklist v1
|
||||
|
||||
> 创建时间:2026-05-21
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> 上位入口:`design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
>
|
||||
> 来源:`design/07-ai/process/7-29-batch-i-ai-tool-final-content-acp-tail-closure-v1.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本清单承接 `7-29` 的 Batch I 结论,只处理 ACP runtime 与 Hermes tool contract 的尾项收口:
|
||||
|
||||
1. 查清 `7-15` Step 16:旧 Hermes HTTP proxy 兼容路径是否仍有默认入口,哪些可以退役,哪些必须保留为 debug / compat。
|
||||
2. 查清 `7-12` Phase B/F:profile-level tool availability 是否在 manifest、UI、execute guard 三处一致。
|
||||
3. 给 `7-15` Step 15 / Step 17 拆出可执行的多会话稳定性验证和 Reasonix cache benchmark,不在本轮扩新 AI 产品面。
|
||||
|
||||
本清单不解冻 Phase C Review Mode,不新增审阅 UI,不把 `mnote.doc.markdown_edit` 升回 local-first 普通 Markdown 默认主路径。
|
||||
|
||||
## 2. 当前已知事实
|
||||
|
||||
- `7-27` 已归档,`mnote.doc.markdown_edit` 在线写回已以最终 markdown 为真源。
|
||||
- ACP Hermes / Reasonix 主链已具备基础运行能力,历史 `7-30` 到 `7-33` 的 session load、permission、tool location、plan UI 已归档。
|
||||
- `hermes_client.rs` 中仍可能保留旧 HTTP proxy 兼容分支;是否仍被默认 runtime 触达需要本轮确认。
|
||||
- `hermes_tools/manifest.rs` 已有 annotations / capabilityScope,但 profile-level dynamic availability 是否贯穿 UI 与 execute guard 仍需复核。
|
||||
|
||||
## 3. P0 Checklist
|
||||
|
||||
- [x] 旧 HTTP proxy 路径盘点完成:列出仍有调用者的 route / function / config,并标明 `default` / `compat` / `debug-only`。
|
||||
- [x] 确认 `page_ai_workflow.rs` 在 local-first 默认口径下不会绕过 ACP / tool executor 重新走旧 block-edit fallback。
|
||||
- [x] `7-15` 文档更新为“核心 ACP runtime 已完成,Step 15-17 拆到 7-34”,避免继续显示为整体未完成。
|
||||
- [x] `7-12` Phase B/F 状态更新:已覆盖项、冻结项、待实现项分开写清楚。
|
||||
|
||||
## 4. P1 Checklist
|
||||
|
||||
- [x] profile-level tool availability 设计落点明确:manifest enabled、UI disabled/hidden、execute guard 三处的同源判断写清。
|
||||
- [x] 多会话稳定性测试方案明确:至少覆盖 3 个并发 ACP session、cancel、子进程异常、事件去重。
|
||||
- [x] 多会话稳定性 smoke 最小脚本已落地:`scripts/task488-acp-multi-session-stability-smoke.js`,覆盖 session/run/events/abort API 与 SSE terminal event 证据。
|
||||
- [x] Reasonix cache benchmark 方案明确:对比 Hermes / Reasonix 的首次运行、二次运行、cache 命中提示和耗时采样。
|
||||
|
||||
## 5. P2 / 后续增强
|
||||
|
||||
- [ ] `7-27` 后续增强是否单独立项:`revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体删除。
|
||||
- [ ] 若旧 HTTP proxy 不能立即删除,补 `debug/internal only` 标记和 route 文档。
|
||||
- [x] profile tool availability 的 P1 代码缺口已完成最小修复:`execute_mnote_tool_call()` 在工具执行前校验调用方声明的 `capabilityScope` 覆盖 manifest 所需 scope;`mnote.page.get` 补齐只读 annotations。
|
||||
|
||||
## 6. Reasonix Worker 拆分
|
||||
|
||||
### Worker A:旧 HTTP proxy / fallback 调用链审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-j-worker-a-http-proxy-fallback.md`
|
||||
|
||||
只读范围:
|
||||
|
||||
- `rust/crates/mnote-web/src/routes/hermes_client.rs`
|
||||
- `rust/crates/mnote-web/src/page_ai_workflow.rs`
|
||||
- `rust/crates/mnote-web/src/acp_runtime.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/routes/mod.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- 不修改代码。
|
||||
- 输出旧 HTTP proxy / page AI fallback 的真实调用链、默认入口、环境变量或 profile 开关。
|
||||
- 分级为:必须退役 / compat 保留 / debug-only / 未被调用。
|
||||
|
||||
### Worker B:tool availability 三处一致性审查
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-j-worker-b-tool-availability.md`
|
||||
|
||||
只读范围:
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/manifest.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/*.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `rust/crates/mnote-web/src/routes/hermes_tools.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- 不修改代码。
|
||||
- 审查 manifest / UI / execute guard 是否消费同一能力判断。
|
||||
- 输出 P0 缺口和建议测试名,不直接实现。
|
||||
|
||||
### Worker C:ACP 稳定性与 benchmark checklist 草案
|
||||
|
||||
Owner:
|
||||
|
||||
- `.codex/reasonix-tasks/results/batch-j-worker-c-acp-stability-benchmark.md`
|
||||
|
||||
只读范围:
|
||||
|
||||
- `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
- `scripts/`
|
||||
- `rust/crates/mnote-web/src/acp_*.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- 不修改代码。
|
||||
- 给出多会话稳定性 smoke / benchmark 的最小脚本方案、浏览器验证点、输出证据格式。
|
||||
- 不要求立即实现压测脚本。
|
||||
|
||||
## 7. Codex 复核项
|
||||
|
||||
- [x] 等待 Worker A/B/C completion hook,单次最长 30 分钟,不短轮询。
|
||||
- [x] 独立核查 Worker 结论涉及的关键调用链。
|
||||
- [x] 更新本清单 P0/P1 状态。
|
||||
- [x] 若出现 P0 代码缺口,拆下一轮实现 worker 或由 Codex 本地最小修复。
|
||||
- [x] 跑 `git diff --check` 与相关 targeted tests。
|
||||
- [x] 更新 `1-8` Batch E 状态。
|
||||
|
||||
## 8. 本轮执行记录
|
||||
|
||||
- 2026-05-21:Codex 创建 `7-34`,准备派发 Worker A/B/C 做只读审查与下一步测试方案拆分。
|
||||
- 2026-05-21:Worker A/B/C 均通过 completion hook 回传。注意:Worker A 越过任务边界,声称同时写入 A/B/C 结果;Codex 已按结果文件和源码重新复核,不直接采信 Reasonix 汇总。
|
||||
- 2026-05-21:Codex 复核 `hermes_http_proxy_enabled()` / `is_acp_profile()` / route 调用链后确认:旧 HTTP proxy 默认关闭,ACP 是默认 runtime;`page_ai_workflow` 不经过 ACP session manager,但会进入共享 `execute_mnote_tool_call()`,受 profile disabled、workspace、shared-read 与写入授权守卫保护。本轮无 P0 退役阻塞。
|
||||
- 2026-05-21:Codex 复核 `execute_mnote_tool_call()`、`ensure_write_authorized()`、`is_read_tool()`、`disabled_mnote_tools()` 后确认:profile disabled list 在 listing / UI / execute guard 三处同源;`capabilityScope` 当前主要是 manifest / audit / runtime target 声明,尚无中心包含关系校验,列为 P1 实现项。
|
||||
- 2026-05-21:多会话稳定性与 Reasonix cache benchmark 已形成脚本方案;下一批可拆实现 worker,优先实现 `capabilityScope` 中心校验与 `scripts/task-acp-stability-smoke.js`,cache benchmark 作为 P2 度量。
|
||||
- 2026-05-21:Codex 完成 `capabilityScope` 中心校验最小实现:缺省 scope 兼容旧调用方,显式声明但不足时返回 `mnote_tool_capability_scope_forbidden`;写 scope 可覆盖同前缀 read scope。验证:`hermes_tools_call_rejects_declared_scope_that_does_not_cover_tool`、`hermes_tools_manifest_returns_first_batch_tools`、`markdown_edit`、`hermes_tools_manifest_describes_markdown_edit_write_contract` 均通过。
|
||||
- 2026-05-21:Codex 新增 `scripts/task488-acp-multi-session-stability-smoke.js`,先完成 `node --check`;真实执行依赖当前 `3000` 服务与选定 ACP runtime 可用,失败时会落 `tmp/acp-multi-session-stability-smoke/error.json`,成功时落 `result.json` 与每个 run 的事件 JSON。
|
||||
- 2026-05-21:Codex 修复 `task488` smoke 的 SSE 读取策略:从 Playwright `context.request.fetch` 改为 Node 原生 `fetch` + 登录 cookie,读取到 `run.completed` / `run.failed` / `run.aborted` terminal event 后主动关闭流,避免 Playwright context 关闭造成假失败。
|
||||
- 2026-05-21:Codex 修复 ACP abort API 的阻塞风险:`abort_run` 对 `session/cancel` notification 采用 2500ms best-effort 超时,并主动向当前 run 的 SSE channel 推送 `run.aborted`;后续 prompt 结束时若 runtime 已 abort,不会再覆盖为 completed。
|
||||
- 2026-05-21:真实 3000 smoke 已通过:`node scripts/task488-acp-multi-session-stability-smoke.js` 创建 3 个 `reasonix` ACP session / run,取消第 2 个 run,最终事件为 `run.completed`、`run.aborted`、`run.completed`;证据写入 `tmp/acp-multi-session-stability-smoke/result.json` 和 `events-*.json`。
|
||||
|
||||
## 9. 归档说明
|
||||
|
||||
本清单 P0/P1 已完成并有代码与 smoke 证据,归档到 `design/07-ai/done/`。
|
||||
|
||||
仍保留为后续增强的事项:
|
||||
|
||||
- `7-27` 的 `revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体删除。
|
||||
- 旧 Hermes HTTP proxy 的更细 debug/internal only 文档标记。
|
||||
- Reasonix cache benchmark 的长期耗时采样;当前只冻结方案,不作为 1-8 Batch E 阻塞项。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,455 +0,0 @@
|
||||
# 7-41 Page AI Hermes / Reasonix user profile isolation v2
|
||||
|
||||
> 创建时间:2026-05-29
|
||||
>
|
||||
> 状态:`done`
|
||||
>
|
||||
> Owner:Page AI agent identity / Hermes profile policy / Reasonix memory policy
|
||||
>
|
||||
> 上位依据:
|
||||
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
|
||||
> - `design/07-ai/done/7-40-page-ai-context-envelope-and-run-receipt-v1.md`
|
||||
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
|
||||
> - `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md`
|
||||
>
|
||||
> 参考依据:
|
||||
> - Hermes 官方 profile 文档:profile 是独立 Hermes home,包含 `config.yaml`、`.env`、`SOUL.md`、memories、sessions、skills、cron、state database、gateway state。
|
||||
> - Hermes WebUI:技能 toggle 直接写 profile `config.skills.disabled`。
|
||||
> - Hermes VSCode:只管理会话和一次性上下文建议,不提供真正的 per-user skill disable。
|
||||
> - PilotDeck:以 WorkSpace 为边界隔离文件、记忆和技能,并强调白盒记忆可追溯。
|
||||
> - Reasonix 文档/本地实现:默认使用 `~/.reasonix/memory/global` 与 `~/.reasonix/memory/<project-hash>`,`REASONIX_MEMORY=off|false|0` 可关闭 memory 注入。
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`7-39` 已把 Page AI UI 收口为 agent selector + contextRefs + SQLite per-user preference。`7-40` 进一步把 MNote skill library 定位为 agent 可按需读取的能力摘要,而不是每轮强行注入 prompt。
|
||||
|
||||
但当前 Hermes skill/profile 的真实边界仍不正确:
|
||||
|
||||
- Hermes 的 `skills.disabled` 是 profile 级配置,不是 MNote 用户级偏好。
|
||||
- Hermes profile 自带 `SOUL.md`、memory、session、skills 和 state;多个 MNote 用户共用同一个可写 profile,会把个人偏好、记忆和技能配置混在一起。
|
||||
- 当前 Page AI UI 已经能展示 Hermes skill toggle,但普通用户如果直接写共享 Hermes profile,就会影响其它用户。
|
||||
- Reasonix 虽然不像 Hermes 那样有 SOUL 人格,但它也有 global/project memory。默认共享 `~/.reasonix` 时,不能假设完全无状态。
|
||||
|
||||
因此本稿把 Page AI agent identity 从“选择 Hermes / Reasonix”提升为“选择 agent + profile scope + memory policy”。
|
||||
|
||||
## 2. 产品决策
|
||||
|
||||
### 2.1 Hermes profile 分层
|
||||
|
||||
Hermes profile 分为两类:
|
||||
|
||||
| profile kind | owner | 普通用户能否使用 | 普通用户能否改 skill | memory/session |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `personal` | 单个 MNote 用户 | 能 | 能,仅限自己的 profile | 用户独立 |
|
||||
| `shared` | 系统 / 管理员 | 能,若管理员公开 | 不能 | 默认不写入用户私有长期记忆 |
|
||||
|
||||
初始共享 Hermes profile 只包含 `lite`。
|
||||
|
||||
要求:
|
||||
|
||||
- 每个 MNote 用户可以访问自己的 personal Hermes profile。
|
||||
- 每个 MNote 用户可以访问被管理员公开的 shared Hermes profile。
|
||||
- shared Hermes profile 的 skill/config 只有管理员能改。
|
||||
- personal Hermes profile 的 skill/config 只有该 profile owner 或管理员能改。
|
||||
- Page AI 发送请求时必须携带解析后的 `agentProfileRef`,不能只携带裸 `profile=lite`。
|
||||
|
||||
### 2.2 MNote 内置技能开关
|
||||
|
||||
MNote 内置 skill / tool 属于 MNote 自己的能力面,普通用户应该可以按用户启停。该开关不写 Hermes profile,而是写 SQLite control-plane。
|
||||
|
||||
含义:
|
||||
|
||||
- `mnote_builtin_skill_enabled` 是 per-user 偏好和服务端执行策略,必须按当前 SQLite 用户隔离。
|
||||
- `hide_builtin_skills=true` 只是 UI 展示偏好,用于折叠或隐藏 MNote 内置能力说明;它不等于禁用。
|
||||
- MNote tool 是否可调用由服务端根据 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 决定。
|
||||
- 普通用户可启停自己的 MNote 内置 skills;管理员可设置默认值或全局禁用策略。
|
||||
- MNote 内置 skill 开关不修改 Hermes profile `config.yaml`,也不影响 shared Hermes profile。
|
||||
|
||||
### 2.3 Hermes profile skills
|
||||
|
||||
可以配置的只有 Hermes profile 自己的 skills。
|
||||
|
||||
规则:
|
||||
|
||||
- personal profile:用户可启停该 profile 下的 Hermes skills,写入该 personal profile 的 `config.yaml`。
|
||||
- shared profile:普通用户只读;管理员可启停 shared profile skills。
|
||||
- skill toggle API 必须做服务端权限检查,不能只靠 UI 禁用按钮。
|
||||
- MNote 不再把 `ai.agent.hermes.profile.<name>.skills.enabled` 当成普通用户对共享 profile 的安全开关;MNote 内置 skill 的 per-user 开关应使用独立 SQLite key。
|
||||
|
||||
### 2.4 Reasonix memory policy
|
||||
|
||||
Reasonix 默认关闭 memory 注入。
|
||||
|
||||
规则:
|
||||
|
||||
- 默认启动 Reasonix ACP 时设置 `REASONIX_MEMORY=off`。
|
||||
- 每个 MNote 用户可在设置中开启 Reasonix memory。
|
||||
- 该设置保存到 SQLite `user_ui_preferences`,按用户隔离。
|
||||
- 开启后,UI 需明确标注 Reasonix 将使用 global/project memory。
|
||||
- 第一阶段不强制给 Reasonix 每用户独立 HOME;若后续需要更强隔离,再增加 `reasonix_home_mode=per_user`。
|
||||
|
||||
## 3. 数据合同
|
||||
|
||||
### 3.1 Agent profile ref
|
||||
|
||||
Page AI payload 中新增或固定以下结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "hermes",
|
||||
"agentProfileRef": {
|
||||
"kind": "personal",
|
||||
"profileId": "usr_123_default",
|
||||
"ownerUserId": "usr_123",
|
||||
"baseProfile": "default",
|
||||
"displayName": "我的 Hermes"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
shared profile:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "hermes",
|
||||
"agentProfileRef": {
|
||||
"kind": "shared",
|
||||
"profileId": "shared_lite",
|
||||
"ownerUserId": null,
|
||||
"baseProfile": "lite",
|
||||
"displayName": "Lite"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务端要求:
|
||||
|
||||
- 不接受浏览器直接提交任意 Hermes filesystem path。
|
||||
- `profileId` 必须由 SQLite control-plane 解析到当前用户可访问的 profile。
|
||||
- 对 personal profile,当前用户必须是 owner 或 admin。
|
||||
- 对 shared profile,必须是公开 profile 或 admin。
|
||||
- 所有 run / skill list / skill toggle / settings read 都通过 `agentProfileRef` 解析,不走裸 profile name。
|
||||
|
||||
### 3.2 SQLite control-plane
|
||||
|
||||
建议新增控制面表,避免只用偏好 key 表达权限关系:
|
||||
|
||||
```text
|
||||
ai_agent_profiles
|
||||
- id
|
||||
- agent_id # hermes
|
||||
- profile_kind # personal | shared
|
||||
- owner_user_id # personal 必填,shared 为空
|
||||
- base_profile_name # Hermes 原始 profile 名或模板名
|
||||
- isolated_profile_name # MNote 管理后的真实 Hermes profile 名
|
||||
- display_name
|
||||
- status # active | disabled
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
ai_agent_profile_grants
|
||||
- profile_id
|
||||
- user_id
|
||||
- role # owner | user | admin
|
||||
- can_run
|
||||
- can_manage_skills
|
||||
- can_manage_config
|
||||
- created_at
|
||||
- updated_at
|
||||
```
|
||||
|
||||
第一阶段也可以在现有 `user_ui_preferences` 中保存默认选择:
|
||||
|
||||
- `ai.agent.hermes.default_profile_id`
|
||||
- `ai.agent.hermes.hide_builtin_skills`
|
||||
- `ai.agent.mnote_builtin.skills.enabled`
|
||||
- `ai.agent.reasonix.memory_enabled`
|
||||
|
||||
但 profile 权限、owner、shared/personal 类型不应只存在于 UI preference。
|
||||
|
||||
MNote 内置 skill 的用户级开关可以第一阶段存在 `user_ui_preferences`,但服务端 tool policy 读取时必须视为执行策略,而不是纯 UI 状态。若后续需要审计、管理员默认值或组织策略,应升级为独立表:
|
||||
|
||||
```text
|
||||
ai_user_skill_preferences
|
||||
- user_id
|
||||
- skill_id
|
||||
- enabled
|
||||
- updated_at
|
||||
```
|
||||
|
||||
### 3.3 Hermes profile provisioning
|
||||
|
||||
创建 personal Hermes profile 时:
|
||||
|
||||
- 可从共享模板复制 `config.yaml`、`.env`、`SOUL.md` 和 skills。
|
||||
- 不复制 memories、sessions、state database、gateway state。
|
||||
- 生成的真实 profile name 必须包含 MNote 用户隔离标识,例如 `mnote-u-<userId>-default`。
|
||||
- provisioning 过程由服务端执行,并记录到 SQLite control-plane。
|
||||
|
||||
shared `lite`:
|
||||
|
||||
- 初始由管理员登记为 `shared_lite`。
|
||||
- 普通用户只可 run / list readonly。
|
||||
- 管理员可改 skill/config。
|
||||
|
||||
## 4. API 合同
|
||||
|
||||
### 4.1 Profile list
|
||||
|
||||
`GET /api/ai/agent-profiles?agentId=hermes`
|
||||
|
||||
返回当前用户可访问 profiles:
|
||||
|
||||
```json
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"profileId": "usr_123_default",
|
||||
"kind": "personal",
|
||||
"displayName": "我的 Hermes",
|
||||
"canRun": true,
|
||||
"canManageSkills": true
|
||||
},
|
||||
{
|
||||
"profileId": "shared_lite",
|
||||
"kind": "shared",
|
||||
"displayName": "Lite",
|
||||
"canRun": true,
|
||||
"canManageSkills": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Skill list
|
||||
|
||||
`GET /api/hermes/client/skills?profileId=...`
|
||||
|
||||
要求:
|
||||
|
||||
- personal profile 返回可 toggle 状态。
|
||||
- shared profile 对普通用户返回 readonly 状态。
|
||||
- MNote 内置 skills 返回值必须标记 `builtin=true`、`configurable=true`、`configScope=user_sqlite`。
|
||||
- Hermes profile skills 标记 `builtin=false`、`configurable=canManageSkills`。
|
||||
|
||||
### 4.3 Skill toggle
|
||||
|
||||
`PUT /api/hermes/client/skills/toggle`
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"profileId": "usr_123_default",
|
||||
"skillName": "writer",
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
服务端必须:
|
||||
|
||||
- 解析 `profileId`。
|
||||
- 若 `skillKind=mnote_builtin`,写当前用户 SQLite skill preference,不写 Hermes profile。
|
||||
- 若 `skillKind=hermes_profile`,校验 `canManageSkills=true`。
|
||||
- 拒绝普通用户修改 shared profile 的 Hermes profile skills。
|
||||
- personal profile skill toggle 只写目标 Hermes profile 的 `config.yaml`。
|
||||
|
||||
错误码建议:
|
||||
|
||||
- `ai_profile_not_found`
|
||||
- `ai_profile_forbidden`
|
||||
- `ai_profile_readonly`
|
||||
- `ai_builtin_skill_preference_failed`
|
||||
- `hermes_skill_toggle_failed`
|
||||
|
||||
### 4.4 Reasonix run
|
||||
|
||||
Reasonix ACP spawn / session create 需读取当前用户设置:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "reasonix",
|
||||
"memoryPolicy": {
|
||||
"enabled": false,
|
||||
"source": "user_ui_preferences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
默认:
|
||||
|
||||
- `enabled=false`
|
||||
- 子进程环境包含 `REASONIX_MEMORY=off`
|
||||
|
||||
开启:
|
||||
|
||||
- 不设置 `REASONIX_MEMORY=off`,或设置为 `on`
|
||||
- UI 明确展示 memory 已开启
|
||||
|
||||
## 5. UI 设计
|
||||
|
||||
### 5.1 Agent selector
|
||||
|
||||
Hermes agent 下增加 profile 子选择:
|
||||
|
||||
```text
|
||||
Hermes
|
||||
我的 Hermes personal · 可配置
|
||||
Lite shared · 只读
|
||||
```
|
||||
|
||||
显示规则:
|
||||
|
||||
- personal profile 显示“可配置”。
|
||||
- shared profile 显示“共享 / 只读”。
|
||||
- 若普通用户选择 shared profile,技能开关显示为只读。
|
||||
- 管理员选择 shared profile,技能开关可用,并显示“管理员正在修改共享 profile”。
|
||||
|
||||
### 5.2 Skills panel
|
||||
|
||||
三组仍保留:
|
||||
|
||||
- MNote 内置技能:可折叠,可隐藏/显示,也可由普通用户按自己账号启停。
|
||||
- Hermes 技能:随当前 Hermes profile 变化;personal 可配置,shared 普通用户只读。
|
||||
- Reasonix 技能:展示可用能力;memory 是单独设置,不混入 skill toggle。
|
||||
|
||||
Hermes profile 切换时:
|
||||
|
||||
- 必须重新加载 profile skills。
|
||||
- 必须清空旧 profile skill cache。
|
||||
- `hide_builtin_skills` 不随 Hermes profile 改变;它是用户 UI 偏好。
|
||||
- MNote 内置 skill enable/disable 不随 Hermes profile 改变;它是当前 MNote 用户的 SQLite policy。
|
||||
|
||||
### 5.3 Settings
|
||||
|
||||
设置页拆分:
|
||||
|
||||
- Common:授权区域、contextRefs 默认值、内置技能显示/隐藏、内置技能启停。
|
||||
- Hermes:默认 Hermes profile、personal profile 管理、shared profile 只读/管理员管理。
|
||||
- Reasonix:默认关闭 memory;用户可开启。
|
||||
- Chat-only:只聊天配置。
|
||||
|
||||
## 6. 非目标
|
||||
|
||||
- 不让普通用户直接修改 shared Hermes profile。
|
||||
- 不把 shared Hermes profile 用作沉淀个人偏好的长期人格。
|
||||
- 不在本阶段实现 Reasonix per-user HOME;只实现默认 memory off 与可选开启。
|
||||
- 不新增第二套目录授权真相;文件访问仍由 SQLite directory grants / allowedRoots 控制。
|
||||
- 不实现 PilotDeck 的完整 router、always-on 或 memory engine。
|
||||
|
||||
## 7. Checklist
|
||||
|
||||
### Batch A - 现状冻结与风险取证
|
||||
|
||||
- [x] 复核当前 Page AI Hermes skill toggle 的真实写入路径,确认是否直接写 Hermes profile `config.yaml`。
|
||||
- 证据:`/api/hermes/client/skills/toggle` 进入 `toggle_skill`,读取 `profile/name/enabled` 后调用 `set_skill_enabled(profile, name, enabled)`;`set_skill_enabled` 直接写 `profile_home(profile)/config.yaml` 中的 `skills.disabled`。当前没有 profile grant / shared readonly 检查。
|
||||
- [x] 复核当前 UI preference 中 `hide_builtin`、MNote 内置 skill enabled、profile skill enabled、default profile 的存储键。
|
||||
- 证据:前端仍使用 `ai.agent.hermes.profile_id` 保存默认 Hermes profile;Reasonix skill 开关写 `ai.agent.reasonix.skills.enabled`;Hermes profile skill 开关写 `ai.agent.hermes.profile.<profile>.skills.enabled`;隐藏内置技能写 `ai.agent.hermes.profile.<profile>.skills.hide_builtin`。这些 key 是 UI preference,不等价于 profile 授权模型。
|
||||
- [x] 复核 Reasonix ACP spawn 环境,确认当前是否默认注入 memory。
|
||||
- 证据:`AcpRuntimeConfig::reasonix` 默认 env 为空;Reasonix wrapper 构造 `CacheFirstLoop` 时未传 memory policy;本地 Reasonix 实现支持 `REASONIX_MEMORY=off|false|0` 关闭 memory,但 MNote 当前没有默认注入。
|
||||
- [x] 形成 RED 证据:普通用户修改 shared profile skill 会影响其它用户,或当前缺少服务端权限边界。
|
||||
- RED:当前服务端 skill toggle 只信任请求中的裸 `profile`,没有 `profileId -> SQLite resolver -> canManageSkills` 边界;如果 UI 选择 shared `lite` 并发起 toggle,会直接写 shared Hermes profile config,影响所有共享使用者。
|
||||
- [x] 验证:Rust/JS 只读审计记录在本文档或后续 checklist evidence 中。
|
||||
- 已完成主线程 `rg` 取证,并由两个只读 subagent 对照 Hermes/Hermes WebUI/Hermes VSCode/PilotDeck/Reasonix 与 MNote 当前 Rust/JS 入口;未修改代码,未运行破坏性命令。
|
||||
|
||||
### Batch B - SQLite profile policy 合同
|
||||
|
||||
- [x] 新增或扩展 SQLite control-plane profile policy:`ai_agent_profiles` / `ai_agent_profile_grants` 或等价结构。
|
||||
- 证据:新增 `007-ai-agent-profile-policy.sql`,并在 `control-plane` store/model/sqlite 中增加 `AiAgentProfile*` 合同与 resolver。
|
||||
- [x] 初始化 shared Hermes profile:仅 `lite`,普通用户 `canRun=true`、`canManageSkills=false`。
|
||||
- 证据:`ensure_ai_agent_profile_policy` 初始化 `shared_lite`,普通用户 grant 为 run-only。
|
||||
- [x] 为每个用户 provision personal Hermes profile。
|
||||
- 证据:当前用户首次查询时生成 `usr_<user>_default` 与 `mnote-u-<user>-default` isolated profile。
|
||||
- [x] 补 Rust 定点测试:personal owner、shared readonly、admin manage、跨用户不可管理。
|
||||
- 证据:`control-plane sqlite::tests::ai_agent_profile_policy_provisions_personal_and_shared_boundaries`。
|
||||
- [x] 验证:不同用户查询 profile list 只返回自己 personal + shared lite。
|
||||
- 证据:`mnote-web routes::hermes_client::tests::page_ai_agent_profiles_are_sqlite_user_scoped`。
|
||||
|
||||
### Batch C - Hermes profile resolver
|
||||
|
||||
- [x] 新增服务端 `agentProfileRef` resolver,禁止前端提交任意 Hermes path。
|
||||
- 证据:新增 `/api/ai/agent-profiles` 与 `profileId -> SQLite policy -> isolatedProfile` resolver;浏览器不提交 filesystem path。
|
||||
- [x] `/api/hermes/client/runs` 从 `profileId` 解析真实 Hermes profile。
|
||||
- 证据:Hermes run payload 在服务端 stamp `profile/profileId/agentProfileRef`,ACP Hermes 使用 isolated profile。
|
||||
- [x] `/api/hermes/client/skills` 从 `profileId` 解析真实 Hermes profile。
|
||||
- 证据:Hermes skills catalog 按 `profileId` 解析并返回 `agentProfileRef/configurable/readonly/configScope`。
|
||||
- [x] 保留旧 `profile=` 参数只作为兼容入口,并映射到当前用户可访问 profile。
|
||||
- 证据:resolver 只接受 `profileId/profile_id/profile` 中能映射到当前用户可访问 policy 的 id、isolated name、display name、`lite`、`mnoteai` 或 personal default。
|
||||
- [x] 验证:旧路径兼容不允许越权访问 shared/admin profile。
|
||||
- 证据:普通用户 toggle `shared_lite` Hermes skill 返回 `403 ai_profile_readonly`。
|
||||
|
||||
### Batch D - Skill toggle 权限收口
|
||||
|
||||
- [x] 修改 skill toggle API:只接受 `profileId + skillName + enabled`。
|
||||
- 证据:服务端接受 `profileId/name/enabled/skillKind`;旧 `profile` 仅作 resolver 兼容。
|
||||
- [x] 区分 `mnote_builtin` 与 `hermes_profile` skill kind。
|
||||
- [x] MNote 内置 skill toggle 写当前用户 SQLite preference。
|
||||
- [x] 拒绝普通用户修改 shared profile 的 Hermes profile skills。
|
||||
- [x] personal profile skill toggle 只写该用户 isolated profile `config.yaml`。
|
||||
- [x] shared profile skill toggle 仅 admin 可写。
|
||||
- 证据:control-plane admin grant 可管理 shared;普通用户 shared readonly。
|
||||
- [x] personal Hermes profile 初始 skill 收口为最小白名单。
|
||||
- 证据:MNote 管理的 `mnote-u-*-default` profile 首次访问 skills 时写入 `mnotePersonalSkillBaseline: v1`,但不把未复制的 skill 写进 `skills.disabled`。
|
||||
- 2026-05-30 修正:personal Hermes 初始状态改为 profile 模板 copy 语义,首次 provision 只把 `vpn` / `zhihu-search` / `global-search` 复制到该 profile 自己的 `skills/` 目录;后续用户可以继续给自己的 personal profile 增加其它 skill 并启停,不做长期强白名单。`skills.disabled` 只表示该 profile 已有 skill 的关闭状态,不表示模板范围。
|
||||
- [x] 验证:Rust API 测试覆盖 `ai_profile_readonly`、MNote 内置 skill per-user toggle、personal profile skill success。
|
||||
- 证据:`mnote-web routes::hermes_client::tests::page_ai_skill_toggle_respects_builtin_user_policy_and_shared_readonly` 覆盖 personal 默认 skill 白名单。
|
||||
|
||||
### Batch E - MNote 内置 skill per-user policy
|
||||
|
||||
- [x] 将 MNote 内置 skill enable/disable 保存为 SQLite per-user policy。
|
||||
- [x] UI 中 `hide_builtin_skills` 只影响展示,不影响 enable/disable。
|
||||
- 证据:隐藏 key 收口为 `ai.agent.hermes.skills.hide_builtin`;MNote 内置 enable 使用独立 `ai.agent.mnote_builtin.skill.<id>.enabled`。
|
||||
- [x] Skills panel 标记 `builtin=true`、`configurable=true`、`configScope=user_sqlite`。
|
||||
- [x] 服务端 MNote tool policy 按 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 判断。
|
||||
- 证据:`create_run` 服务端读取当前用户 SQLite preference 并覆盖 `skillPreferences.mnote`,不信任浏览器提交。
|
||||
- [x] 验证:用户 A 禁用某内置 skill 不影响用户 B;禁用后对应 MNote tool 被服务端拒绝;隐藏展示不影响 enable 状态。
|
||||
- 证据:per-user SQLite preference 测试覆盖用户隔离;本批新增服务端 run policy 覆盖禁用状态进入 capability policy。工具调用显式拒绝仍可后续细化到每个 tool handler。
|
||||
|
||||
### Batch F - Reasonix memory policy
|
||||
|
||||
- [x] 新增 per-user 设置 `ai.agent.reasonix.memory_enabled`,默认 `false`。
|
||||
- [x] Reasonix ACP spawn 默认设置 `REASONIX_MEMORY=off`。
|
||||
- [x] 开启 memory 后不注入 `REASONIX_MEMORY=off`,并在 UI 显示 memory enabled。
|
||||
- 证据:开启后服务端注入 `REASONIX_MEMORY=on`;UI Reasonix 设置页显示 memory 状态。
|
||||
- [x] 补 JS/Rust 测试或 smoke,覆盖默认 off、用户开启、不同用户隔离 preference。
|
||||
- 证据:`mnote-web routes::hermes_client::tests::reasonix_memory_policy_defaults_off_and_reads_user_preference`。
|
||||
- [x] 验证:普通消息由 Reasonix 自己决定是否使用工具;MNote 不再强行注入 memory/context 正文。
|
||||
- 证据:Page AI 仍只发送 contextRefs/allowedRoots/skillPreferences envelope;Reasonix memory 只通过 env policy 控制。
|
||||
|
||||
### Batch G - UI 收口
|
||||
|
||||
- [x] Agent selector 中 Hermes profile 显示 personal/shared/readonly 状态。
|
||||
- [x] Skills panel 用单一技能来源下拉收口为 `mnote` / `reasonix` / `Hermes_user` / `hermes_lite`,选中哪个只显示哪个来源的 skills。
|
||||
- [x] Hermes profile 切换必须刷新 skill catalog,避免旧 profile skill 残留。
|
||||
- [x] shared profile 的 Hermes profile skills 对普通用户展示只读开关或锁定状态。
|
||||
- [x] MNote 内置 skills 对普通用户展示可启停状态,并标明按当前 MNote 用户保存。
|
||||
- [x] 管理员对 shared profile 显示可管理状态,并提示影响所有用户。
|
||||
- [x] 验证:真实浏览器截图覆盖 MNote 内置 skill per-user 可配置、personal Hermes skill 可配置、shared Hermes skill 只读、admin shared 可配置。
|
||||
- 证据:`scripts/task502-page-ai-agent-selector-context-smoke.js` 通过;截图 `tmp/task502-page-ai-agent-selector-context-smoke/00-skills-panel.png` 显示技能来源下拉与 `Hermes_user · 我的 Hermes` 单来源 skill 列表。
|
||||
- 2026-05-30 证据:临时 `dev:hot` + 真实浏览器截图 `tmp/task-page-ai-hermes-personal-template-copy/personal-hermes-template-skills.png`;`Hermes_user · 我的 Hermes` 初始模板只显示 `global-search`、`vpn`、`zhihu-search`,不显示 `writer` / `officecli`。Rust 测试覆盖“初始 3 个 skill,profile 后续新增 `writer` 后可开启并出现在 catalog”。
|
||||
|
||||
### Batch H - 回归矩阵与文档收尾
|
||||
|
||||
- [x] 更新 Page AI 设计说明,明确 MNote 内置 skill per-user policy、personal/shared Hermes profile 与 Reasonix memory policy。
|
||||
- [x] 更新 smoke:agent 切换、MNote 内置 skill per-user toggle、Hermes profile 切换、shared readonly、personal skill toggle、Reasonix memory off/on。
|
||||
- [x] 运行 `node --check` 覆盖相关 browser runtime / smoke。
|
||||
- [x] 运行 Rust 定点测试覆盖 SQLite profile policy 与 Hermes skill toggle 权限。
|
||||
- [x] 运行真实浏览器验证并截图。
|
||||
- [x] 运行 `git diff --check`。
|
||||
- [x] 涉及代码图后运行 `codegraph sync .`。
|
||||
- [x] 完成后将本 checklist 移动到 `done/` 或标记为 `done`。
|
||||
- 状态已标记为 `done`;2026-06-01 已迁入 `design/07-ai/done/`。
|
||||
|
||||
## 8. 验收口径
|
||||
|
||||
完成后必须满足:
|
||||
|
||||
- 普通用户可以启停自己的 MNote 内置 skills,开关保存到 SQLite 并由服务端 tool policy 执行。
|
||||
- 用户 A 的 MNote 内置 skill 开关不影响用户 B。
|
||||
- 普通用户无法修改 shared `lite` 的 Hermes profile skills。
|
||||
- 普通用户可以修改自己的 personal Hermes profile skills。
|
||||
- MNote 内置 skill 的显示/隐藏与启停是两个不同状态:隐藏只影响 UI,启停影响服务端可调用性。
|
||||
- Hermes profile 切换后 skill catalog 正确刷新。
|
||||
- Reasonix 默认 memory off,开启 memory 是 per-user preference。
|
||||
- Page AI run payload 不再用裸 Hermes profile name 表达身份,而是经服务端解析的 `agentProfileRef` / `profileId`。
|
||||
- 所有文件访问仍受 SQLite directory grants / allowedRoots 约束。
|
||||
@@ -1,292 +0,0 @@
|
||||
# 7-44 ChatOnly Doubao session binding v1
|
||||
|
||||
> 创建时间:2026-05-31
|
||||
>
|
||||
> 状态:`done`
|
||||
>
|
||||
> Owner:Page AI ChatOnly / Hermes ACP runtime / SQLite control-plane / OpenClaw Doubao Web provider
|
||||
>
|
||||
> 上位依据:
|
||||
> - `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
> - `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
|
||||
> - `design/07-ai/done/7-33-acp-session-info-plan-ui-checklist-v1.md`
|
||||
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
|
||||
> - `design/07-ai/done/7-41-page-ai-hermes-reasonix-user-profile-isolation-v2.md`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
MNote Page AI 的 `Chat-only / 豆包` 当前已经能通过 Hermes ACP -> OpenClaw `doubao-web` provider 调用豆包网页,但 MNote 会话与豆包网页会话还没有稳定统一:
|
||||
|
||||
- MNote 的会话主记录在 SQLite `ai_runtime_runs` / `ai_runtime_events`,按 `user_id + workspace_id + session_id` 查询和软删除。
|
||||
- Hermes / ACP 层有 `acpSessionId`,MNote 会通过 `session.info.updated` 持久化它。
|
||||
- OpenClaw `doubao-web` provider 会从豆包 SSE 中捕获 `conversation_id`,并在进程内 `sessionMap` 中用 ACP `sessionId` 复用豆包会话。
|
||||
- 该 `sessionMap` 没有落 SQLite。MNote 重启、OpenClaw 重启、跨用户、删除会话时,都无法可靠知道某个 MNote ChatOnly session 对应哪条豆包网页 conversation。
|
||||
|
||||
2026-05-31 已验证 ChatOnly 豆包重复会话的直接根因是 Hermes `title_generation` 复用了豆包主模型;当前已在 `openclaw-doubao-chat` profile 禁用标题生成辅助调用。但这只解决“一条消息变两条豆包会话”的触发点,不解决会话生命周期统一。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
本阶段目标是让 MNote 成为 ChatOnly 豆包会话的本地控制面:
|
||||
|
||||
- MNote ChatOnly session 与豆包 `conversation_id` 建立持久绑定。
|
||||
- 后续同一个 MNote session 继续发送时,复用同一个豆包 conversation。
|
||||
- 删除 MNote session 时,对豆包远端 conversation 做最佳努力删除。
|
||||
- 多用户会话隔离以 SQLite `user_id` 为准,不能只依赖 OpenClaw 全局内存。
|
||||
- 删除失败不能阻塞 MNote 本地删除,但必须可审计、可重试。
|
||||
|
||||
非目标:
|
||||
|
||||
- 不把豆包网页作为 MNote 会话真源。
|
||||
- 不同步豆包网页中用户手工创建的所有历史会话。
|
||||
- 不承诺豆包接口稳定可用;远端删除属于 provider-specific best effort。
|
||||
- 不在 ChatOnly 下申请 MNote 文件写权限。
|
||||
|
||||
## 3. 豆包侧接口取证
|
||||
|
||||
### 3.1 发送 / 继续会话
|
||||
|
||||
当前 OpenClaw `DoubaoWebClientBrowser` 发送消息使用:
|
||||
|
||||
```text
|
||||
POST /samantha/chat/completion
|
||||
```
|
||||
|
||||
请求体关键字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"completion_option": {
|
||||
"need_create_conversation": false,
|
||||
"is_delete": false
|
||||
},
|
||||
"conversation_id": "38428454119180290"
|
||||
}
|
||||
```
|
||||
|
||||
当 `conversation_id` 为空或 `"0"` 时,豆包创建新 conversation。响应 SSE 中会出现 `conversation_id`,OpenClaw 已能解析并打印:
|
||||
|
||||
```text
|
||||
[Doubao Web Browser] Captured conversation_id: ...
|
||||
```
|
||||
|
||||
### 3.2 删除会话
|
||||
|
||||
从豆包当前 Web UI 已加载脚本和 CDP 请求监听确认,删除会话优先走 IM cmd 链路:
|
||||
|
||||
```text
|
||||
POST /im/conversation/batch_del_user_conv
|
||||
```
|
||||
|
||||
请求头关键字段:
|
||||
|
||||
```text
|
||||
content-type: application/json; encoding=utf-8
|
||||
accept: application/json, text/plain, */*
|
||||
agw-js-conv: str
|
||||
```
|
||||
|
||||
请求体结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd": 4171,
|
||||
"uplink_body": {
|
||||
"batch_delete_user_conversation_uplink_body": {
|
||||
"conversation_id": ["38428454119180290"],
|
||||
"delete_all": false,
|
||||
"conversation_type": 3
|
||||
}
|
||||
},
|
||||
"sequence_id": "uuid",
|
||||
"channel": 2,
|
||||
"version": "1"
|
||||
}
|
||||
```
|
||||
|
||||
其中 `conversation_type: 3` 对应 `ONE_TO_BOT_CHAT`。
|
||||
|
||||
脚本中还存在旧 wrapper:
|
||||
|
||||
```text
|
||||
POST /samantha/im/conversation/batch_delete
|
||||
```
|
||||
|
||||
但当前删除弹窗路径使用 `/im/conversation/batch_del_user_conv`,实现优先采用该路径。
|
||||
|
||||
## 4. 数据合同
|
||||
|
||||
新增 SQLite 控制面表:
|
||||
|
||||
```text
|
||||
ai_external_conversation_bindings
|
||||
- id
|
||||
- user_id # 当前 MNote 用户,必填
|
||||
- workspace_id # 可空,但列表/删除必须按上下文过滤
|
||||
- mnote_session_id # MNote ChatOnly session_id
|
||||
- acp_session_id # Hermes/OpenClaw ACP sessionId,可空,收到后补齐
|
||||
- agent_id # chat_only
|
||||
- profile # openclaw-doubao-chat 等
|
||||
- provider # doubao-web
|
||||
- remote_conversation_id # 豆包 conversation_id
|
||||
- remote_url # https://www.doubao.com/chat/{remote_conversation_id}
|
||||
- status # active | local_deleted | remote_deleted | remote_delete_failed
|
||||
- metadata_json # 捕获来源、失败原因、最后响应摘要
|
||||
- created_at
|
||||
- updated_at
|
||||
- deleted_at
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `user_id + provider + remote_conversation_id` 唯一,避免同一豆包会话绑定给多个 MNote 用户。
|
||||
- `user_id + mnote_session_id + provider` 唯一,避免一个 MNote session 绑定多条豆包远端会话。
|
||||
- 查询、恢复、删除都必须带当前 `user_id`;不能只按 `mnote_session_id` 查。
|
||||
|
||||
## 5. 数据流
|
||||
|
||||
### 5.1 创建 MNote ChatOnly session
|
||||
|
||||
1. 前端 `pageAiEnsureHermesSession(forceCreate=true)` 调 `/api/hermes/client/sessions`。
|
||||
2. mnote-web 写入 SQLite session index run,`status=session.created`。
|
||||
3. 不立即创建豆包远端 conversation。
|
||||
4. 绑定表暂不写,或写入一行 `remote_conversation_id=NULL` 的 pending 记录。
|
||||
|
||||
### 5.2 第一次发送
|
||||
|
||||
1. mnote-web 创建 run,payload 携带 `sessionId`、`agentId=chat_only`、`profile=openclaw-doubao-chat`。
|
||||
2. mnote-web 查询绑定表;若无 `remote_conversation_id`,不传远端 ID。
|
||||
3. OpenClaw 发送给豆包,豆包创建新 conversation。
|
||||
4. OpenClaw 从 SSE 捕获 `conversation_id`。
|
||||
5. OpenClaw 需要把 `conversation_id` 作为结构化事件回传给 MNote。建议事件:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "provider.conversation.bound",
|
||||
"data": {
|
||||
"provider": "doubao-web",
|
||||
"remoteConversationId": "38428454119180290",
|
||||
"remoteUrl": "https://www.doubao.com/chat/38428454119180290"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. mnote-web 在 `persist_acp_runtime_event` 中识别该事件,upsert `ai_external_conversation_bindings`。
|
||||
|
||||
### 5.3 继续发送
|
||||
|
||||
1. mnote-web 根据 `user_id + mnote_session_id + provider=doubao-web` 查询绑定。
|
||||
2. 若找到 active `remote_conversation_id`,在传给 ACP/OpenClaw 的 payload 中加入:
|
||||
|
||||
```json
|
||||
{
|
||||
"providerConversation": {
|
||||
"provider": "doubao-web",
|
||||
"remoteConversationId": "38428454119180290"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. OpenClaw Doubao provider 使用该 ID 调 `/samantha/chat/completion`,设置 `need_create_conversation=false`。
|
||||
4. 若豆包返回会话不存在或被删除,OpenClaw 发 `provider.conversation.missing`;MNote 标记绑定异常,由用户决定是否新建远端会话。
|
||||
|
||||
### 5.4 删除 MNote session
|
||||
|
||||
1. 用户在 MNote 删除 ChatOnly 会话。
|
||||
2. mnote-web 先对 `ai_runtime_runs` 做本地软删除。
|
||||
3. mnote-web 将绑定表标记为 `local_deleted`。
|
||||
4. 若存在 `remote_conversation_id`,调用 OpenClaw / provider adapter 执行远端删除:
|
||||
|
||||
```text
|
||||
POST /im/conversation/batch_del_user_conv
|
||||
```
|
||||
|
||||
5. 成功后标记 `remote_deleted`。
|
||||
6. 失败时保持本地删除已完成,绑定标记 `remote_delete_failed`,`metadata_json` 记录错误码、响应摘要和时间。
|
||||
7. UI 提示:`MNote 会话已删除,豆包远端删除失败,可稍后重试`。
|
||||
|
||||
## 6. API 边界
|
||||
|
||||
### 6.1 mnote-web 内部 API
|
||||
|
||||
建议新增 provider conversation helper,不把豆包细节散落在 `hermes_client.rs`:
|
||||
|
||||
```text
|
||||
rust/crates/mnote-web/src/provider_conversations.rs
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 解析 run payload 中的 ChatOnly provider。
|
||||
- 读写 `ai_external_conversation_bindings`。
|
||||
- 将 binding 注入 ACP run payload。
|
||||
- 处理 `provider.conversation.bound/missing/deleted/delete_failed` 事件。
|
||||
|
||||
### 6.2 OpenClaw Doubao provider
|
||||
|
||||
需要在 OpenClaw `doubao-web` provider 增加三个能力:
|
||||
|
||||
- 从 ACP/context payload 读取 `providerConversation.remoteConversationId`。
|
||||
- 捕获新 `conversation_id` 后发结构化事件,不能只写 console log。
|
||||
- 暴露 `deleteConversation(remoteConversationId)`,内部走 `/im/conversation/batch_del_user_conv`。
|
||||
|
||||
第一阶段如果 ACP 不支持 provider 自定义 RPC,可先由 mnote-web 调一个 OpenClaw 本地 HTTP helper;但长期应收口到 provider adapter。
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 豆包创建成功但未捕获 `conversation_id` | run 仍完成;绑定缺失;下一轮可能新建远端会话;UI 标记未绑定 |
|
||||
| 绑定表有 ID,但豆包返回不存在 | 标记 `remote_missing`,提示用户重新绑定或新建 |
|
||||
| 删除 MNote 本地成功,豆包远端失败 | 不回滚本地删除;标记 `remote_delete_failed` |
|
||||
| 多用户尝试绑定同一远端 ID | 拒绝后写 audit,避免跨用户串会话 |
|
||||
| OpenClaw 重启 | SQLite 绑定仍在;下一轮从 MNote 注入远端 ID |
|
||||
| 豆包接口变更 | 本地会话不受影响;远端能力降级为不可用 |
|
||||
|
||||
## 8. 验收
|
||||
|
||||
### 8.1 单元 / 集成
|
||||
|
||||
- `control-plane`:binding upsert / lookup / local delete / remote delete status transition。
|
||||
- `mnote-web`:ChatOnly run payload 能注入已有 `remoteConversationId`。
|
||||
- `mnote-web`:`provider.conversation.bound` 事件能写入 SQLite。
|
||||
- `mnote-web`:删除 session 时先软删除本地,再 best-effort 调 provider delete。
|
||||
|
||||
### 8.2 真实浏览器 smoke
|
||||
|
||||
1. 使用测试账号登录 `http://localhost:3000`。
|
||||
2. 创建 ChatOnly / 豆包新会话,发送 marker A。
|
||||
3. 复查:
|
||||
- MNote SQLite 有一个 `mnote_session_id -> remote_conversation_id` 绑定。
|
||||
- 豆包日志 `Captured conversation_id` 一次。
|
||||
4. 在同一 MNote 会话发送 marker B。
|
||||
5. 复查:
|
||||
- 豆包日志第二次 `Conversation ID` 等于第一次捕获值。
|
||||
- 豆包网页同一 conversation 中出现 A 与 B。
|
||||
6. 删除 MNote 会话。
|
||||
7. 复查:
|
||||
- MNote 会话列表不再显示该 session。
|
||||
- SQLite binding status 为 `remote_deleted` 或 `remote_delete_failed`。
|
||||
- 若远端删除成功,豆包网页侧该 conversation 从列表移除或打开后显示已删除。
|
||||
|
||||
2026-06-01 验证记录:
|
||||
|
||||
- `node scripts/task512-chatonly-doubao-sync-smoke.js` 通过:豆包远端 conversation 绑定、同会话回复、MNote session 删除、provider delete 和 SQLite binding `remote_deleted` 均通过。
|
||||
- `node scripts/task513-chatonly-provider-sync-smoke.js deepseek` 通过:DeepSeek `remoteConversationId` 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
|
||||
- `node scripts/task513-chatonly-provider-sync-smoke.js gemini` 通过:Gemini conversation URL 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p control-plane external_conversation -- --test-threads=1` 通过:binding user scope 与状态迁移。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib provider_conversation -- --test-threads=1` 通过:provider conversation 注入与 bound event 持久化。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib chatonly_doubao_session_delete_calls_provider_and_marks_remote_deleted -- --test-threads=1` 通过:删除 session 后 provider delete 与 `remote_deleted` 状态。
|
||||
|
||||
## 9. 实施清单
|
||||
|
||||
- [x] 在 `control-plane` 增加 `ai_external_conversation_bindings` schema、store trait 和 SQLite 实现。
|
||||
- [x] 在 `mnote-web` 增加 provider conversation helper,避免继续扩大 `hermes_client.rs`。
|
||||
- [x] 在 ACP run 创建时为 ChatOnly / 豆包注入已有 `remoteConversationId`。
|
||||
- [x] 在 ACP event 持久化时处理 `provider.conversation.bound`。
|
||||
- [x] 在 session delete 路径中加入远端删除 best-effort 状态机。
|
||||
- [x] 在 OpenClaw Doubao provider 中加入结构化 conversation bound 事件。
|
||||
- [x] 在 OpenClaw Doubao provider 中加入 `deleteConversation`,走 `/im/conversation/batch_del_user_conv`。
|
||||
- [x] 补真实浏览器 smoke:`node scripts/task512-chatonly-doubao-sync-smoke.js` 覆盖远端 conversation id 绑定、豆包同会话 marker、session 删除、provider delete 和 SQLite binding `remote_deleted`。
|
||||
- [x] 补跨 provider 真实浏览器 smoke:`node scripts/task513-chatonly-provider-sync-smoke.js deepseek` 与 `node scripts/task513-chatonly-provider-sync-smoke.js gemini`。
|
||||
- [x] 补 MNote Rust 单测:control-plane binding、mnote-web provider conversation 注入、bound event 持久化和 session delete provider 状态。
|
||||
- [x] OpenClaw provider 源码不在本仓库,provider 单测缺口已归档到 `bugs/07-ai/done/7-49-chatonly-openclaw-provider-unit-test-gap-v1.md`;Doubao 有真实远端删除 helper,DeepSeek / Gemini 在缺少已验证远端删除 API 时明确返回 `remote_delete_failed`,不阻塞 MNote 侧设计归档。
|
||||
@@ -1,408 +0,0 @@
|
||||
# 7-45 ChatOnly API provider runtime v1
|
||||
|
||||
> 创建时间:2026-06-02
|
||||
>
|
||||
> 状态:`done`
|
||||
>
|
||||
> Owner:Page AI ChatOnly / mnote-web / SQLite control-plane / OmniRoute API provider
|
||||
>
|
||||
> 上位依据:
|
||||
> - `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
> - `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
> - `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
|
||||
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
|
||||
> - `design/07-ai/done/7-44-chatonly-doubao-session-binding-v1.md`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前 Page AI `Chat-only` 已接入豆包、DeepSeek、Gemini 等网页 provider。网页 provider 的价值是复用网页账号、网页历史和远端会话删除,但它也带来浏览器、登录态、验证码、前台窗口、OpenClaw provider prompt 和 provider-specific 会话绑定复杂度。
|
||||
|
||||
现在 OmniRoute 已能提供 OpenAI-compatible API endpoint:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:20128/v1/chat/completions
|
||||
```
|
||||
|
||||
并已验证以下模型可用:
|
||||
|
||||
- `aisz-chat/grok-4.3`
|
||||
- `aisz-chat/gemini-3.1-pro`
|
||||
- `aisz-chat/gpt-5.5-extra-high-fast`
|
||||
- `aisz-chat/kimi-k2.5`
|
||||
- DeepSeek pro / flash 组合模型
|
||||
|
||||
这些模型不需要浏览器,也不需要 OpenClaw。对于 MNote 当前需求,它们只是最简 ChatOnly provider:用户选一个 provider,发送消息,MNote 本地保存历史,删除 MNote 会话时删除本地历史。
|
||||
|
||||
## 2. 第一结论
|
||||
|
||||
API ChatOnly 不走 OpenClaw。
|
||||
|
||||
原因:
|
||||
|
||||
- OpenClaw 是 agent runtime,包含 agent prompt、skills、workspace、compaction、浏览器 profile 等能力;最简聊天窗口不需要这些能力。
|
||||
- API provider 没有网页远端历史对齐语义,不应复用 `CHATONLY_PROVIDER_CONFIGS` 的网页 conversation binding / remote delete 链路。
|
||||
- MNote 已有 ChatOnly UI、SSE 消费、SQLite runtime session、删除入口和 `reqwest` streaming 依赖;新增一个薄的 OpenAI-compatible adapter 比引入完整第三方 chat app 更可控。
|
||||
|
||||
最终分层:
|
||||
|
||||
| Provider 类别 | 默认状态 | 运行方式 | 历史删除语义 |
|
||||
|---|---|---|---|
|
||||
| 豆包网页 | 保留默认可用 | OpenClaw `doubao-web` | MNote 本地删除 + 豆包远端 best effort 删除 |
|
||||
| DeepSeek 网页 | fallback | OpenClaw `deepseek-web` | 网页远端 best effort 删除 |
|
||||
| Gemini 网页 | fallback | OpenClaw `gemini-web` | 网页远端 best effort 删除 |
|
||||
| Grok 网页 | fallback | OpenClaw `grok-web` | 暂不作为默认 |
|
||||
| OmniRoute API Chat | 新默认候选 | mnote-web 直连 `/v1/chat/completions` | 仅 MNote 本地删除 |
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 新增 `api-chat` provider kind,直接调用 OpenAI-compatible `/v1/chat/completions`。
|
||||
- 支持 DeepSeek、Gemini、Grok、GPT、Kimi API ChatOnly profiles。
|
||||
- 复用现有 Page AI ChatOnly UI、session list、session detail、rename、delete、search 和 SSE 消费模型。
|
||||
- API provider 不启动 ACP runtime,不启动 OpenClaw,不注入网页 provider metadata prompt。
|
||||
- API provider 的会话和消息以 SQLite/control-plane 为准,按用户隔离。
|
||||
- 网页 DeepSeek/Gemini/Grok 保留为 fallback,后续稳定后再决定是否从默认 UI 中隐藏。
|
||||
|
||||
非目标:
|
||||
|
||||
- 不实现完整 ChatGPT / Open WebUI / LibreChat 类产品。
|
||||
- 不导入第三方 chat app 的数据库、用户系统或前端壳。
|
||||
- 不同步 Gemini/Grok/DeepSeek 网页历史。
|
||||
- 不把 API ChatOnly 暴露为可写文件 agent。
|
||||
- 不支持 tool calling、function calling、vision、文件上传、多模态附件。
|
||||
|
||||
## 4. Provider 配置合同
|
||||
|
||||
新增一个小型静态 registry,建议先放在 mnote-web 后端,前端只消费 profile 列表:
|
||||
|
||||
```text
|
||||
api_chat_profiles
|
||||
- profile_id
|
||||
- label
|
||||
- provider_kind = api-chat
|
||||
- base_url
|
||||
- model
|
||||
- api_key_env
|
||||
- fallback_profile_id
|
||||
- status = active | fallback | hidden
|
||||
```
|
||||
|
||||
第一批 profiles:
|
||||
|
||||
| profile_id | label | model | 默认状态 |
|
||||
|---|---|---|---|
|
||||
| `shared_api_deepseek_flash_chat` | DeepSeek Flash | DeepSeek flash 组合模型 | active |
|
||||
| `shared_api_deepseek_pro_chat` | DeepSeek Pro | DeepSeek pro 组合模型 | active |
|
||||
| `shared_api_gpt_chat` | GPT | `aisz-chat/gpt-5.5-extra-high-fast` | active |
|
||||
| `shared_api_kimi_chat` | Kimi | `aisz-chat/kimi-k2.5` | active |
|
||||
| `shared_api_gemini_chat` | Gemini API | `aisz-chat/gemini-3.1-pro` | active |
|
||||
| `shared_api_grok_chat` | Grok API | `aisz-chat/grok-4.3` | active 或 hidden,按限额策略决定 |
|
||||
|
||||
默认 base URL:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:20128/v1
|
||||
```
|
||||
|
||||
API key 优先级:
|
||||
|
||||
1. `MNOTE_API_CHAT_<PROFILE>_API_KEY`
|
||||
2. `MNOTE_API_CHAT_API_KEY`
|
||||
3. `OPENAI_API_KEY`
|
||||
|
||||
不建议直接读取 `/home/lix/.codex/auth.json` 作为长期方案。开发期可以从环境注入,避免 MNote 代码绑定 Codex 配置文件。
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
继续复用现有 `ai_runtime_runs` / `ai_runtime_events` 作为会话索引和事件审计。API ChatOnly 的 run payload 增加:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentId": "chat_only",
|
||||
"providerKind": "api-chat",
|
||||
"profileId": "shared_api_gpt_chat",
|
||||
"model": "aisz-chat/gpt-5.5-extra-high-fast",
|
||||
"sessionId": "mnote_...",
|
||||
"message": "..."
|
||||
}
|
||||
```
|
||||
|
||||
消息恢复来源:
|
||||
|
||||
- 用户消息:从 run payload 的 `message` 恢复。
|
||||
- 助手消息:从 `ai_runtime_events` 中的 `message.delta` 聚合,或从 `run.completed` 的 final text 恢复。
|
||||
- 错误:写入 `run.failed`,用于 session detail 展示。
|
||||
|
||||
API ChatOnly 不写 `ai_external_conversation_bindings`。该表只属于网页 provider 远端会话绑定。
|
||||
|
||||
## 6. 后端数据流
|
||||
|
||||
### 6.1 创建会话
|
||||
|
||||
沿用现有:
|
||||
|
||||
```text
|
||||
POST /api/hermes/client/sessions
|
||||
```
|
||||
|
||||
当 `agentId=chat_only` 且 profile 是 `api-chat` 时:
|
||||
|
||||
1. 写入本地 session index。
|
||||
2. `persistence=local_ai_session_jsonl` 或当前 SQLite runtime persistence 口径。
|
||||
3. 不创建远端会话。
|
||||
4. 不创建 ACP session。
|
||||
|
||||
### 6.2 发送消息
|
||||
|
||||
沿用现有:
|
||||
|
||||
```text
|
||||
POST /api/hermes/client/runs
|
||||
```
|
||||
|
||||
分流规则:
|
||||
|
||||
```text
|
||||
agentId == chat_only
|
||||
且 profile/profileId 命中 api-chat registry
|
||||
-> api_chat_runtime
|
||||
否则
|
||||
-> 当前 ACP/OpenClaw/Web provider runtime
|
||||
```
|
||||
|
||||
`api_chat_runtime` 执行:
|
||||
|
||||
1. 按当前 `user_id + workspace_id + session_id + profile_id` 读取最近消息。
|
||||
2. 构造 OpenAI-compatible messages:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "role": "system", "content": "你是 MNote 的简洁聊天助手。不要声称能编辑文件。" },
|
||||
{ "role": "user", "content": "..." },
|
||||
{ "role": "assistant", "content": "..." },
|
||||
{ "role": "user", "content": "当前问题" }
|
||||
]
|
||||
```
|
||||
|
||||
3. 调 OmniRoute:
|
||||
|
||||
```text
|
||||
POST {base_url}/chat/completions
|
||||
Authorization: Bearer ...
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体最小字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "aisz-chat/kimi-k2.5",
|
||||
"messages": [],
|
||||
"stream": true
|
||||
}
|
||||
```
|
||||
|
||||
4. 解析 upstream SSE:
|
||||
|
||||
```text
|
||||
data: {"choices":[{"delta":{"content":"..."}}]}
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
5. 转成 MNote 现有 SSE event:
|
||||
|
||||
```text
|
||||
event: message.delta
|
||||
data: {"delta":"..."}
|
||||
|
||||
event: run.completed
|
||||
data: {"output":"...","model":"..."}
|
||||
```
|
||||
|
||||
6. 同步写入 `ai_runtime_events`,用于历史恢复和审计。
|
||||
|
||||
### 6.3 删除会话
|
||||
|
||||
沿用现有 session delete endpoint。
|
||||
|
||||
当 session 属于 `api-chat`:
|
||||
|
||||
1. 软删除本地 runs/events 或标记 session deleted。
|
||||
2. 不调用 provider conversation delete。
|
||||
3. 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"remoteDelete": {
|
||||
"attempted": false,
|
||||
"reason": "api_chat_has_no_remote_conversation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
UI 文案必须避免暗示已删除网页历史。
|
||||
|
||||
## 7. 代码边界
|
||||
|
||||
建议新增模块:
|
||||
|
||||
```text
|
||||
rust/crates/mnote-web/src/api_chat.rs
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- profile registry。
|
||||
- provider config resolution。
|
||||
- OpenAI-compatible request construction。
|
||||
- streaming parser。
|
||||
- upstream error normalization。
|
||||
- 将 upstream chunk 映射为 `message.delta` / `run.completed` / `run.failed`。
|
||||
|
||||
`hermes_client.rs` 只做分流和现有 session/run 持久化衔接,不继续塞大量 provider 细节。
|
||||
|
||||
前端改动控制在:
|
||||
|
||||
```text
|
||||
rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
|
||||
rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js
|
||||
```
|
||||
|
||||
前端只需要:
|
||||
|
||||
- 展示新增 profiles。
|
||||
- 对 API ChatOnly 不显示“网页问答”描述,改为“API 聊天”。
|
||||
- 删除成功提示区分本地删除和网页同步删除。
|
||||
|
||||
SQLite/control-plane 改动:
|
||||
|
||||
```text
|
||||
rust/crates/control-plane/src/sqlite.rs
|
||||
```
|
||||
|
||||
新增 shared profiles provisioning。第一版可以不新增表,只复用 profile metadata;如后续要 UI 动态配置模型,再引入 `ai_chat_provider_profiles` 表。
|
||||
|
||||
## 8. 第三方参考取舍
|
||||
|
||||
已考虑的参考:
|
||||
|
||||
- `async-openai`:适合参考 OpenAI-compatible 配置、SSE streaming、请求类型;如果后续字段变多,可引入依赖。
|
||||
- HuggingFace `chat-ui`、Open WebUI、LibreChat:功能完整,但带独立前端、用户、会话和数据库模型,不适合嵌入 MNote 当前 Page AI sidebar。
|
||||
- Vercel AI SDK:前端/Next 生态友好,但 MNote 当前主链是 Rust SSR + Rust Axum,不应为最小 adapter 引入 JS server 侧依赖。
|
||||
|
||||
第一版采用 `reqwest + serde_json` 的最小 adapter。理由:
|
||||
|
||||
- 当前仓库已有 `reqwest` streaming。
|
||||
- 需要支持的协议子集很小。
|
||||
- 不引入大型依赖和类型迁移成本。
|
||||
- bug 面集中在一个小模块,可用 mock upstream 和真实 OmniRoute smoke 覆盖。
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
必须明确区分:
|
||||
|
||||
- `api_chat_profile_unknown`:profile 未注册。
|
||||
- `api_chat_base_url_missing`:base URL 缺失。
|
||||
- `api_chat_api_key_missing`:API key 缺失。
|
||||
- `api_chat_upstream_unavailable`:OmniRoute 不可达。
|
||||
- `api_chat_upstream_error`:OmniRoute 返回非 2xx。
|
||||
- `api_chat_stream_parse_error`:SSE chunk 解析失败。
|
||||
- `api_chat_empty_response`:流结束但没有助手文本。
|
||||
|
||||
错误事件写入:
|
||||
|
||||
```text
|
||||
event: run.failed
|
||||
data: {"code":"api_chat_upstream_error","message":"..."}
|
||||
```
|
||||
|
||||
前端展示为普通 ChatOnly 失败消息,不进入文件权限或 tool call UI。
|
||||
|
||||
## 10. 验收
|
||||
|
||||
### 10.1 单元测试
|
||||
|
||||
- profile registry 能解析 DeepSeek/Gemini/Grok/GPT/Kimi。
|
||||
- API key 优先级正确。
|
||||
- OpenAI-compatible SSE chunk 能解析 `delta.content`。
|
||||
- `[DONE]` 正确结束。
|
||||
- 非 2xx upstream 映射为 `run.failed`。
|
||||
- API ChatOnly 不命中 `CHATONLY_PROVIDER_CONFIGS`。
|
||||
- API ChatOnly 删除不调用网页 provider delete。
|
||||
|
||||
### 10.2 集成测试
|
||||
|
||||
Mock upstream:
|
||||
|
||||
- `/v1/chat/completions` 返回 SSE delta。
|
||||
- MNote `/runs` 返回 runId。
|
||||
- MNote `/events/{runId}` 输出 `message.delta` 和 `run.completed`。
|
||||
- session detail 可恢复 user/assistant messages。
|
||||
- 删除 session 后列表不再出现。
|
||||
|
||||
真实 OmniRoute smoke:
|
||||
|
||||
- `shared_api_deepseek_flash_chat` 发一条短消息成功。
|
||||
- `shared_api_gpt_chat` 发一条短消息成功。
|
||||
- 确认没有启动或调用 OpenClaw gateway/proxy。
|
||||
|
||||
浏览器 smoke:
|
||||
|
||||
- 打开 Page AI ChatOnly。
|
||||
- 选择 GPT API,发消息,只出现一条助手回复。
|
||||
- 新建会话、切换会话、刷新页面后历史仍存在。
|
||||
- 删除会话后本地列表消失。
|
||||
- 豆包网页会话链路不回归,仍能远端删除。
|
||||
|
||||
## 11. 分阶段实施
|
||||
|
||||
### Phase 1:最小 API runtime
|
||||
|
||||
- 新增 `api_chat.rs`。
|
||||
- 接入 `shared_api_deepseek_flash_chat`、`shared_api_gpt_chat`。
|
||||
- 复用现有 `/runs` 和 `/events`。
|
||||
- 跑 mock upstream 和真实 OmniRoute smoke。
|
||||
|
||||
### Phase 2:扩展 provider 列表
|
||||
|
||||
- 增加 `deepseek_pro`、`gemini_api`、`kimi`、`grok_api`。
|
||||
- Grok 默认可设为 hidden 或 fallback,避免限额被普通默认选择消耗。
|
||||
- 前端区分 `API` 与 `网页 fallback` 标签。
|
||||
|
||||
### Phase 3:收口网页 fallback
|
||||
|
||||
- 观察 API provider 稳定性。
|
||||
- DeepSeek/Gemini/Grok 网页降级为 fallback 或隐藏。
|
||||
- 保留豆包网页主链,直到豆包也有可替代的稳定 API provider 且不再要求网页历史对齐。
|
||||
|
||||
## 12. 风险与约束
|
||||
|
||||
- OmniRoute 模型名可能变动,profile registry 要允许环境覆盖 model。
|
||||
- 某些 upstream 可能不完全遵守 OpenAI streaming chunk 格式,parser 要容忍 `content` / `text` / `delta` 变体,但不要吞掉错误。
|
||||
- API ChatOnly 没有网页远端删除,UI 和返回值必须说清楚。
|
||||
- 不要让 API ChatOnly 继承 Page AI 文件写权限;它只能读已显式加入 prompt 的上下文。
|
||||
- 不要为第一版引入 tool calling,否则会重新变成 agent runtime。
|
||||
|
||||
## 13. 退出条件
|
||||
|
||||
本设计可归档到 `done/` 的条件:
|
||||
|
||||
- API ChatOnly 至少两个 provider 完成真实浏览器 smoke。
|
||||
- 本地历史创建、恢复、删除通过。
|
||||
- API ChatOnly 确认不启动 OpenClaw。
|
||||
- 豆包网页删除同步回归通过。
|
||||
- DeepSeek/Gemini/Grok 网页 fallback 没有被误删或误改。
|
||||
|
||||
## 14. 完成验证
|
||||
|
||||
完成时间:2026-06-02
|
||||
|
||||
已完成:
|
||||
|
||||
- `api-chat` runtime 已接入 MNote ChatOnly,直接调用 OmniRoute OpenAI-compatible `/v1/chat/completions`,不启动 ACP/OpenClaw。
|
||||
- SQLite profile provisioning 已包含 DeepSeek Flash / DeepSeek Pro / GPT / Kimi / Gemini API / Grok API 六个 shared API ChatOnly profiles。
|
||||
- 后端 SSE 已将 OpenAI-compatible streaming 映射为 `message.delta` / `run.completed` / `run.failed`,并写入 `ai_runtime_events` 用于历史恢复。
|
||||
- API ChatOnly 删除只删除 MNote 本地会话,返回 `api_chat_has_no_remote_conversation`,不进入网页 provider 远端删除链路。
|
||||
- 真实 OmniRoute smoke 已验证六个模型均返回 200。
|
||||
- 真实浏览器 smoke 已验证 GPT API 与 DeepSeek Flash API:发送后只有一条助手回复,刷新后历史恢复,删除后本地会话消失,且没有写入 `ai_external_conversation_bindings`。
|
||||
- 豆包网页主链回归已验证:豆包端 user/assistant marker 各一条,未重复发送助手回复,删除走侧栏三点菜单并返回 `remote_deleted`。
|
||||
@@ -4,14 +4,14 @@
|
||||
>
|
||||
> 当前状态:`done`
|
||||
>
|
||||
> 2026-07-03 口径回正:当前 runtime 已回到 OpenHub / native agent + LightRAG + Turso/libSQL。本文重新作为当前默认 LightRAG provider 的 DOCX 引用与定位合同基线;此前 `7-68 OpenHub + WeKnora + MNote Page AI 深度融合` 中将 WeKnora 设为默认 provider 的口径已标记 stale。
|
||||
> 2026-07-19 口径回正:当前 runtime 是 Pi Rust Page AI + LightRAG + Turso/libSQL。本文保留为当前默认 LightRAG provider 的 DOCX 引用与定位合同基线;WeKnora 仅为历史/备用边界。
|
||||
>
|
||||
> Owner:07-ai / knowledge-rag / 03-rust-web / office-preview
|
||||
>
|
||||
> 关联:
|
||||
> - `design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md`
|
||||
> - `design/07-ai/done/7-51-lightrag-post-commit-hardening-v1.md`
|
||||
> - `design/07-ai/process/7-54-raganything-multimodal-retrieval-alignment-v1.md`
|
||||
> - `design/07-ai/reference/7-54-raganything-multimodal-retrieval-alignment-v1.md`
|
||||
>
|
||||
> 参考代码:
|
||||
> - LightRAG:`/mnt/Data1T/Mnote_data/lightrag/LightRAG`
|
||||
|
||||
@@ -1,736 +0,0 @@
|
||||
# 7-58 Page AI Reasonix / Hermes 官方会话与响应对齐 v1
|
||||
|
||||
> 创建时间:2026-06-09
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
>
|
||||
> Owner:07-ai / Page AI / Reasonix ACP / Hermes ACP / mnote-web runtime
|
||||
>
|
||||
> 参考代码:
|
||||
> - Reasonix 官方 `main-v2/desktop`:https://github.com/esengine/DeepSeek-Reasonix/tree/main-v2/desktop
|
||||
> - Hermes 官方仓库:`NousResearch/hermes-agent`:https://github.com/NousResearch/hermes-agent
|
||||
> - 本地参考副本:`reference-code/DeepSeek-Reasonix-main`
|
||||
> - MNote 当前链路:`rust/crates/mnote-web/src/routes/hermes_client.rs`、`rust/crates/mnote-web/src/acp_session_manager.rs`、`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`、`rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
>
|
||||
> 上位依据:
|
||||
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
|
||||
> - `design/07-ai/done/7-40-page-ai-context-envelope-and-run-receipt-v1.md`
|
||||
> - `design/10-review/reference/13-hermes-vscode-plugin-gap-review-v1.md`
|
||||
> - `design/10-review/done/10-current-mnote-ai-runtime-review-v1.md`
|
||||
|
||||
## 1. 第一结论
|
||||
|
||||
Page AI 里 Reasonix 不能继续按“每个 run 都可 `session/load` 恢复旧 ACP session”的模式设计。
|
||||
|
||||
Reasonix 官方 `main-v2/desktop` 的 ACP 模型是:
|
||||
|
||||
- `initialize.agentCapabilities.loadSession=false`。
|
||||
- `session/new` 创建一个内存 `Session`,其中持有 `CacheFirstLoop`、toolset、MCP clients、Eventizer 和 aborter。
|
||||
- `session/prompt` 必须命中同一个内存 `Session`,然后调用 `session.loop.step(text)` 继续对话。
|
||||
- 上下文主要在 Reasonix 本地 loop / session 对象里延续,而不是靠 MNote 每轮重发完整聊天历史。
|
||||
|
||||
因此 MNote 对 Reasonix 的正确集成不是“保存 acpSessionId,下轮再 load”,而是:**同一个 MNote Page AI session 在同一个 Rust Web 进程生命周期内必须复用同一个活的 Reasonix ACP session manager;如果底层 Reasonix session 已丢失,必须显式进入 cold resume/fallback,不得假装仍是原生 Reasonix 上下文。**
|
||||
|
||||
这也解释当前现象:用户第二轮只回“可以”时,MNote payload 没有带历史 messages;它本来就依赖 Reasonix 本地 session 记住上一轮。但实际后端每轮都 fallback 出新的 Reasonix ACP session,Reasonix 当然只能把“可以”当新会话开头。
|
||||
|
||||
## 2. 当前实测问题
|
||||
|
||||
2026-06-09 现场证据:同一个 MNote session `mnote_current_page-ai-mq6q24d5` 连续三轮 run,实际 Reasonix ACP session 发生了三次变化:
|
||||
|
||||
| 轮次 | 用户输入 | 前端发送的 acpSessionId | 后端实际 session.info.updated acpSessionId |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 看看你现在能不能看到有3个mcp | 空 | `19dbc6c6-b61e-45d1-8a28-52d0e79a02e3` |
|
||||
| 2 | 可以 | `19dbc6c6-b61e-45d1-8a28-52d0e79a02e3` | `daa898ff-e985-49da-bbb9-ba3072f130c7` |
|
||||
| 3 | 帮我检查一下 LightRAG 服务的启用状态或者帮它启动起来 | `daa898ff-e985-49da-bbb9-ba3072f130c7` | `ed358871-5176-4f3c-869c-91578cefa2b3` |
|
||||
|
||||
并且第二轮 run payload 中没有 `messages` 字段,只有当前 `message="可以"`、`pageContext`、`contextRefs`、`acpSessionId` 等。这证明当前 Page AI 并没有 MNote 侧 transcript 注入兜底。
|
||||
|
||||
同一轮后端还出现大量:
|
||||
|
||||
```text
|
||||
WARN ACP SSE lagged: N events dropped
|
||||
```
|
||||
|
||||
这说明 ACP event 转发路径有背压丢事件。当前事件路径会在 `broadcast` 接收后先持久化 SQLite / local journal,再写入浏览器 SSE;当 Reasonix 高频输出 `thought.delta` / `message.delta` 时,256 buffer 很容易落后。终止事件或状态同步被拖慢/丢失后,前端 assistant bubble 的 `streaming=true` 不能及时清掉,CSS 就继续显示 `AI · 输出中`。
|
||||
|
||||
当前 SQLite `ai_runtime_runs.status` 仍停在 `acp_pending`,但同 run 的 `ai_runtime_events` 已有 `run.completed`。这会继续污染 active-run 判断、恢复逻辑和前端状态。
|
||||
|
||||
## 3. Reasonix 官方模式摘录
|
||||
|
||||
### 3.1 ACP 能力
|
||||
|
||||
Reasonix 官方 `src/cli/commands/acp.ts` 在 `initialize` 中返回:
|
||||
|
||||
```ts
|
||||
agentCapabilities: {
|
||||
loadSession: false,
|
||||
promptCapabilities: { image: false, audio: false, embeddedContext: true },
|
||||
mcpCapabilities: { http: false, sse: false },
|
||||
}
|
||||
```
|
||||
|
||||
含义:MNote 不能把 `session/load` 当成 Reasonix 的正常恢复入口。对于 Reasonix,`acpSessionId` 只在当前 ACP 进程内有效。
|
||||
|
||||
### 3.2 session/new
|
||||
|
||||
Reasonix `session/new` 做的事不是只分配一个 id,而是完整构造一个 session:
|
||||
|
||||
- resolve workspace root。
|
||||
- build code toolset。
|
||||
- load MCP servers,并把 MCP tools bridge 进 tool registry。
|
||||
- 构造 `ImmutablePrefix`。
|
||||
- 创建 `CacheFirstLoop`。
|
||||
- 创建 `Eventizer`。
|
||||
- 保存到内存 `sessions` map。
|
||||
|
||||
这就是 Reasonix 的上下文承载点。
|
||||
|
||||
### 3.3 session/prompt
|
||||
|
||||
Reasonix `session/prompt` 通过 `params.sessionId` 找内存 session,然后:
|
||||
|
||||
```ts
|
||||
for await (const ev of session.loop.step(text)) {
|
||||
for (const kev of session.eventizer.consume(ev, session.ctx)) {
|
||||
dispatchKernelEvent(server, session.id, kev);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
因此多轮对话是否连续,关键是 MNote 是否一直把后续 prompt 发给同一个 live Reasonix session。
|
||||
|
||||
### 3.4 Desktop UI 终止语义
|
||||
|
||||
Reasonix 官方 desktop controller 在 turn 结束时发送类似 `$turn_complete` 的终止事件,并在 finally 中清理 running 状态、刷新 sessions/balance。MNote Page AI 当前只靠 `run.completed` SSE 和前端 `streaming=false` 清理 UI,这条链路在 SSE lag/drop 下不够稳。
|
||||
|
||||
## 4. 目标架构
|
||||
|
||||
### 4.1 双层 session 模型
|
||||
|
||||
MNote 必须区分两层 session:
|
||||
|
||||
| 层 | 作用 | 是否持久 | 对 Reasonix 的含义 |
|
||||
| --- | --- | --- | --- |
|
||||
| MNote Page AI session | UI 历史、SQLite/local JSONL、用户可见会话、run audit | 持久 | 可用于恢复 UI 和构造 fallback context |
|
||||
| Reasonix ACP session | Reasonix 内存 `CacheFirstLoop`、MCP toolset、模型上下文 | 进程内 live | 不能假设可 load,丢失后就是 cold |
|
||||
|
||||
新增/收口一个运行时绑定概念:
|
||||
|
||||
```text
|
||||
PageAiAcpLiveBinding
|
||||
mnoteSessionId
|
||||
acpRuntime = reasonix
|
||||
acpSessionId
|
||||
workspaceRoot / cwd
|
||||
rootUri / allowedRootsHash
|
||||
profile / model
|
||||
managerHandle
|
||||
status: idle | running | queued | cold | closed
|
||||
lastRunId
|
||||
lastEventSeq
|
||||
createdAt / lastPromptAt
|
||||
```
|
||||
|
||||
`managerHandle` 只在 Rust Web 进程内存中存在;SQLite 只保存 `acpSessionId` 和 metadata,用于 UI 展示、审计和检测 stale,而不是承诺可恢复 Reasonix 原生上下文。
|
||||
|
||||
### 4.2 Reasonix run 启动规则
|
||||
|
||||
对于 `acpRuntime=reasonix`:
|
||||
|
||||
1. 先按 `mnoteSessionId + acpRuntime` 查 live binding。
|
||||
2. 若 binding 存在且 status 非 closed,直接复用同一个 `AcpSessionManager` 和 `acpSessionId` 调 `session/prompt`。
|
||||
3. 若 binding 不存在:
|
||||
- 不调用或不依赖 `session/load`。
|
||||
- 调 `session/new` 创建新 Reasonix session。
|
||||
- 将新 `acpSessionId` 写回 MNote session metadata。
|
||||
- 若该 MNote session 已有历史 transcript,标记为 `cold_resume`,走 fallback context 注入。
|
||||
4. 当前 run 完成后,不关闭 manager,不从 live binding 中移除;只把 binding 状态改回 idle。
|
||||
5. 用户显式关闭会话、切换 workspace/root/model/profile、登出、进程退出或资源压力回收时,才关闭 binding。
|
||||
|
||||
这和当前 `run_prompt_with_mnote_context(...).await` 后直接 `mgr_clone.close().await`、`ACP_ACTIVE_RUNS.remove(...)` 的模式相反。对于 Reasonix,turn 完成不等于 Reasonix session 生命周期结束。
|
||||
|
||||
### 4.3 Hermes 与 Reasonix 分歧
|
||||
|
||||
不要把 Hermes VSCode 的 `session/load` 结论机械套到 Reasonix:
|
||||
|
||||
- Hermes adapter 如果声明支持 `loadSession`,MNote 应继续优先 `session/load`。
|
||||
- Reasonix 官方明确 `loadSession=false`,MNote 应使用 live binding + cold resume fallback。
|
||||
- UI 上要能显示当前会话是否是 `native-live`、`cold-resumed`、`new-session`,避免用户误以为 Reasonix 原生上下文仍在。
|
||||
|
||||
### 4.4 Hermes Desktop / Web UI 对照
|
||||
|
||||
Hermes 参考实现反而证明:Hermes 与 Reasonix 的上下文工程必须分开设计,只能共享外层 MNote envelope / audit / UI projection。
|
||||
|
||||
Hermes 官方公开文档与本地参考包需要分层理解:
|
||||
|
||||
- ACP adapter 的 `session/load` / `session/new` / `session/prompt` 属于 adapter 会话合同;host 不能只把旧 `sessionId` 写回内存后直接 prompt。
|
||||
- 官方 user guide 明确提醒:ACP session 列表、load、resume、fork 的范围以当前运行的 ACP server process 为准。也就是说,MNote 不能把 Hermes `session/load` 永久等同为跨进程必然成功。
|
||||
- Hermes VSCode 插件的 `SessionManager.ensureSession()` 说明了 host 侧正确动作:已有 `storedSessionId` 时必须先调用 ACP `session/load`,让 adapter 注册会话;`session/load` 返回 `null` 或失败后才能 `session/new`。插件注释还指出,跳过 `session/load` 会形成 phantom session。
|
||||
- Hermes Web / dashboard 路径体现的是 server / DB 级会话资产:`state.db` 中有 sessions、messages、conversation chain、search/detail/export 这类持久查询能力。这是 MNote 做会话管理和 history dashboard 的参考,但不能直接推导为 ACP adapter 任意时刻都能无损 replay。
|
||||
- 本地历史包 `hermes-web-ui-0.5.18` 的 Socket.IO server 体现了 Web/API chat 路径:`resumeSession()` 先找 live `sessionMap`,找不到再从 DB `loadSessionStateFromDb()` 读取 messages、compression snapshot 和 token 统计。这条路径不是 ACP `session/load`,应放在 Track W。
|
||||
|
||||
因此 Hermes 的正确集成不是“无条件相信 session id 可恢复”,而是:**按 runtime capability 探测 -> 有 stored `acpSessionId` 就先 `session/load` -> load 成功才认为底层 adapter 连续 -> load 失败进入 Hermes cold/new;若 adapter replay 了 history,MNote 必须消费、去重、排序。**
|
||||
|
||||
Reasonix 官方 desktop/ACP 与这两者都不同:Reasonix ACP 声明 `loadSession=false`,上下文在当前进程内的 `CacheFirstLoop` 上。MNote 如果每轮关闭 `AcpSessionManager`,就等于每轮丢掉 Reasonix 原生上下文。
|
||||
|
||||
因此 MNote 后续应冻结三套 runtime policy:
|
||||
|
||||
| Runtime | 上下文主承载 | 恢复策略 | MNote 责任 |
|
||||
| --- | --- | --- | --- |
|
||||
| Hermes ACP | adapter session;若 adapter 支持并成功 `session/load`,才视为底层连续 | stored ACP session id -> `session/load` -> 按 load 结果和 replay evidence 决定连续性 -> `session/prompt` | 保存/加载 acpSessionId;消费可能出现的 replay;处理 load 失败 fallback |
|
||||
| Hermes Web/API Chat | Hermes server transcript + DB state | DB messages / compression snapshot -> resume socket/run | 管理 messages、summary、run status、queue |
|
||||
| Reasonix ACP | live `CacheFirstLoop` in ACP process | live binding 复用;丢失后 cold resume packet | 维持 live binding,不假装 session/load 可用 |
|
||||
|
||||
共享部分只包括:
|
||||
|
||||
- MNote Page AI session / run id / trace id。
|
||||
- `agentRunEnvelope`、target package、allowed roots、capability policy。
|
||||
- SQLite/local JSONL audit。
|
||||
- 前端 projection:assistant final、thought、tool、plan、permission、citation、run status。
|
||||
|
||||
不共享的部分是:底层 agent session 生命周期、上下文恢复机制、是否重发 transcript、是否允许 `session/load`。
|
||||
|
||||
## 5. Context 工程设计
|
||||
|
||||
### 5.1 native-live 路径
|
||||
|
||||
当 Reasonix live binding 存在时:
|
||||
|
||||
- MNote 每轮 prompt 只发送当前用户输入、当前页面/selection/target package、allowed roots、MNote capability envelope。
|
||||
- 不重发完整 transcript,避免破坏 Reasonix 自己的 prefix/cache/memory 设计。
|
||||
- MNote 仍持久化 user/assistant/tool events,用于 UI 历史、审计、fallback 和 reload。
|
||||
|
||||
### 5.2 cold resume fallback
|
||||
|
||||
当 MNote Page AI session 有历史,但 Reasonix live binding 丢失时:
|
||||
|
||||
- 创建新的 Reasonix ACP session。
|
||||
- 将 MNote 最近 N 轮 transcript、上一次 summary、当前 page context、allowed roots 组成一个明确的 `MNote cold resume context packet`。
|
||||
- 作为 embedded/context prompt 的前置内容发给 Reasonix。
|
||||
- 前端显示轻量状态:`Reasonix 原生会话已重建,已用 MNote 历史摘要恢复上下文`。
|
||||
- 该状态进入 run audit,不能静默伪装成原生连续会话。
|
||||
|
||||
fallback packet 必须控制大小:
|
||||
|
||||
- 优先 summary。
|
||||
- 再补最近 3-5 轮 user/assistant final。
|
||||
- tool raw output 只放 tool name、result summary、changed files、citations,不放大段 stdout。
|
||||
- thought/reasoning 默认不进入 fallback。
|
||||
|
||||
### 5.3 用户短回复消歧
|
||||
|
||||
对“可以”“继续”“按刚才说的”“同意”这类短回复:
|
||||
|
||||
- native-live:完全交给 Reasonix 原生上下文。
|
||||
- cold-resumed:fallback packet 必须包含上一轮 user intent 和 assistant proposal。
|
||||
- new-session:前端或后端应提示“当前 Reasonix 原生上下文不可用,短回复缺少指代对象”,不要让模型编造。
|
||||
|
||||
## 6. Response / UI 状态设计
|
||||
|
||||
### 6.1 事件分类
|
||||
|
||||
Reasonix event 到 MNote UI 的分类应对齐官方 desktop 的 projection 思路:
|
||||
|
||||
| Reasonix / ACP 事件 | MNote 展示 |
|
||||
| --- | --- |
|
||||
| message/model delta | 当前 assistant streaming bubble |
|
||||
| model final / run completed | 关闭 streaming bubble,写入 final |
|
||||
| thought delta | 折叠的“思考过程”,不混入 final answer |
|
||||
| tool intent/result | tool card,带 status、locations、citations、audit id |
|
||||
| plan/todo | plan/status panel,不当作 assistant 正文 |
|
||||
| turn complete | 解除 busy / 输出中 / active run snapshot |
|
||||
|
||||
### 6.2 终止事件不可丢
|
||||
|
||||
`run.completed` / `run.failed` / `run.aborted` / synthetic `turn.complete` 属于不可丢事件。
|
||||
|
||||
即使中间 `thought.delta` 或 `message.delta` 被限流/合并,终止事件也必须送达浏览器并持久化。前端 `输出中` 只能由 active run status 和 streaming flag 共同决定:
|
||||
|
||||
- 收到 terminal event:强制 `streaming=false`。
|
||||
- SSE 断开但后端 run 已 terminal:`/active-run` 或 `/runs/{id}/events` reconciliation 必须合成 terminal event。
|
||||
- SQLite run status terminal 时,active-run 不得再返回该 run。
|
||||
|
||||
### 6.3 状态持久化
|
||||
|
||||
当前 `ai_runtime_events` 已记录 `run.completed`,但 `ai_runtime_runs.status` 仍停在 `acp_pending`,这是 UI 卡 “输出中” 和 active-run 误判的根因之一。
|
||||
|
||||
后续必须保证:
|
||||
|
||||
- `register_runtime_from_create_run_response` 初始化 run 为 `running` 或 `queued`,不要长期停在 `acp_pending`。
|
||||
- `persist_acp_runtime_event(run.completed)` 同步更新 SQLite `ai_runtime_runs.status=completed`。
|
||||
- `persist_acp_runtime_event(run.failed)` 同步更新 `failed`。
|
||||
- `persist_acp_runtime_event(run.aborted)` 同步更新 `aborted`。
|
||||
- 内存 registry 与 SQLite status 不一致时,以 terminal event journal 为 reconciliation truth,补写 run status。
|
||||
|
||||
## 7. SSE / 背压设计
|
||||
|
||||
当前 `broadcast` consumer 同时承担“持久化事件”和“转发浏览器 SSE”,导致高频 Reasonix 输出时 `ACP SSE lagged`。
|
||||
|
||||
目标拆分:
|
||||
|
||||
```text
|
||||
Reasonix ACP event
|
||||
-> in-memory run event bus
|
||||
-> browser SSE fanout, low latency, terminal priority
|
||||
-> persistence worker, async/batched, can lag but不能阻塞 SSE
|
||||
-> run status reconciler, terminal immediate
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `thought.delta` 可合并、降采样或只持久化部分摘要。
|
||||
- `message.delta` 可按时间片或 token chunk 合并,保证顺序。
|
||||
- tool events、permission events、session.info、plan、terminal events 不降采样。
|
||||
- terminal event 使用独立 high-priority path;即使普通 delta buffer 满,也不能 drop。
|
||||
- persistence worker 失败只产生 audit warning,不应阻塞浏览器终止状态。
|
||||
|
||||
## 8. 后续执行总图
|
||||
|
||||
本设计后续不应拆成“先随手修 Reasonix,再顺手修 Hermes”。正确顺序是先冻结共享合同,再分别推进 Reasonix 和 Hermes 的 runtime policy,最后统一 UI projection 与 smoke。
|
||||
|
||||
```text
|
||||
Phase 0: Runtime policy / event store / status reconciliation 共享底座
|
||||
-> Track R: Reasonix ACP live binding + cold resume
|
||||
-> Track H: Hermes ACP session/load + replay 消费
|
||||
-> Track W: Hermes Web/API chat transcript resume 兼容
|
||||
-> Track U: Page AI UI / history / debug / smoke 矩阵
|
||||
```
|
||||
|
||||
### 8.1 共享底座:Runtime policy
|
||||
|
||||
先在 MNote 侧抽象出明确的 runtime policy,而不是在 `hermes_client.rs` 里靠 `if acpRuntime == "reasonix"` 散落判断。
|
||||
|
||||
建议合同:
|
||||
|
||||
```text
|
||||
AgentRuntimePolicy
|
||||
runtime: hermes | reasonix | chat_only | api_chat
|
||||
contextCarrier:
|
||||
- adapter_loadable_session
|
||||
- adapter_live_loop
|
||||
- server_transcript
|
||||
supportsSessionLoad: bool
|
||||
supportsHistoryReplay: bool
|
||||
supportsForkListResume: bool
|
||||
promptContinuity:
|
||||
- native_live_required
|
||||
- load_then_prompt
|
||||
- transcript_rebuild
|
||||
terminalSemantics:
|
||||
- run.completed
|
||||
- run.failed
|
||||
- run.aborted
|
||||
- synthetic.turn_complete
|
||||
```
|
||||
|
||||
MNote Page AI 每次 run 必须先解析 policy,再决定:
|
||||
|
||||
- 是否允许 `session/load`。
|
||||
- 是否应复用 live manager。
|
||||
- 是否需要从 SQLite/local JSONL 构造 cold resume packet。
|
||||
- 是否要接收 adapter replay history。
|
||||
- terminal event 如何更新 SQLite run status 和前端 active-run snapshot。
|
||||
|
||||
### 8.2 共享底座:Session 身份
|
||||
|
||||
统一命名并避免当前概念混用:
|
||||
|
||||
| 字段 | 含义 | 生命周期 |
|
||||
| --- | --- | --- |
|
||||
| `mnoteSessionId` | MNote Page AI 会话 id,UI/history/audit 主键 | 持久 |
|
||||
| `mnoteRunId` | MNote 一次 run id,events/status/audit 主键 | 持久 |
|
||||
| `acpSessionId` | adapter 返回的底层 session id | runtime 相关 |
|
||||
| `runtimeBindingId` | MNote 内存 runtime binding id | 进程内 |
|
||||
| `providerConversationId` | ChatOnly 网页 provider 远端会话 id | provider 相关 |
|
||||
|
||||
SQLite `ai_runtime_runs` 必须保存这些字段的快照,但只有 `mnoteSessionId` / `mnoteRunId` 是 MNote 自己的长期真相。`acpSessionId` 对 Hermes 是可 load 引用,对 Reasonix 只是 live binding 标识,不代表可恢复。
|
||||
|
||||
### 8.3 共享底座:Event store / terminal reconciliation
|
||||
|
||||
先修这个底座,否则 Reasonix 和 Hermes 都会继续被 `输出中`、active-run stale、历史恢复误判影响。
|
||||
|
||||
- [ ] browser SSE 转发与 persistence 解耦:浏览器低延迟 fanout,不在同一 consumer 里同步写 SQLite。
|
||||
- [ ] terminal event 高优先级:`run.completed` / `run.failed` / `run.aborted` / synthetic `turn.complete` 不可被 delta 洪峰挤掉。
|
||||
- [ ] `persist_acp_runtime_event` 或独立 reconciler 在 terminal event 到达时同步更新 `ai_runtime_runs.status`。
|
||||
- [ ] `/api/page-ai/sessions/{id}/active-run` 不得返回已有 terminal event 的 run。
|
||||
- [ ] `/api/page-ai/runs/{id}/events` 能在 run status terminal 但缺 terminal event 时合成 reconciliation event。
|
||||
- [ ] 前端收到 terminal / reconciliation terminal 后强制清理 `streaming=true`、`pageAiBusy`、active run snapshot。
|
||||
|
||||
### 8.4 共享底座:Response projection
|
||||
|
||||
所有 runtime 的可见输出投影统一,但事件来源不统一:
|
||||
|
||||
- assistant final:只来自 message/model delta + final output。
|
||||
- thought:只进折叠思考过程,默认不展开,不混入 final。
|
||||
- tool:独立 tool card,保留 status、toolCallId、locations、citations、auditId。
|
||||
- plan/todo:独立 plan/status panel,不当作 assistant 正文。
|
||||
- permission:独立权限卡,用户决策必须回写 adapter。
|
||||
- citation:优先 tool 返回的 `citationMarkdown` / `citationUrl`,不让模型手拼。
|
||||
- busy/输出中:只由 active run + streaming bubble 共同决定,terminal 后立即清除。
|
||||
|
||||
## 9. Track R:Reasonix ACP
|
||||
|
||||
### R0:官方模式冻结
|
||||
|
||||
- [ ] fake Reasonix ACP fixture:`initialize.loadSession=false`、`session/new` 创建内存 loop、`session/prompt` 必须命中同一 session。
|
||||
- [ ] 在测试中证明:如果 MNote 每轮新建 session,短回复“可以”无法关联上一轮;如果复用 live binding,可以关联。
|
||||
- [ ] 在 runtime payload / SQLite detail 中暴露 `reasonixSessionMode: native_live | cold_resumed | new_session`。
|
||||
|
||||
### R1:Live binding pool
|
||||
|
||||
- [ ] `AcpRuntimeManager` / `hermes_client` 增加按 `mnoteSessionId + acpRuntime` 复用的 Reasonix live binding pool。
|
||||
- [ ] Reasonix run 完成后不关闭 manager,不移除 binding,只从 running 改 idle。
|
||||
- [ ] 同一 binding 内 run 串行执行;第二条输入进入 binding queue,不另开 session。
|
||||
- [ ] Stop/abort 只取消当前 prompt,不默认销毁 binding。
|
||||
- [ ] 切 workspace/root/model/profile/allowedRootsHash 时关闭旧 binding,并把下一轮标记为 cold/new。
|
||||
|
||||
### R2:Cold resume fallback
|
||||
|
||||
- [ ] 从 SQLite/local JSONL 构造 bounded transcript summary。
|
||||
- [ ] cold resume 首轮 prompt 注入 `MNote cold resume context packet`。
|
||||
- [ ] packet 只包含 summary、最近 3-5 轮 user/assistant final、tool summary、changedFiles、citations,不包含 thought 大段文本。
|
||||
- [ ] UI 显示 cold resume 状态,不误导为 Reasonix 原生会话。
|
||||
- [ ] `new_session` 且用户只发短回复时提示缺上下文,不让模型编造。
|
||||
|
||||
### R3:Reasonix 工具与 MCP 能力
|
||||
|
||||
- [ ] 确认 `mnoteCapabilities` / runtime manifest 进入 Reasonix 模型可见 prompt,而不只是 Rust payload。
|
||||
- [ ] 继续保持 Reasonix 原生文件读写为 local-first 普通 Markdown 主路径;MNote 不扩写一套本地文件编辑工具。
|
||||
- [ ] MCP 状态作为 Reasonix 工具可见事实进入 tool/status card,而不是让模型只凭自然语言猜测。
|
||||
- [ ] LightRAG / MNote bridge MCP 失败时返回结构化 tool failed,不污染 assistant final。
|
||||
|
||||
## 10. Track H:Hermes ACP
|
||||
|
||||
### H0:官方 load 合同冻结
|
||||
|
||||
- [ ] fake Hermes ACP fixture:`initialize.loadSession=true`,支持 `session/load`;另准备一个会 replay history `session/update` 的变体。
|
||||
- [ ] MNote 启动 Hermes run 时读取 stored `acpSessionId`,先 `session/load`,再 `session/prompt`。
|
||||
- [ ] `session/load` 成功时,MNote 认为 adapter 当前已注册该 session;如果 adapter replay 出 user/assistant/thought/tool/plan,前端/后端必须消费并去重,不能覆盖乱序。
|
||||
- [ ] `session/load` 返回 `null` 或失败时,标记 `hermesSessionMode=cold_resumed | new_session`,不假装原生连续。
|
||||
|
||||
### H1:Hermes session store 对齐
|
||||
|
||||
Hermes Web / dashboard 和部分本地服务有 `state.db` 级 session store。MNote 不应复制 Hermes DB,也不能把 Web DB 语义直接套到 ACP adapter;但要保存映射与审计:
|
||||
|
||||
- [ ] SQLite `ai_runtime_runs/session metadata` 保存 `acpSessionId`、Hermes profile、cwd/rootUri、lastLoadedAt、lastReplaySeq。
|
||||
- [ ] MNote session detail 能展示底层 Hermes session id 和 load/replay 状态。
|
||||
- [ ] 删除 MNote Page AI session 时,默认只删除 MNote history/audit;是否调用 Hermes delete/remove 需要单独确认,不默认删除 Hermes 外部 session store。
|
||||
- [ ] 恢复历史会话时,区分“只恢复 MNote UI 历史”和“恢复 Hermes 底层 ACP session”。
|
||||
|
||||
### H2:Hermes replay 消费
|
||||
|
||||
部分 Hermes adapter 或 Web/API resume 路径可能 replay 历史。MNote 当前已有自己的 UI history,因此只要收到 replay,就必须做去重和排序:
|
||||
|
||||
- [ ] replay event 带 `source=adapter_replay`,不能当作新的当前 run 输出。
|
||||
- [ ] replay user/assistant/tool 与 MNote 已有消息按 content/run/time 去重。
|
||||
- [ ] replay tool start/completed 能恢复 tool card 状态,但不触发新的写入刷新。
|
||||
- [ ] replay plan/todo 恢复 plan panel,但不混入 assistant final。
|
||||
- [ ] replay 完成后再允许当前 prompt streaming bubble 开始。
|
||||
|
||||
### H3:Hermes 功能面
|
||||
|
||||
- [ ] 支持 Hermes ACP `session/list` / `session/resume` / `session/fork` 的能力探测,先展示为 debug/高级入口,不默认替代 MNote session list。
|
||||
- [ ] 权限请求沿 Hermes ACP decision loop 回传,不做前端假 allow。
|
||||
- [ ] tool `locations[]` 保留并能打开 MNote resource tab。
|
||||
- [ ] image prompt capability 只在 MNote target/attachment 明确支持时开启,不把图片 silently 变成文本。
|
||||
|
||||
## 11. Track W:Hermes Web/API Chat
|
||||
|
||||
Hermes 官方 Web UI 是 dashboard,不是 MNote Page AI 的直接聊天实现。但它提供两个有用原则:session 是可查询/搜索/导出的持久资产;server 端负责 auth、session token 和 API 管理。
|
||||
|
||||
### W0:ChatOnly / API Chat 边界
|
||||
|
||||
- [ ] ChatOnly / API Chat 继续走 provider conversation 或 MNote server transcript,不复用 ACP session/load 设计。
|
||||
- [ ] providerConversationId 只属于网页 provider,不与 `acpSessionId` 混用。
|
||||
- [ ] API Chat 不写网页 provider conversation binding。
|
||||
- [ ] MNote UI history 和 provider 远端历史删除语义分开。
|
||||
|
||||
### W1:Session dashboard 能力
|
||||
|
||||
参考 Hermes Web UI 的 sessions dashboard,MNote Page AI 可补以下管理能力:
|
||||
|
||||
- [ ] session list 展示 runtime、profile、message count、tool count、active/cold/new 状态。
|
||||
- [ ] session detail 展示 user/assistant/tool/plan/citation 的投影,而不是 raw event dump。
|
||||
- [ ] session search 支持按用户问题、assistant final、tool name、citation file 搜索。
|
||||
- [ ] export session JSON/Markdown 包含 MNote session id、runs、events、runtime binding summary。
|
||||
- [ ] rename/delete/bulk delete 明确只作用 MNote session,外部 provider/Hermes DB 删除需单独入口。
|
||||
|
||||
## 12. Track U:UI 与浏览器验证
|
||||
|
||||
### U0:可见状态
|
||||
|
||||
- [ ] Page AI header/debug surface 显示 runtime、profile、MNote session id、acpSessionId、sessionMode、active run status。
|
||||
- [ ] `native_live`、`cold_resumed`、`new_session` 用小状态标签表示,不占用 assistant 正文。
|
||||
- [ ] `输出中` 只在 run active 且 assistant bubble streaming 时显示;terminal 后必须消失。
|
||||
|
||||
### U1:Smoke 脚本
|
||||
|
||||
- [ ] `task558-page-ai-reasonix-live-session-context-smoke.js`:两轮 Reasonix,同一 acpSessionId,第二轮“可以”能引用上一轮。
|
||||
- [ ] `task559-page-ai-terminal-status-reconciliation-smoke.js`:高频 thought/message delta 后 terminal 不丢,SQLite status terminal,UI 无输出中。
|
||||
- [ ] `task560-page-ai-hermes-load-replay-smoke.js`:Hermes stored acpSessionId -> session/load;覆盖无 replay 与有 replay 两种变体 -> 当前 prompt。
|
||||
- [ ] `task561-page-ai-session-dashboard-smoke.js`:session list/detail/search/export/rename/delete 的最小可见验证。
|
||||
|
||||
### U2:回归矩阵
|
||||
|
||||
- [ ] Reasonix live binding 不破坏 Hermes `session/load`。
|
||||
- [ ] Hermes replay 不污染 Reasonix live transcript。
|
||||
- [ ] ChatOnly provider conversation binding 不受 ACP binding pool 影响。
|
||||
- [ ] local-folder allowed roots / readonly guard / agent audit 对所有 runtime 生效。
|
||||
- [ ] tool write receipt 仍通过 watcher/event bus 刷新当前文档,不重复全量 sidebar refresh。
|
||||
|
||||
## 13. 建议执行顺序
|
||||
|
||||
### 13.1 第一批:先修共性底座
|
||||
|
||||
1. Event store / terminal reconciliation。
|
||||
2. Runtime policy 表和 session 身份字段梳理。
|
||||
3. 前端 `输出中` / active-run 清理。
|
||||
|
||||
这批完成后,Reasonix 和 Hermes 后续调试不会继续被 stale status 干扰。
|
||||
|
||||
### 13.2 第二批:Reasonix 原生连续会话
|
||||
|
||||
1. Reasonix fake ACP fixture。
|
||||
2. Reasonix live binding pool。
|
||||
3. Reasonix 两轮“可以” browser smoke。
|
||||
4. Cold resume fallback。
|
||||
|
||||
这批完成后,解决当前用户可见的上下文断裂。
|
||||
|
||||
### 13.3 第三批:Hermes 官方 load/replay
|
||||
|
||||
1. Hermes fake ACP fixture。
|
||||
2. `session/load` replay 消费与去重。
|
||||
3. Hermes session metadata/debug。
|
||||
4. Hermes load/replay browser smoke。
|
||||
|
||||
这批完成后,Hermes 与 Reasonix 的上下文机制真正分开。
|
||||
|
||||
### 13.4 第四批:会话管理与其它功能
|
||||
|
||||
1. Page AI session dashboard:list/detail/search/export/rename/delete。
|
||||
2. Tool locations/citations/open action 统一。
|
||||
3. permission decision loop 复核。
|
||||
4. model/profile/skills/MCP 状态面板对齐。
|
||||
|
||||
## 14. 验收矩阵
|
||||
|
||||
### 14.1 单元 / 集成测试
|
||||
|
||||
- [ ] fake Reasonix ACP:`loadSession=false`,同一 MNote session 连续两轮只调用一次 `session/new`,第二轮仍用同一 `acpSessionId`。
|
||||
- [ ] fake Reasonix ACP:第二轮输入“可以”,adapter 可读到上一轮 loop 历史,回答能引用上一轮提议。
|
||||
- [ ] fake Hermes ACP:`loadSession=true`,`session/load` 成功后可 prompt;replay 变体中 MNote 能消费且去重。
|
||||
- [ ] manager 丢失后 cold resume:后端创建新 ACP session,并注入 bounded MNote context packet。
|
||||
- [ ] SQLite status reconciliation:有 `run.completed` event 后,`ai_runtime_runs.status` 必为 `completed`。
|
||||
- [ ] 高频 delta:3000 条 `thought.delta` + terminal,不丢 terminal,前端最终无 `data-page-ai-streaming="true"`。
|
||||
|
||||
### 14.2 浏览器 smoke
|
||||
|
||||
- [ ] Reasonix:打开 Page AI,发送“我下一条只说可以,你要根据这条消息继续检查 LightRAG 服务”。
|
||||
- [ ] Reasonix:第二轮发送“可以”。
|
||||
- [ ] Reasonix:捕获 `/api/hermes/client/runs` 和 `session.info.updated`,两轮实际 `acpSessionId` 相同。
|
||||
- [ ] Reasonix:第二轮可见回答能理解“可以”指上一轮提议,不要求用户重新说明任务。
|
||||
- [ ] Hermes:恢复已有 MNote Page AI session 后,先调用 `session/load`;若收到 replay,去重后再开始当前 prompt。
|
||||
- [ ] 通用:回答完成后页面不显示 `AI · 输出中`。
|
||||
- [ ] 通用:SQLite `ai_runtime_runs` 对所有 terminal run 都进入 terminal status。
|
||||
|
||||
### 14.3 回归边界
|
||||
|
||||
- [ ] Hermes runtime 仍可走 `session/load`,不被 Reasonix special case 破坏。
|
||||
- [ ] Reasonix runtime 不调用或不依赖 `session/load`。
|
||||
- [ ] ChatOnly provider conversation binding 不受 Reasonix live binding / Hermes load replay 影响。
|
||||
- [ ] local-folder allowed roots / readonly guard / agent audit 仍按现有 Page AI envelope 生效。
|
||||
- [ ] Stop/abort 能取消当前 prompt,不误删可复用 session;需要销毁 session 时必须有显式 close/reset 语义。
|
||||
|
||||
## 15. 不做事项
|
||||
|
||||
- 不让 MNote 为 Reasonix 重写一套长期 agent memory。
|
||||
- 不把完整 transcript 每轮强塞给 native-live Reasonix,避免破坏 Reasonix 官方 prefix/cache 设计。
|
||||
- 不把 `mnote.doc.*` 工具扩成 local-first 普通 Markdown 主路径。
|
||||
- 不把 Reasonix 官方 desktop 的 React/Wails UI 结构照搬到 MNote;只对齐 session、event、busy/turn-complete 和 response projection 语义。
|
||||
- 不继续把 `session/load` 失败当成普通 fallback 成功;对 Reasonix 这是上下文断裂事件,必须可见、可审计。
|
||||
|
||||
## 16. 当前建议优先级
|
||||
|
||||
优先做 `13.1 第一批:共性底座`,再做 `13.2 Reasonix 原生连续会话`。
|
||||
|
||||
原因:
|
||||
|
||||
- 共性底座直接修复 `ACP SSE lagged`、SQLite status stale、active-run stale 和 `输出中` 卡住;不先修它,Reasonix/Hermes 的会话验证会继续被假状态干扰。
|
||||
- Reasonix live binding 直接修复“回复可以不理解上一轮”的根因。
|
||||
- Hermes `session/load` / replay 应在底座稳定后推进,否则 replay 历史和当前 streaming 输出容易继续混在同一条 UI/event 路径里。
|
||||
- cold resume 是必要兜底,但不能替代 native-live;如果先做 transcript 注入,容易把 Reasonix 官方 session 模型继续绕坏。
|
||||
|
||||
## 17. 文件级实施映射
|
||||
|
||||
本节用于把上面的架构拆到真实 owner,避免后续实现继续在单个 route 中临时判断。
|
||||
|
||||
### 17.1 后端 owner
|
||||
|
||||
| 文件 | 当前职责 | 后续改动边界 |
|
||||
| --- | --- | --- |
|
||||
| `rust/crates/mnote-web/src/routes/hermes_client.rs` | Page AI run/create/stream、ACP active run、SQLite runtime event 持久化、active-run API | 第一批只改 terminal reconciliation、active-run 过滤、runtime policy 分派入口;不要把 Reasonix/Hermes 细节继续散落到所有分支 |
|
||||
| `rust/crates/mnote-web/src/acp_session_manager.rs` | ACP `session/new`、`session/load`、`session/prompt`、event mapping | 增加 capability-aware `ensure_session_with_policy` 或等价接口;Reasonix policy 下不得尝试 `session/load` |
|
||||
| `rust/crates/mnote-web/src/acp_runtime.rs` | runtime config、process switch、active client 生命周期 | 当前只有一个 active runtime process;若支持多 live binding,需要明确是否仍共享同一 process,还是按 runtime/profile/workspace 拆 manager pool |
|
||||
| `rust/crates/mnote-web/src/acp_bridge.rs` | ACP event -> SSE event projection | 补 terminal priority、thought/message delta 限流策略、tool/permission/plan 分类,避免 UI 和持久化各自猜事件语义 |
|
||||
| `rust/crates/mnote-web/src/routes/mod.rs` | Page AI route 注册 | 只在新增 debug/session dashboard API 时扩展,不放业务逻辑 |
|
||||
|
||||
### 17.2 前端 owner
|
||||
|
||||
| 文件 | 当前职责 | 后续改动边界 |
|
||||
| --- | --- | --- |
|
||||
| `rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js` | Page AI 输入、run streaming、event 消费、active run snapshot | terminal event 后强制清理 streaming/busy;显示 sessionMode/debug;不要把 thought/tool/plan 拼进 final |
|
||||
| `rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js` | session list/history/active-run 恢复 | active-run API 返回 stale 时要忽略 terminal run;后续接 session dashboard/detail/search/export |
|
||||
| `rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js` | 消息和 tool card 渲染 | `native_live/cold_resumed/new_session` 用状态标签,不占 assistant 正文;输出中只从 streaming attr 得出 |
|
||||
| `rust/crates/mnote-web/src/ssr/styles.rs` | `AI · 输出中` 等可见样式 | 只保留样式,不承担状态判断;状态必须来自 JS runtime 和后端 terminal truth |
|
||||
|
||||
### 17.3 测试 / fixture owner
|
||||
|
||||
| 文件或建议文件 | 目的 |
|
||||
| --- | --- |
|
||||
| `rust/crates/mnote-web/src/acp_session_manager.rs` tests | fake Reasonix/Hermes ACP 的 `session/load` / `session/new` / `session/prompt` 单元合同 |
|
||||
| `scripts/task558-page-ai-reasonix-live-session-context-smoke.js` | 浏览器两轮 Reasonix,上下文连续、同一底层 session、短回复可解析 |
|
||||
| `scripts/task559-page-ai-terminal-status-reconciliation-smoke.js` | 高频 delta 后 terminal 不丢、SQLite terminal、UI 无输出中 |
|
||||
| `scripts/task560-page-ai-hermes-load-replay-smoke.js` | Hermes stored acpSessionId -> load;覆盖无 replay / 有 replay 两种变体 -> 当前 prompt |
|
||||
| `scripts/task561-page-ai-session-dashboard-smoke.js` | session list/detail/search/export/rename/delete 可见闭环 |
|
||||
|
||||
## 18. Runtime 状态机与不变量
|
||||
|
||||
### 18.1 Run 状态机
|
||||
|
||||
```text
|
||||
created
|
||||
-> queued
|
||||
-> running
|
||||
-> tool_calling
|
||||
-> running
|
||||
-> completed | failed | aborted
|
||||
|
||||
terminal = completed | failed | aborted
|
||||
```
|
||||
|
||||
不变量:
|
||||
|
||||
- `ai_runtime_events` 中出现 terminal event 后,`ai_runtime_runs.status` 必须在同一事务或 reconciliation pass 中进入 terminal。
|
||||
- `/active-run` 只能返回非 terminal run;如果 SQLite status 非 terminal 但 event journal 已 terminal,API 必须先补状态再返回空。
|
||||
- 前端收到 terminal 后必须清除对应 assistant bubble 的 `streaming=true`,即使最后一段 message delta 没到。
|
||||
- `acp_pending` 只能是创建到 SSE 建立前的短暂状态,不得作为完成后残留状态。
|
||||
|
||||
### 18.2 Reasonix binding 状态机
|
||||
|
||||
## 19. 完成记录(2026-06-10)
|
||||
|
||||
本稿已按 `13.1` 到 `13.4` 的顺序完成可执行闭环,代码与验证证据如下:
|
||||
|
||||
- 第一批共性底座:已实现 runtime policy、session 身份字段、terminal reconciliation、active-run 终态过滤、`/runs/{id}/events` synthetic terminal、前端 `streaming` / `输出中` 强制清理。验证:`page_ai_session_active_run*`、`acp_runtime_event_persistence_reconciles_terminal_status`、`task559-page-ai-terminal-status-reconciliation-smoke.js`。
|
||||
- 第二批 Reasonix:已实现 `loadSession=false` fake fixture、live binding pool、Reasonix run 后不关闭 manager、同一 MNote session 二轮复用同一 `acpSessionId`、cold resume packet。验证:`test_reasonix_live_session_keeps_short_reply_context`、`task558-page-ai-reasonix-live-session-context-smoke.js`、`page-ai-browser-verify` Reasonix clean reply。
|
||||
- 第三批 Hermes:已保留 Hermes `session/load` 路径,显式 runtime config 不再被 `MNOTE_WEB_HERMES_BIN` 覆盖;已捕获 `session/load` 期间 adapter replay,标记 `source=adapter_replay` 并在当前 streaming bubble 中去重/忽略。验证:`test_ensure_session_loads_stored`、`test_hermes_load_replay_events_before_prompt`、`task560-page-ai-hermes-load-replay-smoke.js` 覆盖 no replay / replay。
|
||||
- 第四批 session dashboard 与状态面板:既有 history UI 的 list/detail/search/rename/delete 已复核;本轮新增 export API 与 UI 操作,修复 history action 点击遮挡;runtime/profile/session/acpSessionId/status 继续由 Page AI header/debug surface 和 session row meta 展示。验证:`hermes_client_acp_session_export_uses_sqlite_store`、`task561-page-ai-session-dashboard-smoke.js`。
|
||||
- ChatOnly / API Chat 边界:ACP binding pool 与 Hermes load/replay 未复用 provider conversation binding。验证:`cargo test -p mnote-web api_chat`、`cargo test -p mnote-web chatonly_doubao_session_delete`。
|
||||
|
||||
逐项归档说明:
|
||||
|
||||
- `session/list` / `session/resume` / `session/fork` 的 Hermes ACP 高级入口保持为 debug/future 能力探测,不替代 MNote session list;本轮完成的是 stored `acpSessionId -> session/load -> prompt` 的官方必需路径。
|
||||
- 外部 Hermes DB / provider 远端删除未自动执行;删除 MNote Page AI session 仍只默认删除/标记 MNote history 与 provider binding,外部 store 删除需要单独确认。
|
||||
- permission decision loop、tool locations/citations、allowed roots、readonly guard、tool write receipt 属于既有 Page AI envelope/tool projection 路径,本轮只复核不改写;后续若专项扩展需另开设计,不继续堆入 7-58。
|
||||
- 流式 apply / review session、Reasonix 长期 memory、每轮完整 transcript 注入明确不做,保持 `7-18` local-first agent file editing 主线。
|
||||
|
||||
状态:`DONE`。本文件可移入 `design/07-ai/done/`。
|
||||
|
||||
```text
|
||||
missing
|
||||
-> new_session
|
||||
-> native_live.idle
|
||||
-> native_live.running
|
||||
-> native_live.idle
|
||||
-> cold_required | closed
|
||||
```
|
||||
|
||||
不变量:
|
||||
|
||||
- 同一 `mnoteSessionId + runtime + profile + workspaceKey + allowedRootsHash` 同时最多一个 running prompt。
|
||||
- prompt 完成只释放 run,不销毁 Reasonix ACP session manager。
|
||||
- abort 只取消当前 prompt;reset/close 才销毁 binding。
|
||||
- workspace/root/model/profile/allowedRootsHash 变化必须关闭旧 binding,并把下一轮标记为 `cold_resumed` 或 `new_session`。
|
||||
|
||||
### 18.3 Hermes ACP 状态机
|
||||
|
||||
```text
|
||||
missing
|
||||
-> has_stored_acp_session_id
|
||||
-> load_attempted
|
||||
-> adapter_loaded | load_failed
|
||||
-> prompt_running
|
||||
-> completed | failed | aborted
|
||||
```
|
||||
|
||||
不变量:
|
||||
|
||||
- 有 stored `acpSessionId` 时必须先 `session/load`,不能跳过 load 直接 prompt。
|
||||
- `session/load` 成功只表示 adapter 当前承认该 session;是否 replay history 由事件流证据确认。
|
||||
- replay event 必须标记 `source=adapter_replay` 或等价来源,不能当作当前 run 的新输出。
|
||||
- load 失败不是普通成功路径,应进入 `hermesSessionMode=cold_resumed | new_session` 并写入 audit。
|
||||
|
||||
### 18.4 Hermes Web/API Chat 状态机
|
||||
|
||||
```text
|
||||
providerConversationId | serverSessionId
|
||||
-> load server transcript / compression snapshot
|
||||
-> append current user message
|
||||
-> stream provider response
|
||||
-> flush messages/status
|
||||
```
|
||||
|
||||
不变量:
|
||||
|
||||
- Web/API Chat 不复用 ACP `session/load`。
|
||||
- providerConversationId 不得写入 `acpSessionId` 字段。
|
||||
- 删除 MNote UI session 不默认删除 provider 或 Hermes 官方 DB 中的会话资产。
|
||||
|
||||
## 19. 风险登记
|
||||
|
||||
| 风险 | 影响 | 控制方式 |
|
||||
| --- | --- | --- |
|
||||
| 把 Reasonix 做成 transcript replay | 破坏 Reasonix `CacheFirstLoop` / prefix / cache 语义,短期看似恢复,长期上下文和成本异常 | native-live 优先;cold resume 必须显式标记且 bounded |
|
||||
| 把 Hermes `session/load` 写成必然成功 | 进程重启、adapter 清理或 profile 变化后产生 phantom continuity | capability + load result + replay evidence 三重判断 |
|
||||
| terminal event 与 delta 共用可丢 buffer | UI 卡 `输出中`、active-run stale、历史恢复错判 | terminal high-priority path + status reconciler |
|
||||
| replay history 与 MNote history 重复 | 用户看到重复消息、tool card 重复执行感 | replay source 标记、content/run/time 去重、replay 完成后再开当前 bubble |
|
||||
| 多 runtime 共用一个 active process manager | 切 profile/runtime 时杀掉别的 live session | 明确 process pool 设计;Reasonix binding 与 Hermes load 不共用生命周期假设 |
|
||||
| 删除语义混淆 | 删除 MNote session 时误删 Hermes DB 或 provider 远端历史 | UI 和 API 分开 MNote-only delete 与 external delete |
|
||||
| 代码中已有未完成 scaffold | 后续实现误以为 live binding 已完成,或产生重复 static / 不可编译 | 第一批实现前先跑 `cargo test -p mnote-web`,清点当前未提交改动归属,不回滚用户改动 |
|
||||
|
||||
## 20. 第一批落地切片
|
||||
|
||||
第一批只处理共享底座,不碰 Reasonix live binding 的完整生命周期,也不做 Hermes replay UI。目标是让 run 状态可信。
|
||||
|
||||
### 20.1 后端切片 A:terminal reconciliation
|
||||
|
||||
- 输入:任意 runtime 的 `run.completed` / `run.failed` / `run.aborted` event。
|
||||
- 输出:`ai_runtime_runs.status` 同步进入 terminal,`status_reason` 写入 terminal event 名称或 error code。
|
||||
- 改动点:`persist_acp_runtime_event`、`update_runtime_by_run_id`、`/active-run` 查询路径。
|
||||
- 验收:构造 `ai_runtime_events` 已 terminal 但 `ai_runtime_runs.status=acp_pending` 的 fixture,active-run 返回空并补写 status。
|
||||
|
||||
### 20.2 后端切片 B:SSE terminal priority
|
||||
|
||||
- 输入:高频 `thought.delta` / `message.delta` + terminal。
|
||||
- 输出:普通 delta 可合并或丢摘要,terminal 必达浏览器和 event store。
|
||||
- 改动点:ACP event fanout/persistence worker;如果短期不能拆 worker,至少让 terminal 事件绕过可 lag 的普通 receiver。
|
||||
- 验收:3000 条 delta 后仍收到 terminal,日志不再出现 terminal 丢失导致的 stale run。
|
||||
|
||||
### 20.3 前端切片 C:输出中清理
|
||||
|
||||
- 输入:terminal SSE、reconciliation terminal、active-run 空结果。
|
||||
- 输出:对应 assistant bubble `streaming=false`,document root 移除 active-run snapshot/busy 标记。
|
||||
- 改动点:`sidebar-page-ai-runtime.js`、`sidebar-page-ai-session-runtime.js`。
|
||||
- 验收:回答完成后 DOM 不存在 `data-page-ai-streaming="true"`,`.wolai-page-ai-message-role::after` 不再显示 `AI · 输出中`。
|
||||
|
||||
### 20.4 文档归档条件
|
||||
|
||||
本设计留在 `process/`,直到至少完成:
|
||||
|
||||
- 第一批共性底座 smoke 通过。
|
||||
- Reasonix 两轮短回复 smoke 通过。
|
||||
- Hermes load 与 replay 变体最小 smoke 通过。
|
||||
- Page AI session dashboard 的范围已拆成新设计或在本设计中完成验收。
|
||||
|
||||
满足后移动到 `design/07-ai/done/`;若后续另起 v2 覆盖本稿,则本稿移动到 `design/old/process/` 并标记 `[recycle]`。
|
||||
-622
@@ -1,622 +0,0 @@
|
||||
# 7-59 Page AI Reasonix Desktop / Hermes Web 控制面借鉴设计 v1
|
||||
|
||||
> 创建时间:2026-06-10
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> Owner:07-ai / Page AI / ACP runtime / Session dashboard / Runtime control surface
|
||||
>
|
||||
> 前置完成项:`design/07-ai/done/7-58-page-ai-reasonix-official-desktop-session-alignment-v1.md`
|
||||
>
|
||||
> 参考代码:
|
||||
> - Reasonix 官方 desktop:`reference-code/DeepSeek-Reasonix-main/desktop/`
|
||||
> - Reasonix desktop runtime:`reference-code/DeepSeek-Reasonix-main/desktop/src/App.tsx`、`desktop/src/protocol.ts`、`desktop/src/ui/*`
|
||||
> - Hermes Web UI:`reference-code/hermes-web-ui-0.5.18/README.md`、`packages/client/src/api/hermes/*`、`packages/client/src/stores/hermes/*`、`packages/server/src/routes/hermes/*`
|
||||
> - Hermes VSCode ACP host:`reference-code/hermes-vscode-main/src/sessionManager.ts`
|
||||
> - MNote 当前 Page AI:`rust/crates/mnote-web/src/routes/hermes_client.rs`、`rust/crates/mnote-web/browser/sidebar-page-ai-*.js`
|
||||
|
||||
## 1. 第一结论
|
||||
|
||||
`7-58` 已经解决了 Reasonix / Hermes 的底层会话连续性分歧:Reasonix 走 live binding,Hermes ACP 走 `session/load`。
|
||||
|
||||
本稿继续处理下一层问题:**Page AI 不应该只是一个聊天抽屉,而应该收敛成“对话 + agent 运行态 + 会话资产 + 配置/权限/工具状态”的轻量控制面。**
|
||||
|
||||
Reasonix Desktop 和 Hermes Web 各自值得借鉴的部分不同:
|
||||
|
||||
| 参考 | 值得借鉴 | 不应照搬 |
|
||||
| --- | --- | --- |
|
||||
| Reasonix Desktop | 运行态事件 reducer、busy/turn complete、queued send、plan/checkpoint/path permission/job/MCP/statusbar | Tauri/Wails 壳、完整桌面布局、Reasonix 专用设置结构 |
|
||||
| Hermes Web UI | session dashboard、usage/cost、profiles/models/providers、gateway/jobs/logs/config/status、search/export/delete 资产面 | 独立 Web UI IA、Hermes DB 真相、平台 channel 全量管理 |
|
||||
|
||||
MNote 的正确方向是:
|
||||
|
||||
```text
|
||||
Page AI drawer = 当前页上下文里的工作面
|
||||
Page AI session dashboard = MNote 自己的会话资产面
|
||||
Runtime control surface = Hermes/Reasonix/ChatOnly/API Chat 的统一状态面
|
||||
External provider/Hermes DB/Reasonix live loop = 外部或进程内 runtime,不是 MNote 长期真相
|
||||
```
|
||||
|
||||
## 2. Reasonix Desktop 对 MNote 的启发
|
||||
|
||||
### 2.1 事件不是纯文本流,而是 UI reducer 输入
|
||||
|
||||
Reasonix desktop `App.tsx` 将 incoming event 映射为明确状态:
|
||||
|
||||
- `$turn_complete`:解除 busy、清 active skill、驱动 queued send。
|
||||
- `$path_access_required` / `$confirm_required` / `$choice_required`:进入前端待决策队列。
|
||||
- `$plan_required` / `$checkpoint_required` / `$revision_required` / `$step_completed`:进入 plan/checkpoint UI。
|
||||
- `$mcp_specs`:显示 MCP 配置/握手/连接/失败/工具数。
|
||||
- `$jobs`:显示后台 job 列表、运行/退出状态、stop/stop all。
|
||||
- `$ctx_breakdown` / `$balance` / usage:进入 statusbar / context panel。
|
||||
- `$session_loaded`:恢复消息、清队列、恢复 carryover usage。
|
||||
|
||||
MNote 当前 Page AI 已有 message/tool/permission/session/run 的事件基础,但仍偏“聊天消息优先”。后续应把 Page AI event projection 正式拆成:
|
||||
|
||||
| Projection | UI owner | 事件来源 |
|
||||
| --- | --- | --- |
|
||||
| final assistant | chat transcript | `message.delta` / final output |
|
||||
| thought/reasoning | folded reasoning panel | `thought.delta` |
|
||||
| tool card | tool timeline | `tool.started/completed/failed` |
|
||||
| permission queue | permission panel/dialog | `permission.*` / ACP decision request |
|
||||
| plan/checkpoint | plan panel | `plan.updated` / future `checkpoint.*` |
|
||||
| runtime status | header/statusbar | `session.info.updated` / `run.*` / `usage.updated` |
|
||||
| MCP/status | runtime control panel | runtime manifest / MCP bridge health |
|
||||
| background jobs | jobs popover/panel | future `job.*` / tool long-running receipt |
|
||||
|
||||
### 2.2 busy 后输入应排队,而不是丢弃或再开 session
|
||||
|
||||
Reasonix Desktop 有 `queuedSends`:busy 时用户继续发送,不直接开新底层 session;turn complete 后 FIFO 自动发送。
|
||||
|
||||
MNote 对 Reasonix 尤其需要这个模型,因为同一 live binding 内必须串行 prompt。当前 `7-58` 对 busy binding 会返回 `acp_reasonix_session_busy`。后续应该升级为:
|
||||
|
||||
```text
|
||||
Reasonix binding running
|
||||
-> 新输入进入 MNote Page AI session queue
|
||||
-> UI 显示 queued count / 可取消
|
||||
-> 当前 run terminal 后自动启动下一条 run
|
||||
-> 仍复用同一 live binding / acpSessionId
|
||||
```
|
||||
|
||||
验收不应只是“不报 busy”,而是验证两条快速输入仍同一 `acpSessionId` 且顺序正确。
|
||||
|
||||
### 2.3 Stop 语义必须拆三层
|
||||
|
||||
Reasonix Desktop 的 stop 更接近“停止当前 turn/job”,不是删除 session。MNote 后续 UI 应明确三个按钮/动作:
|
||||
|
||||
| 动作 | 含义 | 默认影响 |
|
||||
| --- | --- | --- |
|
||||
| Stop turn | 取消当前 prompt | 保留 MNote session 与 live binding |
|
||||
| Reset runtime | 关闭 Reasonix live binding / Hermes ACP manager | 保留 MNote history,下一轮 cold/new |
|
||||
| Delete session | 删除 MNote session history/audit | 不默认删除外部 Hermes DB / provider 会话 |
|
||||
|
||||
这能避免用户点击“停止”后误以为历史或底层 session 被删。
|
||||
|
||||
### 2.4 MCP / jobs / usage 要进状态条,不进 assistant 正文
|
||||
|
||||
Reasonix Desktop 把 MCP specs、jobs、usage、balance 放到 sidebar/statusbar/settings,而不是让模型自然语言解释。
|
||||
|
||||
MNote 后续应在 Page AI header 下增加 compact status strip:
|
||||
|
||||
```text
|
||||
runtime: Reasonix native-live | Hermes loadable | ChatOnly provider
|
||||
session: mnoteSessionId / acpSessionId / mode
|
||||
model/profile: profile id + resolved model
|
||||
roots: write/read-only grant count
|
||||
MCP: connected/failed/tool count
|
||||
tools: enabled/disabled count
|
||||
jobs: running count
|
||||
usage: last run tokens/cost/cache if available
|
||||
```
|
||||
|
||||
## 3. Hermes Web 对 MNote 的启发
|
||||
|
||||
Hermes Web UI 的 README 将能力分成 Chat、Platform Channels、Usage、Jobs、Model Management、Multi-Profile/Gateway、File Browser、Group Chat、Skills/Memory、Logs、Auth、Settings、Web Terminal。
|
||||
|
||||
MNote 不能照搬完整 dashboard,但可以抽取控制面能力:
|
||||
|
||||
### 3.1 Session asset dashboard
|
||||
|
||||
Hermes Web 的 session 管理包含:多会话、rename/delete/switch、SQLite session DB、按来源分组、active session pin、搜索、模型 badge、context token usage。
|
||||
|
||||
MNote 已有最小 list/detail/search/export/rename/delete。后续应补:
|
||||
|
||||
- active session pin:正在 run 的 session 固定到历史列表顶部。
|
||||
- runtime grouping:Reasonix / Hermes ACP / ChatOnly / API Chat 分组。
|
||||
- status filters:active / completed / failed / cold_resumed / native_live / replay_seen。
|
||||
- search fields:user question、assistant final、tool name、citation path、changed file、error code。
|
||||
- export profile:JSON / Markdown / audit JSONL 三种导出。
|
||||
- delete semantics badge:仅删 MNote history,外部 provider/Hermes DB 不默认删除。
|
||||
|
||||
### 3.2 Usage / cost / context panel
|
||||
|
||||
Hermes Web 有 usage analytics:token breakdown、daily trend、model distribution、cost/cache。
|
||||
|
||||
MNote 不需要先做全局财务面板,但 Page AI 应先做 session/run 级 usage:
|
||||
|
||||
- 当前 run:prompt/completion/reasoning/cache tokens。
|
||||
- 当前 session:累计 tokens/cost/cache hit。
|
||||
- 当前 runtime:reserved tool/system/context tokens。
|
||||
- fallback/cold resume:显示 packet 大小和截断原因。
|
||||
|
||||
Reasonix Desktop 的 `ctx_breakdown` 与 Hermes Web 的 usage store 可以合并成 MNote 的 `page_ai_usage_projection.v1`。
|
||||
|
||||
### 3.3 Profiles / models / providers
|
||||
|
||||
Hermes Web 将 providers/models/profiles 拆成独立设置面,并支持 credential pool、provider presets、default model switching。
|
||||
|
||||
MNote 当前 Page AI profile selector 已可用,但缺少“解析后的真实模型与凭据状态”。后续应显示:
|
||||
|
||||
| 字段 | 示例 |
|
||||
| --- | --- |
|
||||
| profileId | `mnoteai` / `shared_api_deepseek_flash_chat` |
|
||||
| providerKind | `hermes-acp` / `api-chat` / `web-provider` |
|
||||
| resolvedModel | `deepseek-v4-flash` / `gpt-5.5` |
|
||||
| credentialStatus | configured / missing / inherited / masked |
|
||||
| runtimeBinary | `hermes` / `node scripts/reasonix-acp-wrapper.mjs` |
|
||||
| configSource | control-plane / env / profile file |
|
||||
|
||||
### 3.4 Gateway / logs / jobs
|
||||
|
||||
Hermes Web 把 gateway、logs、jobs 作为运维一等公民。MNote 后续应只引入轻量版:
|
||||
|
||||
- Runtime health:Hermes ACP process、Reasonix wrapper、MNote MCP bridge、LightRAG MCP/HTTP。
|
||||
- Logs tail:最近 N 条 ACP stderr、tool failed、permission denied、MCP failed。
|
||||
- Jobs:长任务工具的 running/completed/failed,不把后台进度塞进 assistant final。
|
||||
|
||||
### 3.5 Skills / memory
|
||||
|
||||
Hermes Web 有 skills/memory 浏览。Reasonix Desktop 也有 skills、memory、MCP specs。
|
||||
|
||||
MNote Page AI 后续应把 skill/memory 作为 runtime capability,而不是继续分散在设置页:
|
||||
|
||||
- 当前 runtime 可用 skills。
|
||||
- enabled/disabled user preference。
|
||||
- inline/subagent 模式。
|
||||
- project/global/builtin scope。
|
||||
- memory source:Reasonix native memory / Hermes memory / MNote profile note。
|
||||
- 对 Reasonix native-live,不把 MNote memory 强塞到每轮 prompt;只作为 capability manifest 或 cold resume fallback 输入。
|
||||
|
||||
## 4. 目标信息架构
|
||||
|
||||
### 4.1 Page AI drawer 分区
|
||||
|
||||
```text
|
||||
Header
|
||||
runtime badge / profile / model / session mode / active-run status
|
||||
|
||||
Chat
|
||||
user / assistant final
|
||||
folded thought
|
||||
tool cards
|
||||
citations
|
||||
changed files receipt
|
||||
|
||||
Runtime
|
||||
live binding / acpSessionId / load/replay / cold resume
|
||||
MCP status
|
||||
allowed roots
|
||||
tool enablement
|
||||
permission queue
|
||||
jobs
|
||||
usage/context
|
||||
|
||||
History
|
||||
list / search / filters / export / rename / delete
|
||||
|
||||
Settings
|
||||
profile/model/provider
|
||||
skills/memory
|
||||
runtime health/logs
|
||||
```
|
||||
|
||||
### 4.2 不同 runtime 的展示差异
|
||||
|
||||
| Runtime | Header mode | Runtime panel 必显 | 不显示/禁用 |
|
||||
| --- | --- | --- | --- |
|
||||
| Reasonix ACP | `native-live` / `cold-resumed` / `new-session` | live binding、MCP specs、jobs、allowed roots、queued sends | `session/load` 成功率、provider conversation |
|
||||
| Hermes ACP | `loaded` / `load-failed-new` / `replay-seen` | acpSessionId、load/replay、profile/model、tool locations | Reasonix live binding status |
|
||||
| ChatOnly Web | `provider-conversation` | providerConversationId、remote delete status | ACP session/load/live binding |
|
||||
| API Chat | `server-transcript` | model/provider/usage、MNote transcript | provider web conversation binding |
|
||||
|
||||
## 5. 后端合同
|
||||
|
||||
### 5.1 Runtime status snapshot
|
||||
|
||||
新增或收口一个 Page AI runtime status API:
|
||||
|
||||
```text
|
||||
GET /api/page-ai/runtime/status?sessionId=...&workspaceId=...&documentId=...
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "mnote.page_ai_runtime_status.v1",
|
||||
"mnoteSessionId": "...",
|
||||
"runtime": "reasonix",
|
||||
"profile": "reasonix",
|
||||
"mode": "native_live",
|
||||
"activeRun": { "runId": "...", "status": "running" },
|
||||
"acp": {
|
||||
"acpSessionId": "...",
|
||||
"supportsSessionLoad": false,
|
||||
"replaySeen": false,
|
||||
"liveBinding": { "status": "idle", "workspaceKey": "file://..." }
|
||||
},
|
||||
"model": { "resolvedModel": "...", "providerKind": "..." },
|
||||
"roots": { "write": 1, "readOnly": 0 },
|
||||
"mcp": { "status": "connected", "toolCount": 8, "failed": [] },
|
||||
"tools": { "enabled": 12, "disabled": 2 },
|
||||
"jobs": { "running": 0 },
|
||||
"usage": { "sessionTokens": 0, "lastRunTokens": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `acpSessionId` 只作为 runtime reference,不是 MNote 长期真相。
|
||||
- Reasonix `supportsSessionLoad=false` 必须明确展示。
|
||||
- Hermes replay 只能影响 `replaySeen/replaySeq`,不能当当前 answer。
|
||||
- ChatOnly/API Chat 不返回 ACP binding。
|
||||
|
||||
### 5.2 Session search/export 升级
|
||||
|
||||
现有 `/api/hermes/client/sessions/search` 与 `/export` 后续收口为 Page AI 命名:
|
||||
|
||||
```text
|
||||
GET /api/page-ai/sessions
|
||||
GET /api/page-ai/sessions/search?q=...
|
||||
GET /api/page-ai/sessions/{id}
|
||||
GET /api/page-ai/sessions/{id}/export?format=json|markdown|jsonl
|
||||
POST /api/page-ai/sessions/{id}/rename
|
||||
DELETE /api/page-ai/sessions/{id}
|
||||
```
|
||||
|
||||
`/api/hermes/client/sessions/*` 保留 compat,但前端新代码优先走 `/api/page-ai/*`。
|
||||
|
||||
### 5.3 Queued sends
|
||||
|
||||
新增队列表:
|
||||
|
||||
```text
|
||||
page_ai_queued_runs
|
||||
id
|
||||
user_id
|
||||
workspace_id
|
||||
document_id
|
||||
session_id
|
||||
run_id
|
||||
runtime
|
||||
profile
|
||||
payload_json
|
||||
status: queued | started | cancelled | failed
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
Reasonix live binding busy 时不返回 failed,而是入队。
|
||||
|
||||
## 6. 前端实现边界
|
||||
|
||||
### 6.1 文件 owner
|
||||
|
||||
| 文件 | 改动边界 |
|
||||
| --- | --- |
|
||||
| `sidebar-page-ai-runtime.js` | 只保留 orchestration;不要继续塞 dashboard 渲染细节 |
|
||||
| `sidebar-page-ai-session-runtime.js` | session list/detail/search/export/rename/delete/filters |
|
||||
| `sidebar-page-ai-render-runtime.js` | Page AI 面板渲染,新增 Runtime / Dashboard 分区 |
|
||||
| `sidebar-page-ai-profile-runtime.js` | profile/model/provider 解析展示 |
|
||||
| `sidebar-page-ai-skill-runtime.js` | skill/memory/MCP capability 偏好 |
|
||||
| `sidebar-page-ai-permission-runtime.js` | permission decision queue |
|
||||
| 新建议:`sidebar-page-ai-runtime-status-runtime.js` | runtime status polling/event apply,不和 chat streaming 混写 |
|
||||
|
||||
### 6.2 UI 原则
|
||||
|
||||
- Runtime 状态只做 compact status strip + detail panel,不占 assistant 正文。
|
||||
- thought/tool/plan/permission/citation 分区渲染,不拼进 final answer。
|
||||
- history action 永远可点击,不靠 hover 才能完成核心操作。
|
||||
- stop/reset/delete 三类危险程度不同,按钮必须分开。
|
||||
- 短回复“可以/继续”在 `new-session` 且无 history 时显示缺上下文提示。
|
||||
|
||||
## 7. 执行顺序
|
||||
|
||||
### Phase A:Runtime status surface
|
||||
|
||||
- [x] 后端新增 `/api/page-ai/runtime/status`,返回 runtime/profile/model/session/acp/mcp/tools/jobs/usage。
|
||||
- [x] 前端 header 显示 runtime badge、session mode、active-run status。
|
||||
- [x] Runtime panel 展示 `acpSessionId`、Reasonix live binding、Hermes load/replay、allowed roots。
|
||||
- [x] 浏览器 smoke:Reasonix / Hermes / API Chat 三种 runtime 切换时 status 不串线。
|
||||
|
||||
### Phase B:Reasonix Desktop 运行态对齐
|
||||
|
||||
- [x] Reasonix busy binding 改为 queued send,不再直接 failed。
|
||||
- [x] 队列 UI 显示 queued count、queued text preview、cancel。
|
||||
- [x] 当前 run terminal 后自动启动下一条 queued run,仍复用同一 live binding。
|
||||
- [x] Stop turn 不关闭 binding;Reset runtime 才关闭 binding。
|
||||
- [x] MCP specs/status/toolCount 进入 runtime panel。
|
||||
- [x] Jobs popover 支持 long-running tool receipt / stop all。(当前无真实后台 job registry 时展示空态和 no-op stop-all,后续长任务接入只需填充 `jobs.items`。)
|
||||
- [x] Smoke:快速发送两条消息,验证 FIFO、同一 `acpSessionId`、无输出中残留。
|
||||
|
||||
### Phase C:Hermes Web 控制面最小引入
|
||||
|
||||
- [x] Page AI session API 命名从 `/api/hermes/client/sessions` 收口到 `/api/page-ai/sessions`。
|
||||
- [x] session list 支持 runtime grouping、active pin、status filters。
|
||||
- [x] search 支持 message/tool/citation/changed file/error code。
|
||||
- [x] export 支持 json/markdown/jsonl 三格式。
|
||||
- [x] usage panel 展示 session/run/model/cache/cost。
|
||||
- [x] profile/model/provider panel 展示 resolved model、credential status、config source。
|
||||
- [x] Smoke:search/filter/export/usage/profile status 在真实浏览器可见。
|
||||
|
||||
### Phase D:权限/配置/外部删除边界
|
||||
|
||||
- [x] permission decision loop 做成持久 pending queue,刷新后不丢。
|
||||
- [x] delete session 明确只删除 MNote history;外部 Hermes/provider 删除必须二次确认。
|
||||
- [x] reset runtime 只关闭 live binding/ACP manager,不删除 MNote history。
|
||||
- [x] logs tail 显示 ACP stderr、tool failed、MCP failed 最近 N 条。
|
||||
- [x] Smoke:delete/reset/stop 三类动作互不误伤。
|
||||
|
||||
## 8. 验收矩阵
|
||||
|
||||
### 8.1 Reasonix
|
||||
|
||||
- [x] `native-live` 状态清晰可见。
|
||||
- [x] busy 时第二条输入进入 queue,不开新 session。
|
||||
- [x] queued send 自动执行后仍同一 `acpSessionId`。
|
||||
- [x] Stop turn 后下一轮仍可 native-live。
|
||||
- [x] Reset runtime 后下一轮显示 `cold_resumed` 或 `new_session`。
|
||||
- [x] MCP failed 显示为 status/tool card,不污染 assistant final。
|
||||
|
||||
### 8.2 Hermes ACP
|
||||
|
||||
- [x] stored `acpSessionId` 先 `session/load`。
|
||||
- [x] replay 显示为 replay evidence / history restore,不混入当前回答。
|
||||
- [x] load failed 明确显示 `load-failed-new`。
|
||||
- [x] session dashboard 可按 Hermes profile/model 过滤。
|
||||
|
||||
### 8.3 ChatOnly / API Chat
|
||||
|
||||
- [x] providerConversationId 与 acpSessionId 不混用。
|
||||
- [x] API Chat 不写 web provider binding。
|
||||
- [x] session dashboard 仍可展示/search/export。
|
||||
- [x] delete MNote history 不默认删除远端 provider 会话。
|
||||
|
||||
### 8.4 通用 UI
|
||||
|
||||
- [x] 回答完成后无 `AI · 输出中`。
|
||||
- [x] history actions 在桌面/移动都可点击。
|
||||
- [x] runtime status 与当前 session 一致,切 session 后不显示旧 acpSessionId。
|
||||
- [x] tool/citation/permission/plan/job 不进入 assistant final。
|
||||
|
||||
## 9. 不做事项
|
||||
|
||||
- 不把 Reasonix Desktop 的 Tauri shell 或完整布局搬进 MNote。
|
||||
- 不把 Hermes Web UI 作为 MNote 的独立 dashboard 复刻。
|
||||
- 不把 Hermes DB 当成 MNote session 真相。
|
||||
- 不把 Reasonix native memory 改造成 MNote 每轮 transcript 注入。
|
||||
- 不自动删除外部 Hermes/provider session store。
|
||||
- 不在 Page AI 主路径继续叠加无界轮询;runtime status 优先事件/command result 驱动,短期 fallback 必须有退出条件。
|
||||
|
||||
## 10. 首批可执行切片
|
||||
|
||||
### 10.1 Slice A1:runtime status 后端只读快照
|
||||
|
||||
目标:先让 Page AI 能回答“当前 agent 是谁、是否还在运行、底层 session 是否连续、MCP/工具是否可用”。
|
||||
|
||||
范围:
|
||||
|
||||
- 新增 `/api/page-ai/runtime/status` 只读 route。
|
||||
- 数据先从现有 `PageAiSessionStore` / `AcpSessionManager` / profile resolver / tool registry 组合,不新增长期表。
|
||||
- Reasonix 返回 `mode=native_live|cold_resumed|new_session`,Hermes 返回 `mode=loaded|load_failed_new|replay_seen|new_session`,ChatOnly/API Chat 返回非 ACP mode。
|
||||
- active run 状态必须来自 event store / terminal reconciliation 后的最终状态,不能只看前端 streaming flag。
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web page_ai_runtime_status
|
||||
```
|
||||
|
||||
若 Rust route 暂时没有专名测试,至少补一个 fake store/unit test 覆盖 Reasonix、Hermes、API Chat 三类 snapshot。
|
||||
|
||||
### 10.2 Slice A2:Page AI header/status strip
|
||||
|
||||
目标:把 runtime status 放到聊天抽屉顶部,而不是让 assistant 正文解释。
|
||||
|
||||
范围:
|
||||
|
||||
- `sidebar-page-ai-render-runtime.js` 增加 compact status strip。
|
||||
- `sidebar-page-ai-runtime.js` 只负责加载/应用 status,不塞具体 DOM 细节。
|
||||
- 回答终止后 status strip 必须同步清理 active-run,避免 `AI · 输出中` 残留。
|
||||
- 切换 session/document 后先清空旧 status,再加载新 status,避免旧 `acpSessionId` 短暂串线。
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
```
|
||||
|
||||
并补一条真实浏览器 smoke:Reasonix/Hermes/API Chat 切换后 header badge 与当前 profile 一致。
|
||||
|
||||
### 10.3 Slice B1:Reasonix queued send 最小版
|
||||
|
||||
目标:busy binding 不再直接失败,先支持同一 Page AI session 内 FIFO 串行发送。
|
||||
|
||||
范围:
|
||||
|
||||
- 先实现内存队列或 event-store backed queue;如果实现跨刷新不稳定,再升级 `page_ai_queued_runs` 表。
|
||||
- queue item 必须记录 `sessionId`、`runId`、`runtime`、`profile`、`acpSessionId`、原始 prompt payload。
|
||||
- 当前 run terminal 后自动 dequeue 下一条,仍复用 Reasonix live binding。
|
||||
- cancel queued item 只取消队列项,不 stop 当前 run。
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
node scripts/task558-page-ai-reasonix-live-session-context-smoke.js
|
||||
```
|
||||
|
||||
新增/扩展浏览器断言:
|
||||
|
||||
- 快速发送两条消息。
|
||||
- 第二条先显示 queued。
|
||||
- 两轮完成后 `acpSessionId` 一致。
|
||||
- 第二轮短回复能引用第一轮上下文。
|
||||
- 最终无 `AI · 输出中`。
|
||||
|
||||
### 10.4 Slice C1:Page AI sessions 命名兼容层
|
||||
|
||||
目标:前端新主路径不再把 Page AI session dashboard 命名成 Hermes client。
|
||||
|
||||
范围:
|
||||
|
||||
- 新增 `/api/page-ai/sessions/*` wrapper。
|
||||
- 复用现有 `/api/hermes/client/sessions/*` handler 或 store,不重复实现。
|
||||
- 老路由继续保留 compat;测试必须证明老路由仍可用。
|
||||
- 前端 session runtime 优先请求 `/api/page-ai/sessions/*`。
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web page_ai_sessions
|
||||
node scripts/task561-page-ai-session-dashboard-smoke.js
|
||||
```
|
||||
|
||||
### 10.5 Slice C2:Hermes load/replay 可见化
|
||||
|
||||
目标:把 Hermes ACP `session/load` 与 replay 作为 runtime evidence 展示,而不是混入当前回答。
|
||||
|
||||
范围:
|
||||
|
||||
- status snapshot 增加 `loadAttempted`、`loadStatus`、`replaySeen`、`replaySeq`、`replayMessageCount`。
|
||||
- UI 显示为 runtime panel evidence。
|
||||
- 当前 assistant final 只来自当前 run final,不消费 replay 作为回答。
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
node scripts/task560-page-ai-hermes-load-replay-smoke.js
|
||||
```
|
||||
|
||||
覆盖无 replay / 有 replay 两种 fake Hermes ACP fixture。
|
||||
|
||||
## 11. Event projection 合同
|
||||
|
||||
Page AI 后续统一把 runtime events 规整成下面几类 projection。前端只能消费 projection,不直接按 provider 原始事件猜 UI 状态。
|
||||
|
||||
| Projection | 关键字段 | terminal 规则 |
|
||||
| --- | --- | --- |
|
||||
| `runProjection` | `runId/status/startedAt/endedAt/errorCode` | `completed/failed/cancelled` 必须清 active-run |
|
||||
| `messageProjection` | `role/final/thought/toolCalls/citations` | final 与 thought 分离 |
|
||||
| `sessionProjection` | `mnoteSessionId/runtime/acpSessionId/mode` | 切 session 后旧 projection 不可复用 |
|
||||
| `runtimeProjection` | `profile/model/provider/binary/configSource` | profile 切换后立即 invalidated |
|
||||
| `capabilityProjection` | `mcp/tools/skills/memory/roots` | failed capability 不污染 assistant final |
|
||||
| `decisionProjection` | `permission/choice/confirm/pathAccess` | pending 决策可刷新恢复 |
|
||||
| `queueProjection` | `queuedCount/items/cancellable` | current terminal 后 FIFO dequeue |
|
||||
| `usageProjection` | `tokens/cost/cache/contextBreakdown` | usage 缺失时显示 unknown,不伪造 0 |
|
||||
|
||||
最低要求:
|
||||
|
||||
- `AI · 输出中` 只能绑定 `runProjection.status=running|streaming`。
|
||||
- `acpSessionId` 只能来自 `sessionProjection`。
|
||||
- replay、load、MCP、job、permission 都不能写进 assistant final。
|
||||
- provider 原始 event 增加新字段时,未识别字段进入 `rawEvidence`,不直接影响 UI。
|
||||
|
||||
## 12. 风险与边界
|
||||
|
||||
### 12.1 上下文连续性的误判风险
|
||||
|
||||
短回复“可以/继续”天然依赖历史。UI 必须区分:
|
||||
|
||||
| 状态 | 用户提示 |
|
||||
| --- | --- |
|
||||
| `native_live` / `loaded` | 显示连续会话 |
|
||||
| `cold_resumed` | 显示已用 MNote history fallback 恢复 |
|
||||
| `new_session` 且 history 为空 | 提示当前没有可用上下文 |
|
||||
| `load_failed_new` | 提示底层 ACP load 失败,已新建 runtime session |
|
||||
|
||||
这比单纯把 transcript 拼进 prompt 更重要,因为 Reasonix native-live 与 Hermes load/replay 的真实连续机制不同。
|
||||
|
||||
### 12.2 删除与重置边界
|
||||
|
||||
删除、重置、停止必须保持三套语义:
|
||||
|
||||
- `Stop turn`:只影响当前 run。
|
||||
- `Reset runtime`:只影响 Reasonix live binding / Hermes ACP process 或 manager。
|
||||
- `Delete session`:只影响 MNote session history;外部 Hermes DB / provider 会话删除必须另设显式二次确认。
|
||||
|
||||
### 12.3 轮询边界
|
||||
|
||||
runtime status 首选 event apply / command result refresh。短期允许有界 fallback,但必须满足:
|
||||
|
||||
- 仅在 Page AI drawer 可见且当前 session active 时启用。
|
||||
- interval 有最大时长和 stop 条件。
|
||||
- run terminal、session switch、drawer close 都必须停止。
|
||||
- 后续进入 done 前,要记录是否已替换为 event-driven。
|
||||
|
||||
## 13. 设计归档规则
|
||||
|
||||
本稿进入 `done/` 前必须满足:
|
||||
|
||||
- Phase A-D checklist 至少按 slice 完成,并记录每个 slice 的测试命令。
|
||||
- 浏览器 smoke 证明 Reasonix、Hermes ACP、ChatOnly/API Chat 不串状态。
|
||||
- 用户最初两个问题已有可见验收:短回复能继承上下文,回答完成不残留 `AI · 输出中`。
|
||||
- 若某项因为外部 provider 权限、Hermes DB 删除策略或 Reasonix 官方行为无法完成,必须在本稿末尾追加“归档说明”,不能只留 unchecked TODO。
|
||||
|
||||
## 14. 本稿完成条件
|
||||
|
||||
- Phase A-D checklist 有真实代码、单测或 browser smoke。
|
||||
- Reasonix queued send / stop / reset 语义明确可见。
|
||||
- Hermes-style session dashboard 不再挂在 `/api/hermes/client/*` 命名主路径上。
|
||||
- Runtime status 能解释用户最关心的四件事:当前 agent 是谁、上下文是否连续、工具/MCP 是否可用、为什么还在输出中或已经结束。
|
||||
- ChatOnly / API Chat 回归不受 ACP runtime status 改动影响。
|
||||
|
||||
## 15. 实施记录
|
||||
|
||||
### 2026-06-10 Slice A-D 完成记录
|
||||
|
||||
已完成:
|
||||
|
||||
- 后端新增 `/api/page-ai/runtime/status`,返回 `runtime/profile/model/session/acp/mcp/tools/jobs/usage/logs`。
|
||||
- 前端 Page AI header 增加 runtime status strip,runtime panel 增加 status detail。
|
||||
- `AI · 输出中` / active-run 清理继续走 terminal reconciliation;`session.created` 不再被判定为 active run。
|
||||
- Reasonix 前端 busy/queued/running 发送改为 client FIFO queue:当前 run 结束后自动发送下一条,仍复用当前 MNote session 和已保存的 `acpSessionId`。
|
||||
- Reasonix live binding 的 direct busy fallback 不再立即返回 `acp_reasonix_session_busy` failed;stream 侧等待 binding idle 后继续同一 live binding。
|
||||
- 队列 UI 显示 queued count、preview、cancel;client queued item 可本地取消。
|
||||
- 新增 `/api/page-ai/sessions/*` 主命名 route,旧 `/api/hermes/client/sessions/*` 保留 compat。
|
||||
- 前端 session runtime 已切到 `/api/page-ai/sessions/*`。
|
||||
- Hermes replay 继续作为 `adapter_replay` evidence;runtime status 展示 `replaySeen/replaySeq/replayMessageCount`。
|
||||
- permission pending queue 增加 localStorage 恢复,刷新后可恢复未决 permission。
|
||||
- 新增 `/api/page-ai/runtime/reset`,只关闭 Reasonix live binding / runtime binding,不删除 MNote history 或外部 provider session。
|
||||
- session delete 默认只删除 MNote history / 本地 provider binding 标记;外部 provider 远端删除需要 `deleteExternalProviderSession=1`。
|
||||
- session dashboard 增加 runtime/status filter、active pin、可见 action,前端主路径使用 `/api/page-ai/sessions/*`。
|
||||
- session export 增加 `json/markdown/jsonl` 三格式。
|
||||
- runtime panel 增加 jobs/logs tail;当前无真实长任务 registry 时显示空态,`Stop all` 不删除 history / runtime,只在无 job 时提示无可停止项。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
cargo test -p mnote-web page_ai_runtime_status -- --nocapture
|
||||
cargo test -p mnote-web page_ai_session_active_run -- --nocapture
|
||||
cargo test -p mnote-web delete_session -- --nocapture
|
||||
cargo test -p mnote-web hermes_client -- --nocapture
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js
|
||||
node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
node scripts/task558-page-ai-reasonix-live-session-context-smoke.js
|
||||
node scripts/task560-page-ai-hermes-load-replay-smoke.js
|
||||
node scripts/task561-page-ai-session-dashboard-smoke.js
|
||||
```
|
||||
|
||||
最新验证输出:
|
||||
|
||||
- `cargo test -p mnote-web hermes_client -- --nocapture`:87 passed。
|
||||
- `node scripts/task558-page-ai-reasonix-live-session-context-smoke.js`:通过,验证快速连续发送、queued preview、同一 `acpSessionId`、第二轮“可以”继承上下文、无输出中残留。
|
||||
- `node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js`:通过,验证 terminal reconciliation、runtime strip、runtime panel jobs/logs/status 可见。
|
||||
- `node scripts/task560-page-ai-hermes-load-replay-smoke.js`:通过,覆盖 no replay / with replay。
|
||||
- `node scripts/task561-page-ai-session-dashboard-smoke.js`:通过,验证 `/api/page-ai/sessions/*`、search、status filter、export `json/markdown/jsonl`、rename/delete。
|
||||
|
||||
归档说明:
|
||||
|
||||
- 当前 jobs 面板已经有 `jobs.running/items` 合同、空态和 Stop all 控件;由于 Page AI 还没有真实 long-running job registry,本稿不伪造 job receipt,不接外部 provider job 删除。后续长任务工具接入时只扩充 `jobs.items` 和 stop handler。
|
||||
- 外部 Hermes/provider 远端删除仍不提供默认 UI 动作;后端只接受显式 `deleteExternalProviderSession=1`,前端普通 Delete session 只删 MNote history。
|
||||
@@ -1,308 +0,0 @@
|
||||
# 7-6 [done] mnote Hermes plugin tool 合同 v1
|
||||
|
||||
> 更新时间:2026-05-14
|
||||
>
|
||||
> 上位依据:`design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`、`design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`
|
||||
>
|
||||
> Hermes Web UI 参考:`packages/client/src/api/hermes/plugins.ts`、`packages/client/src/api/hermes/skills.ts`、`packages/server/src/services/hermes/plugins.ts`、`packages/server/src/services/hermes/chat-run-socket.ts`
|
||||
>
|
||||
> 2026-05-16 口径补充:本文定义的是页面 AI 最小可用工具合同。`mnote.page.save` 只作为页面级兜底写入工具,不再代表长期精确块编辑方案;页面/块级 AI 工具体系路线图已归档到 `design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`。
|
||||
>
|
||||
> 2026-05-22 归档治理补充:旧 `7-10` 已移入 `design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`,不再作为当前执行验收入口;`7-14` 也已移入 `design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`。当前 AI 编辑 active 口径以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准,MNote 不再提供普通 Markdown 编辑工具。
|
||||
|
||||
## 1. 总边界
|
||||
|
||||
- [x] Hermes 看到的工具名必须是 `mnote.*`,不是 `mnote-cli`、Next route 或前端私有函数。
|
||||
- [x] plugin 内部可以临时调用 `mnote-cli` JSON adapter,但这只是内部 adapter,不是长期 tool 名称。
|
||||
- [x] Hermes plugin 不直接写 Convex;所有写入必须回到 Rust runtime / kernel。
|
||||
- [x] mnote 只保存业务事实、audit、artifact、edge、page/body/title/options 结果,不保存 Hermes 聊天历史。
|
||||
- [x] 所有 tool call 必须携带 `sessionId/runId/toolCallId/traceId` 便于串联 Hermes run 与 Rust command。
|
||||
|
||||
## 2. 统一入参
|
||||
|
||||
```json
|
||||
{
|
||||
"toolName": "mnote.page.get",
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"actorId": "user_1",
|
||||
"sessionId": "mnote_doc_1_trace_1",
|
||||
"runId": "run_123",
|
||||
"toolCallId": "call_123",
|
||||
"traceId": "trace_1",
|
||||
"idempotencyKey": "idem_123",
|
||||
"dryRun": false,
|
||||
"capabilityScope": ["page.read"],
|
||||
"args": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 统一出参
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"toolName": "mnote.page.get",
|
||||
"toolCallId": "call_123",
|
||||
"traceId": "trace_1",
|
||||
"result": {},
|
||||
"audit": {
|
||||
"effect": "read",
|
||||
"commandId": null,
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"toolName": "mnote.page.save",
|
||||
"toolCallId": "call_123",
|
||||
"traceId": "trace_1",
|
||||
"result": null,
|
||||
"audit": {
|
||||
"effect": "none",
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1"
|
||||
},
|
||||
"error": {
|
||||
"code": "mnote_tool_permission_denied",
|
||||
"message": "当前用户没有页面写权限"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 第一批工具 schema
|
||||
|
||||
### `mnote.page.get`
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"includeBody": { "type": "boolean", "default": true },
|
||||
"includeOptions": { "type": "boolean", "default": true },
|
||||
"includeBlocks": { "type": "boolean", "default": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"toolName": "mnote.page.get",
|
||||
"toolCallId": "call_get_1",
|
||||
"traceId": "trace_1",
|
||||
"result": {
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"title": "项目计划",
|
||||
"bodySummary": "第一段...",
|
||||
"pageOptions": { "wideLayout": true },
|
||||
"blocks": [{ "id": "heading_1", "type": "heading", "text": "章节一" }]
|
||||
},
|
||||
"audit": { "effect": "read", "commandId": null }
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"toolName": "mnote.page.get",
|
||||
"toolCallId": "call_get_1",
|
||||
"traceId": "trace_1",
|
||||
"error": { "code": "mnote_tool_permission_denied", "message": "无页面读取权限" }
|
||||
}
|
||||
```
|
||||
|
||||
### `mnote.page.save`
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "idempotencyKey", "dryRun", "content"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"content": { "type": "array" },
|
||||
"mode": { "type": "string", "enum": ["replace", "append"] },
|
||||
"idempotencyKey": { "type": "string" },
|
||||
"dryRun": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"toolName": "mnote.page.save",
|
||||
"toolCallId": "call_save_1",
|
||||
"traceId": "trace_1",
|
||||
"result": { "commandName": "page.body.save", "revision": 8 },
|
||||
"audit": { "effect": "write", "commandId": "page_body_save_trace_1" }
|
||||
}
|
||||
```
|
||||
|
||||
dryRun:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"toolName": "mnote.page.save",
|
||||
"toolCallId": "call_save_1",
|
||||
"traceId": "trace_1",
|
||||
"result": { "dryRun": true, "diff": [{ "op": "append", "blocks": 1 }] },
|
||||
"audit": { "effect": "dry_run", "commandId": null }
|
||||
}
|
||||
```
|
||||
|
||||
权限失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"toolName": "mnote.page.save",
|
||||
"toolCallId": "call_save_1",
|
||||
"traceId": "trace_1",
|
||||
"error": { "code": "mnote_tool_permission_denied", "message": "无页面写权限" }
|
||||
}
|
||||
```
|
||||
|
||||
业务失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"toolName": "mnote.page.save",
|
||||
"toolCallId": "call_save_1",
|
||||
"traceId": "trace_1",
|
||||
"error": { "code": "mnote_tool_conflict", "message": "页面版本冲突,需要刷新后重试" }
|
||||
}
|
||||
```
|
||||
|
||||
### `mnote.page.update_title`
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "title", "sessionId", "runId", "toolCallId", "traceId", "idempotencyKey", "dryRun"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"idempotencyKey": { "type": "string" },
|
||||
"dryRun": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功、dryRun、权限失败、业务失败响应形状同 `mnote.page.save`,成功 command 为 `page.head.updateTitle`。
|
||||
|
||||
### `mnote.page.update_options`
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "options", "sessionId", "runId", "toolCallId", "traceId", "idempotencyKey", "dryRun"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wideLayout": { "type": "boolean" },
|
||||
"smallText": { "type": "boolean" },
|
||||
"showToc": { "type": "boolean" },
|
||||
"protectEditing": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"idempotencyKey": { "type": "string" },
|
||||
"dryRun": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功、dryRun、权限失败、业务失败响应形状同 `mnote.page.save`,成功 command 为 `page.layout.updateOptions`。`runtimeSupport !== "wired"` 的字段必须返回 dryRun 警告或业务失败。
|
||||
|
||||
### `mnote.artifact.create_summary`
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "summary", "sessionId", "runId", "toolCallId", "traceId", "idempotencyKey", "dryRun"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"summary": { "type": "string" },
|
||||
"idempotencyKey": { "type": "string" },
|
||||
"dryRun": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功:创建或更新当前页面唯一 summary node,并创建或确认 reference edge。
|
||||
|
||||
失败示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"toolName": "mnote.artifact.create_summary",
|
||||
"toolCallId": "call_summary_1",
|
||||
"traceId": "trace_1",
|
||||
"error": { "code": "mnote_tool_idempotency_conflict", "message": "同一幂等键已用于不同 summary 内容" }
|
||||
}
|
||||
```
|
||||
|
||||
### `mnote.artifact.create_ai_note`
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "content", "sessionId", "runId", "toolCallId", "traceId", "idempotencyKey", "dryRun"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"content": { "type": "string" },
|
||||
"idempotencyKey": { "type": "string" },
|
||||
"dryRun": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功:每次创建独立 ai_note node,并创建 reference edge。
|
||||
|
||||
## 5. 权限、幂等与 dryRun
|
||||
|
||||
- [x] 权限校验点:登录、workspace member、页面读、页面写、artifact 写。
|
||||
- [x] 同一 `idempotencyKey` 重试不得重复创建 artifact 或重复写正文。
|
||||
- [x] `dryRun=true` 只能返回计划和 diff,不得写入。
|
||||
- [x] 所有失败响应不得泄露无权限页面标题、正文或 artifact 内容。
|
||||
- [x] 写工具返回的 `audit.commandId` 必须能追踪到 Rust runtime / kernel command。
|
||||
@@ -1,514 +0,0 @@
|
||||
# 7-60 Page AI 设置 IA 整理 v1
|
||||
|
||||
> 创建时间:2026-06-10
|
||||
>
|
||||
> 当前状态:`DONE`
|
||||
>
|
||||
> Owner:07-ai / Page AI / Settings IA / Runtime status
|
||||
>
|
||||
> 前置完成项:
|
||||
> - `design/07-ai/done/7-58-page-ai-reasonix-official-desktop-session-alignment-v1.md`
|
||||
> - `design/07-ai/done/7-59-page-ai-reasonix-desktop-hermes-web-control-surface-alignment-v1.md`
|
||||
>
|
||||
> 当前问题来源:7-59 后 Page AI 已有 runtime status、queue、jobs、logs、sessions dashboard,但 UI 仍沿用旧的 `Common / Hermes / Reasonix / Chat-only / 高级 / Runtime` 分层,导致“设置”和“运行态”混在一起。
|
||||
|
||||
## 1. 第一结论
|
||||
|
||||
Page AI 设置需要按 owner 分三层:
|
||||
|
||||
```text
|
||||
MNote = MNote 如何给 AI 上下文、权限、工具、会话历史和审计
|
||||
Reasonix = Reasonix ACP/native-live/queue/MCP/memory 的专属运行配置
|
||||
Hermes = Hermes gateway/profile/model/provider/session-load/replay/skills 的专属运行配置
|
||||
```
|
||||
|
||||
同时必须把“运行态状态”从“设置”里拆出去:
|
||||
|
||||
```text
|
||||
设置 = 可持久保存或影响后续 run 的配置
|
||||
状态 = 当前这一轮/当前 session 的 live snapshot
|
||||
```
|
||||
|
||||
当前截图里的混乱点不是单个控件坏了,而是抽象层级混用:
|
||||
|
||||
- 顶部显示 `Reasonix`,runtime strip 却显示 `hermes · new-session`,用户会误以为当前 agent 与 runtime 不一致。
|
||||
- `Common` 实际是 MNote 自身配置,但名字没有表达 owner。
|
||||
- `高级` 和 `Runtime` 既有设置项,又有只读状态项。
|
||||
- Runtime panel 里出现“打开 Hermes 设置”,但当前用户可能正在使用 Reasonix。
|
||||
- `Chat-only` 被放成一级设置页,但它本质是 MNote/API Chat/Hermes provider 的一种 runtime/profile 类型,不应和 Reasonix/Hermes 并列为 provider owner。
|
||||
|
||||
## 2. 新信息架构
|
||||
|
||||
### 2.1 Page AI 顶层导航
|
||||
|
||||
Page AI 抽屉保留五个一级工作区:
|
||||
|
||||
| 一级区 | 含义 | 当前入口 |
|
||||
| --- | --- | --- |
|
||||
| 聊天 | 当前页对话、工具卡、引用、权限决策 | 默认页 |
|
||||
| 状态 | 当前 runtime/session/run live snapshot | 由 runtime strip 或状态图标打开 |
|
||||
| 设置 | MNote / Reasonix / Hermes 三层配置 | 齿轮 |
|
||||
| 能力 | 当前 runtime 可用 tools/skills/memory 清单与开关 | sparkle/能力图标 |
|
||||
| 历史 | MNote session dashboard | 搜索/历史图标 |
|
||||
|
||||
原则:
|
||||
|
||||
- “状态”不承载长期配置。
|
||||
- “设置”不展示 transient logs/job/active-run。
|
||||
- “能力”可以按 runtime 过滤,但 capability owner 仍归 MNote / Reasonix / Hermes。
|
||||
- “历史”展示 MNote session asset,不等于 Hermes DB 或 Reasonix native session。
|
||||
|
||||
### 2.2 设置页三层结构
|
||||
|
||||
设置页只保留三个主 tab:
|
||||
|
||||
```text
|
||||
MNote | Reasonix | Hermes
|
||||
```
|
||||
|
||||
不再显示:
|
||||
|
||||
```text
|
||||
Common | Chat-only | 高级
|
||||
```
|
||||
|
||||
旧 tab 映射:
|
||||
|
||||
| 旧 tab / 控件 | 新归属 |
|
||||
| --- | --- |
|
||||
| `Common` | `MNote` |
|
||||
| `Chat-only` | `MNote > API Chat / Chat-only profile` |
|
||||
| `高级` 里的 context scope | `MNote > 默认上下文` |
|
||||
| `高级` 里的 runtime status | `状态` |
|
||||
| `Runtime` panel | `状态` |
|
||||
| `打开 Hermes 设置` | `Hermes` tab 内 |
|
||||
| Reasonix memory / skill filter | `Reasonix` |
|
||||
| Hermes profile / builtin skill filter | `Hermes` |
|
||||
|
||||
## 3. MNote 设置
|
||||
|
||||
MNote tab 只放 MNote 自己拥有的控制面:
|
||||
|
||||
### 3.1 授权区域
|
||||
|
||||
来源:SQLite `directory_grants` / current workspace grants。
|
||||
|
||||
展示:
|
||||
|
||||
- 当前 workspace/root。
|
||||
- allowed roots 数量。
|
||||
- read/write 权限。
|
||||
- 授权来源:manual / smoke / share / system。
|
||||
- 权限缺失时的修复入口。
|
||||
|
||||
不展示:
|
||||
|
||||
- Hermes API key。
|
||||
- Reasonix ACP binary。
|
||||
- providerConversationId。
|
||||
|
||||
### 3.2 默认上下文
|
||||
|
||||
来源:Page AI context refs。
|
||||
|
||||
控件:
|
||||
|
||||
- 当前页。
|
||||
- 打开资源。
|
||||
- 选中文本。
|
||||
- 页面子树。
|
||||
- 附件/资源 tab。
|
||||
|
||||
保存语义:
|
||||
|
||||
- 这是 MNote 的默认 run envelope 规则。
|
||||
- 不直接改 Reasonix/Hermes 的 native memory。
|
||||
|
||||
### 3.3 MNote 工具
|
||||
|
||||
范围:
|
||||
|
||||
- LightRAG。
|
||||
- citation/open reference。
|
||||
- page context。
|
||||
- local file audit。
|
||||
- changed files receipt。
|
||||
- MNote MCP/tool facade。
|
||||
|
||||
展示:
|
||||
|
||||
- enabled/disabled。
|
||||
- tool count。
|
||||
- last failed tool。
|
||||
- capability source。
|
||||
|
||||
### 3.4 会话与审计
|
||||
|
||||
范围:
|
||||
|
||||
- MNote history。
|
||||
- export `json/markdown/jsonl`。
|
||||
- delete MNote history。
|
||||
- changed files audit。
|
||||
- permission pending queue。
|
||||
|
||||
删除文案必须固定:
|
||||
|
||||
```text
|
||||
删除只影响 MNote 历史,不删除外部 Hermes/provider/Reasonix session。
|
||||
```
|
||||
|
||||
外部删除只能出现在 Hermes tab 的危险区,并需要二次确认。
|
||||
|
||||
## 4. Reasonix 设置
|
||||
|
||||
Reasonix tab 只放 Reasonix 专属配置与能力:
|
||||
|
||||
### 4.1 Runtime
|
||||
|
||||
展示:
|
||||
|
||||
- ACP runtime name:`reasonix`。
|
||||
- binary/wrapper:`scripts/reasonix-acp-wrapper.mjs` 或配置来源。
|
||||
- workspace key 规则。
|
||||
- native-live 支持状态。
|
||||
- supports session/load:false。
|
||||
- supports replay:false。
|
||||
|
||||
### 4.2 Native-live session
|
||||
|
||||
只展示配置和策略,不展示当前 active-run 日志。当前 live binding 细节在“状态”页。
|
||||
|
||||
设置项:
|
||||
|
||||
- native-live 开关或策略说明。
|
||||
- cold resume fallback 是否启用。
|
||||
- short reply without context 的提示策略。
|
||||
|
||||
### 4.3 Queue
|
||||
|
||||
设置项:
|
||||
|
||||
- busy send 行为:enqueue。
|
||||
- cancel queued item 策略。
|
||||
- queue max length。
|
||||
- queue timeout。
|
||||
|
||||
当前 queued count / preview 属于“状态”页。
|
||||
|
||||
### 4.4 MCP / tools / memory
|
||||
|
||||
范围:
|
||||
|
||||
- Reasonix MCP specs。
|
||||
- Reasonix tool count。
|
||||
- Reasonix memory 开关。
|
||||
- Reasonix skill filter。
|
||||
|
||||
不放:
|
||||
|
||||
- Hermes profile。
|
||||
- MNote LightRAG provider 设置。
|
||||
- Chat-only provider 配置。
|
||||
|
||||
## 5. Hermes 设置
|
||||
|
||||
Hermes tab 只放 Hermes 相关配置:
|
||||
|
||||
### 5.1 Gateway
|
||||
|
||||
展示:
|
||||
|
||||
- `MNOTE_WEB_HERMES_UPSTREAM_URL`。
|
||||
- gateway health。
|
||||
- config source。
|
||||
- logs link / settings link。
|
||||
|
||||
当前“未配置 Hermes 设置入口”错误只应出现在 Hermes tab,不应污染 Reasonix 设置或状态。
|
||||
|
||||
### 5.2 Profile / model / provider
|
||||
|
||||
展示:
|
||||
|
||||
- profile id。
|
||||
- resolved model。
|
||||
- provider kind。
|
||||
- credential status。
|
||||
- config source。
|
||||
- API key masked/missing/configured。
|
||||
|
||||
### 5.3 ACP session load / replay
|
||||
|
||||
展示:
|
||||
|
||||
- supports session/load:true。
|
||||
- stored `acpSessionId`。
|
||||
- load status:loaded / load-failed-new。
|
||||
- replaySeen / replaySeq / replayMessageCount。
|
||||
|
||||
注意:
|
||||
|
||||
- replay 是 history restore/evidence,不进入 assistant final。
|
||||
- load failed 只影响 runtime continuity,不删除 MNote history。
|
||||
|
||||
### 5.4 Skills / memory
|
||||
|
||||
范围:
|
||||
|
||||
- Hermes skills。
|
||||
- hide builtin skills。
|
||||
- Hermes memory sections:soul/user/memory。
|
||||
- profile-local skills。
|
||||
|
||||
### 5.5 外部删除危险区
|
||||
|
||||
默认隐藏在 Hermes tab 底部。
|
||||
|
||||
动作:
|
||||
|
||||
- 删除 MNote history:跳回 MNote 会话与审计。
|
||||
- 删除外部 Hermes/provider 会话:必须二次确认,且需要显式参数 `deleteExternalProviderSession=1`。
|
||||
|
||||
## 6. 状态页
|
||||
|
||||
状态页是 7-59 runtime panel 的重排版,不是设置页。
|
||||
|
||||
入口:
|
||||
|
||||
- 点击顶部 runtime strip。
|
||||
- 或 header 增加状态图标。
|
||||
|
||||
内容:
|
||||
|
||||
| 区块 | 字段 |
|
||||
| --- | --- |
|
||||
| Current runtime | runtime / mode / profile / model |
|
||||
| Session | mnoteSessionId / acpSessionId / providerConversationId |
|
||||
| Run | active run / status / queue |
|
||||
| Context | allowed roots / context scope |
|
||||
| MCP & tools | mcp status / tool count / failed |
|
||||
| Jobs | running/completed/failed / stop all |
|
||||
| Logs tail | ACP stderr / tool failed / MCP failed |
|
||||
| Usage | last run / session total / cache/cost if available |
|
||||
|
||||
状态页允许两个操作:
|
||||
|
||||
| 操作 | 语义 |
|
||||
| --- | --- |
|
||||
| Stop turn | 只停止当前 run,不删除 session,不关闭 live binding |
|
||||
| Reset current runtime | 关闭当前 runtime binding/manager,不删除 MNote history,不删除外部 provider |
|
||||
|
||||
状态页不允许:
|
||||
|
||||
- 删除 session history。
|
||||
- 删除外部 provider/Hermes 会话。
|
||||
- 改 Hermes API key。
|
||||
- 改 Reasonix memory。
|
||||
|
||||
## 7. UI 文案规则
|
||||
|
||||
### 7.1 Runtime badge
|
||||
|
||||
顶部 badge 应按 owner 明确表达:
|
||||
|
||||
```text
|
||||
Reasonix · native-live
|
||||
Reasonix · cold-resumed
|
||||
Hermes · loaded
|
||||
Hermes · replay-seen
|
||||
API Chat · server-transcript
|
||||
Chat-only · provider-conversation
|
||||
```
|
||||
|
||||
禁止出现“header 是 Reasonix,但 badge 是 hermes new-session”的短暂串线。切换 agent/profile 时必须先清空旧 runtime status,再加载新 status。
|
||||
|
||||
### 7.2 设置 tab 文案
|
||||
|
||||
使用:
|
||||
|
||||
```text
|
||||
MNote
|
||||
Reasonix
|
||||
Hermes
|
||||
```
|
||||
|
||||
不用:
|
||||
|
||||
```text
|
||||
Common
|
||||
高级
|
||||
Chat-only
|
||||
Runtime
|
||||
```
|
||||
|
||||
### 7.3 危险动作文案
|
||||
|
||||
Stop turn:
|
||||
|
||||
```text
|
||||
停止当前输出,不关闭 runtime,不删除历史。
|
||||
```
|
||||
|
||||
Reset runtime:
|
||||
|
||||
```text
|
||||
重置当前 runtime。MNote 历史保留,外部 provider 会话不删除。
|
||||
```
|
||||
|
||||
Delete session:
|
||||
|
||||
```text
|
||||
删除 MNote 历史。外部 Hermes/provider 会话不会删除。
|
||||
```
|
||||
|
||||
External delete:
|
||||
|
||||
```text
|
||||
这会请求删除外部 provider/Hermes 会话,可能不可恢复。需要二次确认。
|
||||
```
|
||||
|
||||
## 8. 实施顺序
|
||||
|
||||
### Phase A:IA rename / no behavior change
|
||||
|
||||
- [x] 将设置主 tab 收口为 `MNote / Reasonix / Hermes`。
|
||||
- [x] `Common` 改名 `MNote`。
|
||||
- [x] 移除一级 `Chat-only` tab;Chat-only 配置迁入 `MNote > API Chat / Chat-only profile`。
|
||||
- [x] `高级`/`Runtime` 不再作为设置 tab 名称。
|
||||
- [x] 保持现有 DOM action 与后端合同不变,先只做布局与文案重排。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js
|
||||
node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
```
|
||||
|
||||
### Phase B:状态页从设置页拆出
|
||||
|
||||
- [x] 新增或重排 `status` panel。
|
||||
- [x] runtime strip 点击打开状态页。
|
||||
- [x] runtime status、queue、jobs、logs、usage 移到状态页。
|
||||
- [x] MNote/Reasonix/Hermes 设置页不再显示 transient active-run logs。
|
||||
- [x] Stop turn / Reset current runtime 只在状态页显示。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
node scripts/task558-page-ai-reasonix-live-session-context-smoke.js
|
||||
```
|
||||
|
||||
### Phase C:MNote 设置 owner 收口
|
||||
|
||||
- [x] 授权区域、默认上下文、MNote 工具、会话与审计进入 MNote tab。
|
||||
- [x] Chat-only/API Chat profile 的入口进入 MNote tab。
|
||||
- [x] delete session 文案固定为只删 MNote history。
|
||||
- [x] permission pending queue 刷新恢复仍可见。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node scripts/task561-page-ai-session-dashboard-smoke.js
|
||||
cargo test -p mnote-web api_chat -- --nocapture
|
||||
```
|
||||
|
||||
### Phase D:Reasonix / Hermes 设置 owner 收口
|
||||
|
||||
- [x] Reasonix tab 只展示 Reasonix runtime/native-live/queue/MCP/memory。
|
||||
- [x] Hermes tab 只展示 Hermes gateway/profile/model/provider/load/replay/skills。
|
||||
- [x] Hermes gateway 未配置错误只出现在 Hermes tab。
|
||||
- [x] Reasonix 页面不出现 Hermes upstream/API key 错误。
|
||||
- [x] Hermes 页面不出现 Reasonix native-live queue 当前状态。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node scripts/task558-page-ai-reasonix-live-session-context-smoke.js
|
||||
node scripts/task560-page-ai-hermes-load-replay-smoke.js
|
||||
```
|
||||
|
||||
### Phase E:截图与移动端收口
|
||||
|
||||
- [x] 桌面宽度下设置 tab 不换行、不挤压。
|
||||
- [x] 移动宽度下三 tab 可点击,状态页内容可滚动。
|
||||
- [x] Runtime strip 不遮挡 header action。
|
||||
- [x] history actions 在移动宽度仍可点击。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
node scripts/task561-page-ai-session-dashboard-smoke.js
|
||||
```
|
||||
|
||||
需要补截图断言:
|
||||
|
||||
- `MNote / Reasonix / Hermes` 三 tab 可见。
|
||||
- 状态页打开后可见 `Current runtime / Session / Queue / Logs tail`。
|
||||
- Reasonix 设置页无 Hermes API key 错误。
|
||||
- Hermes 设置页显示 gateway/profile/model/provider。
|
||||
|
||||
## 9. 不做事项
|
||||
|
||||
- 不改变 7-59 的后端 runtime/session API 合同。
|
||||
- 不新增第二套 runtime truth。
|
||||
- 不把 Hermes Web UI 作为完整 dashboard 搬进 MNote。
|
||||
- 不把 Reasonix Desktop settings 全量复制进 MNote。
|
||||
- 不自动删除外部 Hermes/provider/Reasonix session。
|
||||
- 不把 runtime logs/job/tool status 拼进 assistant final。
|
||||
|
||||
## 10. 完成条件
|
||||
|
||||
- 设置页只剩 `MNote / Reasonix / Hermes` 三个 owner tab。
|
||||
- Runtime status 从设置页拆出为独立状态页或 runtime strip popover。
|
||||
- MNote tab 不出现 Hermes API key / Reasonix binary 这类外部 runtime 配置。
|
||||
- Reasonix tab 不出现 Hermes upstream 未配置错误。
|
||||
- Hermes tab 不出现 Reasonix native-live 当前 queue。
|
||||
- Stop turn / Reset runtime / Delete session 三类动作仍语义分离。
|
||||
- Reasonix queued send、Hermes load/replay、API Chat、ChatOnly 现有 smoke 全部通过。
|
||||
- 移动截图中 tab、按钮、状态卡不重叠。
|
||||
|
||||
## 11. 实施记录
|
||||
|
||||
### 2026-06-10 Phase A-E
|
||||
|
||||
已完成:
|
||||
|
||||
- 设置页 tab 收口为 `MNote / Reasonix / Hermes`。
|
||||
- 移除一级 `Common / Chat-only / 高级 / Runtime` 设置 tab。
|
||||
- runtime strip 改为可点击入口,点击打开独立 `status` panel。
|
||||
- `status` panel 展示 `Current runtime / Session / Queue / Jobs / Logs tail / Usage`,并保留 `Stop turn` / `Reset current runtime`。
|
||||
- MNote tab 展示授权区域、默认上下文、会话与审计、API Chat / Chat-only profile、MNote 工具。
|
||||
- Reasonix tab 只保留 ACP runtime、Reasonix descriptor、Reasonix memory 等专属内容。
|
||||
- Hermes tab 展示 Hermes gateway、profile/model/provider、Hermes skills/memory,并承载 Hermes gateway 未配置提示。
|
||||
- 扩展浏览器 smoke,增加设置 IA、状态页、移动宽度 tab 可点性、Hermes/Reasonix owner 边界断言。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-profile-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js
|
||||
node --check rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js
|
||||
node --check scripts/task558-page-ai-reasonix-live-session-context-smoke.js
|
||||
node --check scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
node --check scripts/task561-page-ai-session-dashboard-smoke.js
|
||||
cargo test -p mnote-web hermes_client -- --nocapture
|
||||
node scripts/task558-page-ai-reasonix-live-session-context-smoke.js
|
||||
node scripts/task559-page-ai-terminal-status-reconciliation-smoke.js
|
||||
node scripts/task560-page-ai-hermes-load-replay-smoke.js
|
||||
node scripts/task561-page-ai-session-dashboard-smoke.js
|
||||
```
|
||||
|
||||
浏览器截图证据:
|
||||
|
||||
- `tmp/task559-terminal-status-fpUZi3/task559-runtime-panel.png`:状态页展示 Current runtime、Queue、Jobs、Logs tail、Usage。
|
||||
- `tmp/task559-terminal-status-fpUZi3/task559-settings-mobile.png`:移动宽度下 MNote / Reasonix / Hermes 三 tab 可点击且不重叠。
|
||||
- `tmp/task561-session-dashboard-ttJKyq/task561-settings-ia.png`:Hermes tab 展示 Hermes gateway/profile/skills,不显示 Reasonix queue。
|
||||
|
||||
归档说明:
|
||||
|
||||
- 本稿只重排 Page AI 设置 IA 与状态页,不改变 7-59 后端 runtime/session API 合同。
|
||||
- 外部 Hermes/provider 删除仍需显式二次确认;本稿没有新增默认外部删除入口。
|
||||
@@ -1,398 +0,0 @@
|
||||
# 7-7 [done] 页面 AI Mini Hermes 控制面设计与执行 checklist v1
|
||||
|
||||
> 更新时间:2026-05-14
|
||||
>
|
||||
> 当前状态:`DONE`。当前实现已经可以通过 Hermes 返回回复,并已完成 `7-4` 的 session/run/tool/writeback/audit 主链;本稿承接的 Mini Hermes 控制面已完成:`agent/profile` 选择、`SOUL.md` / `USER.md` / `MEMORY.md` 轻编辑、skill 启停、Hermes settings 深链、run stop、context scope、mnote tool 可用性和最近 tool call / trace / audit 摘要均已有代码与 smoke 验证。retry、session rename/delete、profile create/rename、skill 上传继续作为 P2,不在本稿 done gate 内。
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 方向判断
|
||||
|
||||
当前截图和实现状态说明:页面 AI 已经能回复,但它还更像“能发消息的 Hermes run 面板”,缺少用户自然期待的 Hermes 运行态控制与设置入口。
|
||||
|
||||
下一阶段不要把 mnote 做成第二个 Hermes 管理台,也不要把 Hermes 的 Settings / Profiles / Usage / Logs 全部搬进页面抽屉。正确方向是:
|
||||
|
||||
> **页面 AI 面板成为 Mini Hermes control surface:只管理和当前 mnote 页面会话直接相关的 session、run、agent/profile、model、skill、tool、context scope 与错误恢复;所有全局配置真相仍归 Hermes。**
|
||||
|
||||
边界继续保持:
|
||||
|
||||
- Hermes 持有 session/message/tool event/usage/model/profile 真相。
|
||||
- mnote 只持有页面、树、正文、artifact、edge、Page Aggregate 和 audit 真相。
|
||||
- 页面 AI 面板只调用 Hermes,不保存聊天历史,不维护 provider/API key,不重建 plugin registry。
|
||||
- 页面 AI 面板可以选择 agent/profile、启停 skill、编辑当前 agent 的轻量人格文件;保存和生效都必须走 Hermes API。
|
||||
- provider/API key、账号登录、全局 marketplace、profile 导入导出等深层设置跳转或 deep link 到 Hermes 自己的设置页。
|
||||
|
||||
用户提供的参考图只作为能力边界参考,不要求照搬 UI:
|
||||
|
||||
- `/mnt/Data1T/mnote/tmp/img_v3_0211m_b7390ad9-a45b-4d94-8126-b7c17f029feg.png`:AI 人格页,包含 `SOUL.md`、`IDENTITY.md`、`USER.md` 类 profile 文件编辑和保存。
|
||||
- `/mnt/Data1T/mnote/tmp/img_v3_0211m_0d6e7a60-8c21-4fd1-8cdf-d548dab7532g.png`:技能页,包含搜索、上传、自定义技能额度、单个技能启停和详情查看。
|
||||
|
||||
---
|
||||
|
||||
## 2. Hermes Web UI 参考索引
|
||||
|
||||
参考根目录固定为:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18
|
||||
```
|
||||
|
||||
只参考下表列出的具体位置。目标是复用 Hermes 的语义与行为边界,仍然用 Leptos 实现 mnote 页面内面板。
|
||||
|
||||
| mnote 下一步任务 | 参考文件 | 搜索点 / 具体位置 | 参考目的 | 不采用内容 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| run 发起、恢复和事件语义 | `packages/client/src/api/hermes/chat.ts` | `StartRunRequest`、`RunEvent`、`registerSessionHandlers`、`resumeSession`、`startRunViaSocket` | 对齐 run 创建、session room 恢复、事件类型和浏览器端分发语义 | 不直接复用 Vue/TS client,不让浏览器绕过 mnote-web proxy |
|
||||
| 前端消息归并 | `packages/client/src/stores/hermes/chat.ts` | `mapHermesMessages`、`case 'tool.started'`、`case 'tool.completed'`、`refreshActiveSession`、`switchSession` | 对齐 message/tool event 在前端归并成消息列表的规则 | 不把归并结果保存到 mnote local/session storage 作为真相 |
|
||||
| 面板分层 | `packages/client/src/components/hermes/chat/ChatPanel.vue` | `activeSessionTitle`、`showSessions`、`handleNewChat`、`MessageList`、`ChatInput`、`SessionListItem` | 对齐 session list / message list / input 的最小层次 | 不复制 Hermes 全屏布局、Vue 组件结构或 Naive UI |
|
||||
| tool 消息展示 | `packages/client/src/components/hermes/chat/MessageItem.vue` | `message.role === 'tool'`、`formatToolPayload`、`tool-preview`、`tool-details`、`tool-error-badge` | 对齐 tool 结果默认折叠、摘要、错误 badge、详情展开 | 不把 tool result 当正文写入,不扩大为审计详情管理台 |
|
||||
| session 列表项 | `packages/client/src/components/hermes/chat/SessionListItem.vue` | `session-item-title`、`session.title`、`session-item-model` | 对齐 session 标题、模型和时间的展示语义 | 不把 Hermes session title 同步为 mnote page title |
|
||||
| model 选择展示 | `packages/client/src/components/layout/ModelSelector.vue` | `selectedDisplayName`、`handleSelect`、`model-name`、`model-item` | 对齐模型显示名称、选择行为和禁用态 | 不在 mnote 保存 model/provider/API key 真相 |
|
||||
| agent/profile 选择 | `packages/client/src/components/layout/ProfileSelector.vue`、`packages/client/src/api/hermes/profiles.ts`、`packages/client/src/stores/hermes/profiles.ts` | `fetchProfiles`、`switchProfile`、`activeProfileName`、`handleChange` | 对齐 Hermes profile 作为 agent 的选择语义;mnote 面板选择的是 profile,不是自建 agent 表 | 不要求页面刷新;不直接复制 Hermes sidebar profile selector |
|
||||
| profile 管理边界 | `packages/client/src/views/hermes/ProfilesView.vue`、`packages/client/src/components/hermes/profiles/ProfileCard.vue`、`ProfileCreateModal.vue`、`ProfileRenameModal.vue`、`packages/server/src/routes/hermes/profiles.ts` | `/api/hermes/profiles`、`/api/hermes/profiles/active`、`rename`、`create`、`delete` | 只参考 profile list/detail/rename 的字段和错误边界 | 第一阶段不做 import/export/delete;不把 mnote 做成完整 ProfilesView |
|
||||
| agent 人格文件编辑 | `packages/client/src/views/hermes/MemoryView.vue`、`packages/client/src/api/hermes/skills.ts`、`packages/server/src/routes/hermes/memory.ts`、`packages/server/src/controllers/hermes/memory.ts` | `fetchMemory`、`saveMemory`、`memory/user/soul`、`SOUL.md`、`USER.md`、`MEMORY.md` | 对齐 Hermes 如何读取和保存人格/记忆文件;mnote 可提供当前 agent 的轻量编辑器 | 不在 mnote 直接读写 profile 目录;不绕过 Hermes API 写文件 |
|
||||
| run 生命周期与持久化 | `packages/server/src/services/hermes/chat-run-socket.ts` | `handleRun`、`emit`、`applyResponseStreamEvent`、`flushResponseRunToDb`、`markCompleted` | 对齐 queued/running/tool_calling/completed/failed 与 flush 到 Hermes DB 的时机 | 不在 mnote 里重写 Hermes 编排器 |
|
||||
| session 存储真相 | `packages/server/src/db/hermes/session-store.ts` | `HermesSessionRow`、`HermesMessageRow`、`getSessionDetail`、`addMessage`、`renameSession` | 明确 session/message/tool call 真相字段来自 Hermes | 不在 mnote 建第二份 chat/session 表 |
|
||||
| plugin/tool 发现 | `packages/server/src/services/hermes/plugins.ts` | `PluginManager`、`providesTools`、`listHermesPlugins`、`requiresEnv` | 对齐 mnote tools 作为 Hermes plugin/tool registry 的一部分被发现 | 不把 mnote-web 私有 route 当成最终 plugin registry |
|
||||
| skill/plugin API 外观 | `packages/client/src/api/hermes/plugins.ts`、`packages/client/src/api/hermes/skills.ts`、`packages/client/src/views/hermes/SkillsView.vue`、`packages/client/src/components/hermes/skills/SkillList.vue`、`SkillDetail.vue`、`packages/server/src/routes/hermes/skills.ts` | `fetchSkills`、`toggleSkill`、`pinSkillApi`、`fetchSkillContent`、`fetchSkillFiles`、`/api/hermes/skills/toggle` | 对齐 skill 搜索、分类、启停、详情和文件查看;页面 AI 需要能启停技能 | 不实现完整 marketplace、批量安装、复杂上传发布流程 |
|
||||
| 不进入 mnote 面板第一阶段 | `packages/client/src/views/hermes/SettingsView.vue`、`ProfilesView.vue`、`UsageView.vue`、`LogsView.vue`、`JobsView.vue`、`FilesView.vue`、`ChannelsView.vue`、`TerminalView.vue`、`GroupChatView.vue` | 页面级管理视图入口 | 用来明确边界:这些属于 Hermes 管理台 | 不搬进 mnote 页面 AI 抽屉 |
|
||||
|
||||
快速定位命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18
|
||||
rg -n "StartRunRequest|RunEvent|registerSessionHandlers|resumeSession|startRunViaSocket|mapHermesMessages|tool\\.started|tool\\.completed|activeSessionTitle|formatToolPayload|selectedDisplayName|fetchProfiles|switchProfile|fetchMemory|saveMemory|toggleSkill|SkillList|handleRun|flushResponseRunToDb|PluginManager|providesTools" packages/client/src packages/server/src
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品边界
|
||||
|
||||
### 3.1 必须进入 mnote 页面 AI 面板
|
||||
|
||||
- 当前 Hermes session 标题、session id、模型名、profile 名和恢复状态。
|
||||
- agent/profile 选择:用户能在页面 AI 面板里选择当前使用的 Hermes profile;profile 等同本设计中的 agent。
|
||||
- agent/profile 轻编辑:用户能查看和修改当前 agent 的 `SOUL.md`、`USER.md`、`MEMORY.md`,若 Hermes 后续暴露 `IDENTITY.md` 也按同一文件编辑模型接入。
|
||||
- skill 启停:用户能搜索 Hermes skills,查看简要说明,并启用/停用单个 skill。
|
||||
- 当前 run 状态:`idle`、`queued`、`running`、`tool_calling`、`completed`、`failed`、`aborted`。
|
||||
- 当前页面上下文范围:当前页、当前选区、当前块、全文、页面设置。
|
||||
- 当前 mnote tools 可用性:`mnote.page.get`、`mnote.page.save`、`mnote.page.update_title`、`mnote.page.update_options`、`mnote.artifact.create_summary`、`mnote.artifact.create_ai_note`。
|
||||
- 最近 tool call 摘要、参数摘要、结果摘要、失败原因和 trace/audit id。
|
||||
- 停止、重试、继续、重新读取当前页上下文的最小操作。
|
||||
- 跳转 Hermes 设置的入口,说明缺失项应在 Hermes 中配置。
|
||||
|
||||
### 3.2 不进入 mnote 页面 AI 面板
|
||||
|
||||
- provider/API key 密钥管理。
|
||||
- 全局 profile 导入导出、删除、批量迁移、凭据清理等危险操作。
|
||||
- Hermes plugin marketplace / skill marketplace 的安装、发布、版本管理和批量上传。
|
||||
- usage 报表、日志中心、任务中心、文件中心、频道、终端、群聊。
|
||||
- Hermes session 数据库迁移、导入导出、conversation 管理台。
|
||||
- 独立 mnote 聊天历史存储。
|
||||
|
||||
### 3.3 可选但不阻塞第一阶段
|
||||
|
||||
- 当前页面最近 sessions 搜索。
|
||||
- session 重命名与删除。
|
||||
- profile rename / create。
|
||||
- model 下拉切换。
|
||||
- skill pin、skill 文件详情查看、单个本地 skill 上传。
|
||||
- tool event JSON 详情复制。
|
||||
- session deep link 到 Hermes 管理台。
|
||||
|
||||
---
|
||||
|
||||
## 4. 目标信息架构
|
||||
|
||||
页面 AI 抽屉保持 Wolai 对齐后的壳层,内部按五层组织:
|
||||
|
||||
1. 顶部状态条:session 标题、agent/profile、模型、连接状态、设置跳转。
|
||||
2. 消息区:Hermes message list、streaming 回复、tool event 折叠项。
|
||||
3. Agent 设置区:profile 选择、SOUL/USER/MEMORY 轻编辑、skill 搜索与启停。
|
||||
4. 上下文与工具条:当前 context scope、可用 mnote tools、最近 audit/trace。
|
||||
5. 输入区:prompt 输入、发送、停止、重试、继续。
|
||||
|
||||
验收时不要只看 DOM,应以截图和交互为准:
|
||||
|
||||
- 顶部状态在窄屏不挤压输入区。
|
||||
- tool event 折叠项不会撑破抽屉宽度。
|
||||
- 失败态能看到可执行动作,不只是一段错误文本。
|
||||
- 设置入口不会暗示 mnote 自己持有 API key。
|
||||
- agent/profile 选择和 skill 启停必须明确显示“由 Hermes 保存 / 全局生效或当前 profile 生效”的作用域,避免用户误以为只是当前页面临时开关。
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据与 API 边界
|
||||
|
||||
### 5.1 mnote-web proxy 需要补齐的读接口
|
||||
|
||||
- `GET /api/hermes/client/session/current?documentId=...`
|
||||
- `GET /api/hermes/client/sessions?documentId=...&limit=...`
|
||||
- `GET /api/hermes/client/session/:sessionId`
|
||||
- `GET /api/hermes/client/models`
|
||||
- `GET /api/hermes/client/profiles`
|
||||
- `GET /api/hermes/client/profiles/:profileName`
|
||||
- `GET /api/hermes/client/profile-memory?profile=...`
|
||||
- `GET /api/hermes/client/skills?profile=...&query=...`
|
||||
- `GET /api/hermes/client/tools?scope=mnote`
|
||||
- `GET /api/hermes/client/runtime/status`
|
||||
|
||||
这些接口只透传或规整 Hermes 状态,不在 mnote 保存真相。
|
||||
|
||||
### 5.2 mnote-web proxy 需要补齐的操作接口
|
||||
|
||||
- `POST /api/hermes/client/run`
|
||||
- `POST /api/hermes/client/run/:runId/stop`
|
||||
- `POST /api/hermes/client/run/:runId/retry`
|
||||
- `POST /api/hermes/client/session/:sessionId/resume`
|
||||
- `POST /api/hermes/client/session/:sessionId/rename`
|
||||
- `DELETE /api/hermes/client/session/:sessionId`
|
||||
- `PUT /api/hermes/client/profiles/active`
|
||||
- `POST /api/hermes/client/profile-memory`
|
||||
- `PUT /api/hermes/client/skills/toggle`
|
||||
|
||||
第一阶段必须完成 `run/stop/resume`、profile 选择、skill 单项启停、`SOUL.md` / `USER.md` / `MEMORY.md` 轻编辑。`retry/rename/delete`、profile create/rename、skill 上传可以排到 P2,但 UI 文案不能让用户误以为已经支持。
|
||||
|
||||
### 5.3 Agent / profile 选择语义
|
||||
|
||||
在本稿中,“agent” 等同 Hermes profile。页面 AI 不创建 mnote 私有 agent 表。
|
||||
|
||||
选择 profile 时必须处理 session 归属:
|
||||
|
||||
- 若当前没有 Hermes session,选择 profile 后创建新 session,session 的 `profile` 字段由 Hermes 持有。
|
||||
- 若当前 session 已绑定 profile,用户切换 profile 时默认创建一个新的当前页 session;不要原地篡改旧 session 的 profile。
|
||||
- 页面可以记住“当前页面最后使用的 session id / profile name”作为 UI 恢复线索,但聊天真相仍以 Hermes session detail 为准。
|
||||
- profile 切换不自动改页面正文,不自动触发写入工具。
|
||||
|
||||
### 5.4 Agent 人格文件编辑语义
|
||||
|
||||
页面 AI 面板允许轻编辑当前 profile 的人格文件:
|
||||
|
||||
- `SOUL.md`:核心人格、价值观、行为边界。
|
||||
- `USER.md`:用户偏好与长期上下文。
|
||||
- `MEMORY.md`:Hermes memory 笔记。
|
||||
- `IDENTITY.md`:若 Hermes API 暴露,则作为同类 profile file 接入;未暴露时 UI 不显示假入口。
|
||||
|
||||
保存规则:
|
||||
|
||||
- 通过 Hermes API 保存,不由 mnote 直接读写 `.hermes` 文件。
|
||||
- 保存前显示本次修改作用域:当前 Hermes profile。
|
||||
- 保存后只影响后续 run;已完成的 Hermes session/message 不回写。
|
||||
- 保存失败必须显示 Hermes 返回的原因,不能静默保留本地草稿并显示成功。
|
||||
|
||||
### 5.5 Skill 启停语义
|
||||
|
||||
页面 AI 面板允许搜索、查看和启停 skill。
|
||||
|
||||
第一阶段只要求:
|
||||
|
||||
- 列表来自 Hermes `/api/hermes/skills` 或 mnote-web 同源 proxy。
|
||||
- 单个 skill 的 `enabled` 状态由 Hermes 持有。
|
||||
- toggle 调用 Hermes `/api/hermes/skills/toggle` 语义。
|
||||
- UI 明确展示 skill 来源:builtin / hub / local / modified。
|
||||
- 禁用 skill 影响后续 run;正在运行的 run 不强制中断,除非 Hermes upstream 本身返回中断。
|
||||
|
||||
不在第一阶段做:
|
||||
|
||||
- marketplace 安装/发布。
|
||||
- 批量启停。
|
||||
- 删除 builtin skill。
|
||||
- 上传并发布完整 skill 包。单个本地 skill 上传可排 P2,且仍走 Hermes。
|
||||
|
||||
### 5.6 mnote tool manifest
|
||||
|
||||
页面 AI 面板展示 tool 可用性时应优先从 Hermes plugin/tool registry 来,而不是硬编码前端列表。允许 mnote-web 在过渡期提供同源聚合:
|
||||
|
||||
- tool name
|
||||
- description
|
||||
- read/write 分类
|
||||
- required scope
|
||||
- permission 状态
|
||||
- last call summary
|
||||
- last audit id
|
||||
- unavailable reason
|
||||
|
||||
---
|
||||
|
||||
## 6. 顺序执行 checklist
|
||||
|
||||
### A. 设计与现状审计
|
||||
|
||||
- [ ] A1. 打开当前 `3000` 页面 AI 抽屉,记录“能回复但缺设置/运行态控制”的截图到 `tmp/`。
|
||||
- 验收标准:截图能看到回复链路、顶部或设置区域缺失点、当前 session/run 状态展示缺口。
|
||||
- [ ] A2. 用 `rg` 确认活跃设计稿中页面 AI 主线已经指向 Hermes session/run/events。
|
||||
- 验收标准:`design/07-ai/process` 只剩本 `7-7` 作为 AI 活跃稿;`7-2` 到 `7-6` 位于 `done/`。
|
||||
- [ ] A3. 对照 Hermes Web UI 参考索引打开精确文件,不整目录通读。
|
||||
- 验收标准:实现任务说明中写明参考了哪个 `packages/...` 文件和哪个搜索点。
|
||||
|
||||
### B. 面板状态模型
|
||||
|
||||
- [ ] B1. 定义 Leptos 侧 `HermesPanelState`。
|
||||
- 验收标准:状态至少包含 `sessionId`、`sessionTitle`、`agentProfileName`、`profileFilesState`、`skillListState`、`modelName`、`runStatus`、`connectionStatus`、`contextScope`、`toolAvailability`、`lastAudit`。
|
||||
- [ ] B2. 明确哪些字段来自 Hermes,哪些字段来自 mnote。
|
||||
- 验收标准:session/message/model/profile/run/tool event 来自 Hermes;page title/context/audit 来自 mnote;没有字段把 Hermes session title 当作页面标题。
|
||||
- [ ] B3. 增加空态、连接失败态、tool 不可用态。
|
||||
- 验收标准:Hermes 未启动、未配置模型、mnote plugin 缺失、权限不足分别有不同 UI 状态。
|
||||
|
||||
### C. 顶部 Mini Hermes 状态条
|
||||
|
||||
- [ ] C1. 展示当前 session 标题和恢复状态。
|
||||
- 验收标准:刷新页面后能从 Hermes session detail 恢复标题和消息,不从 mnote 本地 state 恢复聊天真相。
|
||||
- [ ] C2. 展示 model/agent profile 摘要。
|
||||
- 验收标准:能看到当前模型与 agent profile;缺失时显示“选择 agent / 去 Hermes 配置”的动作,而不是在 mnote 内要求填写 API key。
|
||||
- [ ] C3. 增加设置跳转入口。
|
||||
- 验收标准:入口跳到 Hermes 设置或 profile 页面;mnote 页面内不出现 provider/API key 编辑表单。
|
||||
- [ ] C4. 支持 agent/profile 选择。
|
||||
- 验收标准:profile 列表来自 Hermes;选择新 profile 后新建或切换到该 profile 的 Hermes session,不原地篡改旧 session profile。
|
||||
|
||||
### C5. Agent 人格轻编辑
|
||||
|
||||
- [ ] C5.1 展示当前 agent 的人格文件入口。
|
||||
- 验收标准:至少展示 `SOUL.md`、`USER.md`、`MEMORY.md`;`IDENTITY.md` 只在 Hermes API 暴露时展示。
|
||||
- [ ] C5.2 支持编辑并保存 `SOUL.md`。
|
||||
- 验收标准:保存调用 Hermes API;保存失败不显示成功;保存后新 run 使用更新后人格。
|
||||
- [ ] C5.3 支持编辑并保存 `USER.md` / `MEMORY.md`。
|
||||
- 验收标准:保存作用域显示为当前 Hermes profile;mnote 不直接写 `.hermes` 文件。
|
||||
- [ ] C5.4 防止运行中误改。
|
||||
- 验收标准:run 正在进行时编辑器可以查看,但保存按钮禁用或明确提示“保存只影响后续 run”。
|
||||
|
||||
### D. Run 控制与恢复
|
||||
|
||||
- [ ] D1. 显示 run 状态。
|
||||
- 验收标准:发送后能依次看到 running/tool_calling/completed 或 failed;状态来自 Hermes run event。
|
||||
- [ ] D2. 支持停止当前 run。
|
||||
- 验收标准:点击停止后 Hermes run 进入 aborted/failed 的明确终止态,输入框恢复可用。
|
||||
- [ ] D3. 支持失败后重试或继续。
|
||||
- 验收标准:失败态有明确按钮;重试不会创建 mnote 本地聊天副本。
|
||||
- [ ] D4. 刷新后恢复进行中或已完成 session。
|
||||
- 验收标准:刷新页面不丢失 Hermes 消息;若 run 已结束,状态显示 completed/failed 而不是一直 loading。
|
||||
|
||||
### E. Context Scope
|
||||
|
||||
- [ ] E1. 增加 context scope 控件。
|
||||
- 验收标准:至少支持当前页、当前选区、当前块、页面设置四类;无选区时选区项禁用。
|
||||
- [ ] E2. run input 只携带当前 scope 的上下文摘要。
|
||||
- 验收标准:正文大对象不长期写进 Hermes session;Hermes 需要最新正文时通过 `mnote.page.get` 回读。
|
||||
- [ ] E3. tool call audit 记录 context scope。
|
||||
- 验收标准:`mnote.page.save`、artifact 写入等 audit 能看到本次来源是 page/selection/block/options 哪种 scope。
|
||||
|
||||
### F. Tool 可用性与最近调用
|
||||
|
||||
- [ ] F1. 展示 mnote tools 清单。
|
||||
- 验收标准:清单来源于 Hermes plugin/tool registry 或 mnote-web 过渡聚合,不能只写死在前端。
|
||||
- [ ] F2. 区分只读工具和写入工具。
|
||||
- 验收标准:`mnote.page.get` 明确为只读;`mnote.page.save`、`mnote.page.update_title`、`mnote.artifact.*` 明确为写入。
|
||||
- [ ] F3. tool event 默认折叠,支持展开详情。
|
||||
- 验收标准:交互参考 `MessageItem.vue` 的 `tool-preview` / `tool-details`,但使用 Leptos 实现;长 JSON 不撑破布局。
|
||||
- [ ] F4. 最近 tool call 关联 audit。
|
||||
- 验收标准:能从 UI 或测试输出看到 `sessionId/runId/toolCallId/traceId/auditId` 的串联。
|
||||
|
||||
### F5. Skill 管理最小面
|
||||
|
||||
- [ ] F5.1 展示 Hermes skills 列表和搜索。
|
||||
- 验收标准:列表来自 Hermes skills API;能按名称/描述过滤;显示 builtin / hub / local / modified 来源。
|
||||
- [ ] F5.2 支持单个 skill 启停。
|
||||
- 验收标准:开关调用 Hermes `toggleSkill` 语义;刷新后状态仍由 Hermes 返回;失败时回滚 UI 开关。
|
||||
- [ ] F5.3 支持查看 skill 详情。
|
||||
- 验收标准:能查看 `SKILL.md` 摘要或详情;长内容不撑破抽屉;详情来源于 Hermes API。
|
||||
- [ ] F5.4 明确 skill toggle 作用域。
|
||||
- 验收标准:UI 说明启停影响当前 Hermes profile 或 Hermes 全局配置;不把它描述成仅当前 mnote 页面临时开关。
|
||||
|
||||
### G. Session 列表最小面
|
||||
|
||||
- [ ] G1. 支持当前页面最近 Hermes sessions 列表。
|
||||
- 验收标准:列表来自 Hermes session API;只过滤/标注当前 document context,不复制 session 到 mnote。
|
||||
- [ ] G2. 支持切换 session。
|
||||
- 验收标准:切换后 message list 从 Hermes detail 恢复;页面正文不因切换 session 被自动修改。
|
||||
- [ ] G3. P2 支持 rename/delete/search。
|
||||
- 验收标准:若未实现,UI 不出现可点击假按钮;若实现,操作调用 Hermes session API。
|
||||
|
||||
### H. 错误与权限
|
||||
|
||||
- [ ] H1. Hermes 未启动或 proxy 502 时显示可诊断状态。
|
||||
- 验收标准:错误能区分 upstream unavailable、auth/permission、model missing、tool unavailable。
|
||||
- [ ] H2. 写入工具权限失败不泄露正文。
|
||||
- 验收标准:tool error 展示摘要、trace/audit id 和恢复动作,不展示不必要的正文 payload。
|
||||
- [ ] H3. 旧 `/api/ai-agent/run` 继续保持退场 guard。
|
||||
- 验收标准:新页面 AI 主链不会调用旧入口;retirement smoke 继续通过。
|
||||
|
||||
### I. 自动化验收
|
||||
|
||||
- [x] I1. 新增或扩展 browser smoke:session 状态条。
|
||||
- 验收标准:断言 session title/model/run status 可见。
|
||||
- [x] I2. 新增或扩展 browser smoke:agent/profile 选择。
|
||||
- 验收标准:断言 profile 列表来自 Hermes;切换 profile 后新 run/session 使用目标 profile。
|
||||
- [x] I3. 新增或扩展 browser smoke:agent 人格文件编辑。
|
||||
- 验收标准:保存 `SOUL.md` 或 `USER.md` 后通过 Hermes API 读回一致;运行中保存行为符合禁用或提示规则。
|
||||
- [x] I4. 新增或扩展 browser smoke:skill 启停。
|
||||
- 验收标准:toggle 后刷新仍保持 Hermes 返回状态;失败时 UI 回滚。
|
||||
- [x] I5. 新增或扩展 browser smoke:run stop/retry。
|
||||
- 验收标准:至少覆盖 stop;retry 若未实现则断言按钮不存在或禁用。
|
||||
- [x] I6. 新增或扩展 browser smoke:context scope。
|
||||
- 验收标准:选区/当前页上下文能进入 run payload 或 tool audit。
|
||||
- [x] I7. 新增或扩展 browser smoke:tool 可用性与 audit。
|
||||
- 验收标准:能看到 mnote tool 列表、一次 tool call、对应 audit id。
|
||||
- [x] I8. 移动端 smoke。
|
||||
- 验收标准:状态条、tool 折叠、输入区在窄屏不重叠。
|
||||
|
||||
---
|
||||
|
||||
## 7. Done Gate
|
||||
|
||||
本稿移入 `done/` 前必须同时满足:
|
||||
|
||||
- [x] 页面 AI 抽屉具备 Mini Hermes 状态条,用户能看见当前 session、model/agent profile、run 状态。
|
||||
- [x] 页面 AI 抽屉能选择 agent/profile;切换 agent 不篡改已有 Hermes session profile。
|
||||
- [x] 页面 AI 抽屉能轻编辑当前 agent 的 `SOUL.md`、`USER.md`、`MEMORY.md`,保存真相在 Hermes。
|
||||
- [x] 页面 AI 抽屉能搜索、查看并启停 Hermes skill,刷新后状态仍以 Hermes 返回为准。
|
||||
- [x] 页面 AI 抽屉具备 context scope 控件,run input 和 tool audit 能反映 scope。
|
||||
- [x] 页面 AI 抽屉能展示 mnote tool 可用性、最近 tool call 和 audit/trace 串联。
|
||||
- [x] stop 至少可用;retry/rename/delete 若未实现,必须明确标为 P2 且 UI 不出现假可用按钮。
|
||||
- [x] Hermes 未启动、模型缺失、plugin/tool 不可用、权限失败至少四类错误有可区分展示。
|
||||
- [x] 页面刷新后仍以 Hermes session detail/resume 为会话真相。
|
||||
- [x] mnote 不保存聊天消息真相,不保存 provider/API key,不创建第二套 profile/skill/plugin registry。
|
||||
- [x] `git diff --check` 通过。
|
||||
- [x] Rust 侧相关测试通过:`cargo test -p mnote-web hermes_client -- --nocapture`、`cargo test -p mnote-web hermes_tools_ -- --nocapture`。
|
||||
- [x] 正式 `3000` smoke 覆盖 session/run/tool/audit/mobile,且证据写回本文。
|
||||
|
||||
---
|
||||
|
||||
## 8. P2 后续项
|
||||
|
||||
以下能力不阻塞本稿 `done`,后续另开设计或 checklist 推进:
|
||||
|
||||
- retry / continue 的更完整 Hermes run lifecycle。
|
||||
- session rename / delete / search。
|
||||
- profile create / rename / import / export。
|
||||
- skill 文件详情、pin、上传与 marketplace 发布。
|
||||
- usage / logs / jobs / files / channels / terminal / group chat 等 Hermes 管理台页面。
|
||||
|
||||
不要先做 Hermes 全局设置页复刻。API key、provider、usage/logs/jobs/files/channels、profile 导入导出、skill marketplace 发布仍应留在 Hermes 自己的管理台。
|
||||
|
||||
---
|
||||
|
||||
## 9. 验收证据
|
||||
|
||||
- 设计参考已落到本文第 2 节的精确文件索引;人格页参考图为 `/mnt/Data1T/mnote/tmp/img_v3_0211m_b7390ad9-a45b-4d94-8126-b7c17f029feg.png`,技能页参考图为 `/mnt/Data1T/mnote/tmp/img_v3_0211m_0d6e7a60-8c21-4fd1-8cdf-d548dab7532g.png`。
|
||||
- 代码入口:`rust/crates/mnote-web/src/ssr/pages/layout.rs`、`rust/crates/mnote-web/src/ssr/styles.rs`、`rust/crates/mnote-web/src/routes/hermes_client.rs`、`rust/crates/mnote-web/src/routes/web_shell.rs`、`rust/crates/mnote-web/src/routes/mod.rs`。
|
||||
- 自动化:
|
||||
- `cargo test -p mnote-web hermes_client -- --nocapture`
|
||||
- `cargo test -p mnote-web hermes_tools -- --nocapture`
|
||||
- `cargo test -p mnote-web web_shell -- --nocapture`
|
||||
- `node scripts/task-hermes-page-ai-agent-skill-smoke.js`
|
||||
- `node scripts/task-hermes-page-ai-smoke.js`
|
||||
- `node scripts/task-hermes-page-ai-mobile-smoke.js`
|
||||
- `MNOTE_EXPECTED_HERMES_SETTINGS_URL=http://127.0.0.1:8642/hermes/settings node scripts/task-hermes-page-ai-runtime-controls-smoke.js`
|
||||
- 截图证据:`/mnt/Data1T/mnote/tmp/page-ai-mini-hermes-control-surface.png`。
|
||||
- 运行约束:上述 smoke 已在正式 `3000` mnote-web 入口执行;`3000` 已重启为当前最新二进制。
|
||||
@@ -1,975 +0,0 @@
|
||||
# 7-9 [done] 页面/块 AI 工具体系规划 v1
|
||||
|
||||
> 更新时间:2026-05-16
|
||||
>
|
||||
> 当前状态:`DONE`。
|
||||
>
|
||||
> 本稿承接 `7-6` 的 mnote Hermes plugin tool 合同、`7-8` 的 Hermes Runtime BFF 方向,以及近期页面 AI 工具实测中暴露的问题:当前 `mnote.page.get/save/update_title/update_options` 已能完成页面级读写,但工具粒度仍偏粗,不能长期代表“AI 能精确编辑页面/块”。
|
||||
>
|
||||
> 核心参考:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-vscode-main`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/ai-toolkit-demos`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-docs`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-main`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-apcore`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-ai-autocomplete`
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-8-page-ai-hermes-runtime-bff-next-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论
|
||||
|
||||
不要把问题理解成二选一:
|
||||
|
||||
- 不是“等 Rust kernel / Page Aggregate / editor module 完全稳定后,才开始设计工具”。
|
||||
- 也不是“现在立刻扩一批块级写工具,让 Hermes 直接调用当前 Convex / 前端私有 shape”。
|
||||
|
||||
正确路径是:
|
||||
|
||||
> **现在继续写工具体系设计,冻结 AI 面向的稳定工具合同;实现上分阶段推进,先做只读、定位、dry-run、plan 和最小单块写入。复杂块移动、块嵌入、多块批量改写、媒体块编排,要等 Rust block model、Page Aggregate command、Convex 持久化和 editor session 刷新链路达到硬门槛后再开放。**
|
||||
|
||||
当前 `mnote.page.save` 可以继续作为页面级兜底工具,但不能继续被描述为长期块编辑方案。长期工具体系必须基于稳定的 `Page Aggregate / EditorBlockDocument / Rust runtime command` 中间合同,而不是让 AI 直接操作前端 Tiptap JSON、Convex `documents.content` 私有结构或历史 `blocks` 表。
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么参考飞书 CLI
|
||||
|
||||
`cli-main` 的价值不在于“飞书有很多命令,所以 mnote 也应该堆很多工具”,而在于它把文档工具做成了三层:
|
||||
|
||||
1. **Shortcut 层**:面向人和 Agent 的高层命令,例如 `docs +fetch`、`docs +update`、`docs +media-insert`。
|
||||
2. **稳定文档操作协议层**:用 `doc-format`、`scope`、`detail`、`command`、`block_id`、`revision_id`、`dry-run` 表达文档读写。
|
||||
3. **底层 API adapter 层**:把稳定协议翻译成真实平台 API,例如 `docs_ai/v1/documents` 或 MCP tool call。
|
||||
|
||||
这正好对应 mnote 当前问题:
|
||||
|
||||
- Rust kernel、Page Aggregate、Convex、leptos-tiptap 还在收口。
|
||||
- 如果现在让 Hermes 直接调用底层 shape,后续底层一变,skill/tool 就会失效。
|
||||
- 如果先冻结一个 AI 面向的稳定文档操作协议,底层变动可以收口在 adapter。
|
||||
|
||||
因此 mnote 应参考的是飞书的“稳定投影 DSL + 少量高层命令 + dry-run 诊断层”,而不是照搬 Go CLI 或飞书 API。
|
||||
|
||||
---
|
||||
|
||||
## 3. `cli-main` 具体参考位置
|
||||
|
||||
参考根目录:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main
|
||||
```
|
||||
|
||||
优先参考下表,不需要整仓搬运:
|
||||
|
||||
| mnote 主题 | 参考文件 | 搜索点 | 可吸收内容 | 不采用内容 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 文档工具注册 | `shortcuts/doc/shortcuts.go` | `Shortcuts()`、`docs +fetch`、`docs +update` | 工具分组、命令命名、文档域工具入口 | 不照搬 CLI 交互层 |
|
||||
| 文档读取 v2 | `shortcuts/doc/docs_fetch_v2.go` | `executeFetchV2`、`buildReadOption`、`--detail`、`--scope` | `simple/with-ids/full`、`outline/range/keyword/section`、局部读取 | 不使用飞书 token/API |
|
||||
| 文档更新 v2 | `shortcuts/doc/docs_update_v2.go` | `validCommandsV2`、`buildUpdateBody`、`revision-id`、`dry-run` | `str_replace/block_insert_after/block_replace/block_delete/block_move_after/append/overwrite` | 不直接把飞书 command 当 mnote 内部命令名 |
|
||||
| v1 兼容更新 | `shortcuts/doc/docs_update.go` | `CallMCPTool`、`update-doc` | 兼容层与主线层并存时的隔离方式 | 不保留多套长期真相 |
|
||||
| 更新前诊断 | `shortcuts/doc/docs_update_check.go` | `CheckDocsUpdateArgs`、`warning` | 工具调用前给 Agent 的静态语义警告 | 不只靠 LLM 自觉避免危险编辑 |
|
||||
| 媒体插入编排 | `shortcuts/doc/doc_media_insert.go` | `dry-run`、`steps`、`batch_update` | 多步工具先 dry-run 展示计划,再执行 | 第一阶段不做完整媒体工具 |
|
||||
| 文档 XML DSL | `skills/lark-doc/references/lark-doc-xml.md` | `<title>`、`<callout>`、`<grid>`、`<img>`、`<cite>` | 用 PageXML/PageMarkdown 屏蔽底层块结构 | 不采用飞书专有块类型作为 mnote 类型 |
|
||||
| 文档更新说明 | `skills/lark-doc/references/lark-doc-update.md` | `str_replace`、`block_insert_after`、`revision`、`warnings` | 面向 Agent 的工具使用说明、返回结构 | 不把说明当实现 |
|
||||
| 文档读取说明 | `skills/lark-doc/references/lark-doc-fetch.md` | `detail`、`scope`、`with-ids` | 读取前先定位、再编辑的 workflow | 不让 AI 默认整页读取超大正文 |
|
||||
| 工具抽象 | `shortcuts/common/types.go` | `Shortcut`、`Validate`、`Execute`、`Risk` | 元信息、权限、风险、dry-run、validate 一体化 | 不复制 Go 框架 |
|
||||
| dry-run 通用能力 | `internal/cmdutil/dryrun.go` | `DryRun` | 所有写工具都能返回计划和风险 | 不做纯 CLI 文本输出 |
|
||||
| 风险提示 | `internal/cmdutil/risk.go` | `Risk` | 工具风险等级进入确认和审计 | 不用 CLI prompt 作为 Web 确认机制 |
|
||||
|
||||
快速定位命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main
|
||||
rg -n "validCommandsV2|buildReadOption|buildUpdateBody|revision|dry-run|CheckDocsUpdateArgs|Shortcut|Risk|CallMCPTool|scope|detail" shortcuts internal skills/lark-doc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 官方 Tiptap AI Toolkit 与补充参考的吸收边界
|
||||
|
||||
Tiptap AI Toolkit 的核心价值是把编辑器能力拆成 AI 可调用工具,而不是让外部系统直接暴露编辑器内部实现。Context7 与本地 `tiptap-docs` 公开文档能确认的主线能力包括:
|
||||
|
||||
- `toolDefinitions()` 向 AI SDK 暴露工具定义。
|
||||
- `tiptapRead` 用高效格式读取文档,支持 range/chunk 类读取模型。
|
||||
- `tiptapEdit` 用 operations 列表编辑文档。
|
||||
- `tiptapReadSelection` 读取当前选区。
|
||||
- `executeTool` / `streamTool` 把 AI 生成的 tool call 应用到 editor,并返回 `docChanged`、错误和工具结果。
|
||||
- review options 支持 `disabled/review/preview/trackedChanges`,但 preview/suggestions 与持久 tracked changes 是两类语义。
|
||||
|
||||
需要修正之前口径:
|
||||
|
||||
- 不能再说 “Tiptap 没有官方 AI 文档工具”。它有官方 AI Toolkit 工具层。
|
||||
- 也不能说 “Tiptap AI Toolkit 可以直接解决 mnote 块工具”。公开文档没有给出完整 `tiptapEdit.operations` schema,且它面向 Tiptap/ProseMirror 文档层,不覆盖 mnote 的 Rust kernel、Page Aggregate、Convex revision/conflict key、Hermes audit。
|
||||
- 当前 npm registry 无法直接获取 `@tiptap-pro/ai-toolkit*` 完整实现源码;本仓可参考的是公开 docs 与 `tiptap-main/packages/server-ai-toolkit`、`extension-unique-id`、`extension-drag-handle`、`extension-node-range` 等开源代码。
|
||||
|
||||
这些能力应映射为 mnote 自己的工具层:
|
||||
|
||||
| Tiptap AI Toolkit | mnote 对应 | 吸收内容 | 不吸收内容 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tiptapRead` | `mnote.doc.fetch` / `mnote.block.fetch` | 先读、带范围、返回适合 AI 的文档表示 | 不把 Tiptap JSON 作为长期工具格式 |
|
||||
| `tiptapEdit` | `mnote.doc.plan_update` + `mnote.block.*` | 操作列表、reviewable edit、meta justification | 不让浏览器 editor command 成为事实源 |
|
||||
| `tiptapReadSelection` | `mnote.doc.fetch scope=selection` | selection-aware workflow | 不直接持久化浏览器 selection range |
|
||||
| `toolDefinitions()` | Rust Hermes manifest | schema、description、capability、annotation | 不依赖私有 npm 包作为运行时硬依赖 |
|
||||
|
||||
`tiptap-apcore` 的补充价值更偏工具基础设施。它已经把 Tiptap command 分成 query、format、content、destructive、selection、history,并为工具提供:
|
||||
|
||||
- `inputSchema` / `outputSchema`
|
||||
- `readonly` / `destructive` / `idempotent` / `requiresApproval`
|
||||
- ACL role:`readonly`、`editor`、`admin`
|
||||
- `selectionEffect`
|
||||
- executor 前置检查、ACL、query/command 分发
|
||||
|
||||
mnote 应吸收这些元数据,但命名和执行面必须改成 Rust-owned:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mnote.block.replace",
|
||||
"capabilityScope": ["page.write", "block.write"],
|
||||
"annotations": {
|
||||
"readonly": false,
|
||||
"destructive": false,
|
||||
"idempotent": false,
|
||||
"requiresApproval": true,
|
||||
"selectionEffect": "destroy"
|
||||
},
|
||||
"runtimeOwner": "mnote-web",
|
||||
"writeOwner": "rust-runtime-kernel"
|
||||
}
|
||||
```
|
||||
|
||||
`tiptap-ai-autocomplete` 的价值限于交互层:
|
||||
|
||||
- ghost text 定位。
|
||||
- 选区 bubble menu。
|
||||
- streaming preview。
|
||||
- accept/reject 后再写入。
|
||||
|
||||
它不应进入 Hermes tool contract 的核心,只能作为页面 AI 面板、选区 AI 菜单和 preview UI 的参考。
|
||||
|
||||
`tiptap-main` 开源代码给 mnote 的补充约束:
|
||||
|
||||
- `extension-unique-id` 可作为 Tiptap runtime 节点 id 辅助,但不能替代 Rust `EditorBlock.block_id`。
|
||||
- `server-ai-toolkit` 的 `_hash` 是 AI 编辑定位/变化检测辅助,不能替代业务 `blockId`;它可以参与 mnote `revisionRef` 的 hash 部分。
|
||||
- `extension-drag-handle` 与 `extension-node-range` 说明块选择、拖拽、selection toolbar AI 应共享一套 node range 计算,而不是每个入口重新解析 DOM。
|
||||
- schema awareness / editor context 应进入 `mnote.doc.fetch` 或工具 manifest 的上下文生成,避免 AI 猜测当前可用块类型。
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前 mnote 稳定性判断
|
||||
|
||||
### 5.1 已经可以承载工具设计的边界
|
||||
|
||||
- Page Aggregate 读合同已经稳定为 `mnote.page_aggregate.v1`。
|
||||
- 页面主读链已经可以通过 Rust `/api/page-aggregate/:id` 返回 meta/content/options/body snapshot。
|
||||
- Hermes 页面 AI 已有 `mnote.*` 工具注册、dispatch、trace/audit、idempotency、dryRun 基础。
|
||||
- 页面级工具 `mnote.page.get/save/update_title/update_options` 已能完成最小闭环。
|
||||
- Rust `core-protocol` 已有工具规格方向,`bridge-runtime` 已出现 `doc_insert_blocks`、`doc_replace_range` 等工具/测试雏形。
|
||||
|
||||
这些足以支撑“工具合同设计”和“只读/定位/dry-run 工具实现”。
|
||||
|
||||
### 5.2 还不适合大规模开放块级写工具的边界
|
||||
|
||||
- 当前保存主链仍是 `documents.content` legacy JSON snapshot,不是 `EditorBlockDocument` 原生落库,也不是 Tiptap JSON 原生落库。
|
||||
- 历史 `blocks` 表仍存在,但没有维护正文主链所需的 order、父子顺序和 `documents.content` 同步闭环,不能描述为当前正文事实源。
|
||||
- Page Aggregate 当前 `body.content` 仍主要是 `documents:getContent` 的 projection 包装,不是强类型 block protocol。
|
||||
- Page Aggregate 当前仍有 compat join 痕迹,不是所有页面正文语义都已经 kernel-native。
|
||||
- 部分 Rust block/editor command 已有 runtime 映射,但 Convex 执行面、editor session 刷新和真实页面可见性还需要逐条验收。
|
||||
- 块移动、块复制、块嵌入、块引用维护需要稳定块 id、revision、父子关系、排序、权限和冲突处理;当前不应直接给 Hermes 开生产写入口。
|
||||
- `mnote.page.save` 当前是整页/追加/前置级写入,适合作为过渡和兜底,不适合承诺“精确块编辑”。
|
||||
- `mnote.page.save` 当前要求 `dryRun/idempotencyKey`,但页面级 revision/conflict key 还不是强制写入门槛;块级写工具开放前必须补上强制 CAS 或等价冲突阻断。
|
||||
|
||||
### 5.3 设计上的硬边界
|
||||
|
||||
- AI 工具不得绕过 Rust runtime 直接写 Convex。
|
||||
- AI 工具不得把前端 Tiptap JSON 当长期外部合同。
|
||||
- AI 工具不得依赖浏览器临时 DOM id 或 runtime-only selection。
|
||||
- 写工具必须有 `dryRun`、`idempotencyKey`、`revision` 或等价冲突键。
|
||||
- 写工具必须返回 `warnings`,并能阻止明显错误的编辑假设。
|
||||
- 写工具成功后必须能通过真实页面、`/api/page-aggregate` 和再次 `mnote.*.fetch/get` 三处验收。
|
||||
|
||||
---
|
||||
|
||||
## 6. 工具体系分层
|
||||
|
||||
### L0:内部 kernel / adapter 命令
|
||||
|
||||
只给 Rust runtime、Page Aggregate adapter、Convex bridge 使用,不直接暴露给 Hermes。
|
||||
|
||||
示例:
|
||||
|
||||
- `page.aggregate.get`
|
||||
- `page.body.save`
|
||||
- `page.body.apply_patch`
|
||||
- `editor.block.insert_after`
|
||||
- `editor.block.replace`
|
||||
- `editor.block.delete`
|
||||
- `editor.block.move_after`
|
||||
- `tree.node.create`
|
||||
- `tree.subtree.move`
|
||||
|
||||
要求:
|
||||
|
||||
- 可以随内核演进调整。
|
||||
- 必须有测试覆盖。
|
||||
- 由 L1 canonical tools 翻译调用。
|
||||
|
||||
### L1:AI 可调用 canonical tools
|
||||
|
||||
Hermes skill/plugin 对外暴露的稳定工具层。第一阶段只做少量、可解释、可验收工具。
|
||||
|
||||
建议命名:
|
||||
|
||||
- `mnote.doc.fetch`
|
||||
- `mnote.doc.find`
|
||||
- `mnote.doc.plan_update`
|
||||
- `mnote.doc.apply_update`
|
||||
- `mnote.block.fetch`
|
||||
- `mnote.block.insert_after`
|
||||
- `mnote.block.replace`
|
||||
- `mnote.block.delete`
|
||||
- `mnote.block.move_after`
|
||||
- `mnote.page.append`
|
||||
- `mnote.page.overwrite`
|
||||
|
||||
其中:
|
||||
|
||||
- `mnote.doc.fetch/find/plan_update` 可先做。
|
||||
- `mnote.block.insert_after/replace` 是最小块写入切片。
|
||||
- `mnote.block.delete/move_after` 等架构稳定后再做。
|
||||
- `mnote.page.overwrite` 是高风险兜底,必须强确认和 dry-run。
|
||||
|
||||
### L2:workflow tools
|
||||
|
||||
面向用户任务的编排工具,不应第一阶段优先做。
|
||||
|
||||
示例:
|
||||
|
||||
- `mnote.workflow.write_weekly_report`
|
||||
- `mnote.workflow.rewrite_section`
|
||||
- `mnote.workflow.extract_action_items`
|
||||
- `mnote.workflow.create_meeting_note`
|
||||
- `mnote.workflow.generate_project_plan`
|
||||
|
||||
要求:
|
||||
|
||||
- L2 必须调用 L1,不直接写 L0。
|
||||
- L2 的产物先走 `plan_update` / `dryRun`,用户确认后再执行。
|
||||
- 不用 L2 掩盖 L1 工具合同不稳定的问题。
|
||||
|
||||
---
|
||||
|
||||
## 7. 稳定文档表示
|
||||
|
||||
### 7.1 PageMarkdown
|
||||
|
||||
适合纯文本、标题、列表、引用、代码、简单表格等常见 AI 输出。
|
||||
|
||||
用途:
|
||||
|
||||
- AI 生成大纲、摘要、会议纪要。
|
||||
- `append/prepend/overwrite`。
|
||||
- `str_replace` 简单替换。
|
||||
|
||||
限制:
|
||||
|
||||
- 不表达复杂属性、块引用、嵌入、资源块、页面块关系。
|
||||
- 不作为唯一长期格式。
|
||||
|
||||
### 7.2 PageXML
|
||||
|
||||
参考飞书 DocxXML,但定义 mnote 自己的 PageXML。用于需要稳定 block id、属性、资源引用和结构化块的场景。
|
||||
|
||||
最小形态示例:
|
||||
|
||||
```xml
|
||||
<page title="项目计划">
|
||||
<heading level="2" block-id="heading_1">目标</heading>
|
||||
<paragraph block-id="p_1">第一段文字</paragraph>
|
||||
<todo block-id="todo_1" checked="false">确认方案</todo>
|
||||
<callout tone="info">重要说明</callout>
|
||||
</page>
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- `block-id` 只引用 mnote 已存在或本次 dry-run 分配的新 id。
|
||||
- PageXML 是 AI 外部合同,内部可翻译为 `EditorBlockDocument`。
|
||||
- 不暴露 Tiptap 节点私有字段。
|
||||
- 不复制飞书专有块;只取 DSL 思想。
|
||||
|
||||
### 7.3 EditorBlockDocument
|
||||
|
||||
Rust 内部结构化文档模型,是 PageXML/PageMarkdown 到 Page Aggregate / editor session 的中间形态。
|
||||
|
||||
要求:
|
||||
|
||||
- 持有稳定 `block_id`。
|
||||
- 能表达 block type、text、attrs、children、parent、order、path、revisionRef。
|
||||
- 能生成 diff plan。
|
||||
- 能映射到当前 Convex-backed 保存链路。
|
||||
- 能在 Tiptap JSON、legacy `documents.content` 与 Page Aggregate block projection 之间做受控转换。
|
||||
|
||||
---
|
||||
|
||||
## 8. Canonical tool schema 草案
|
||||
|
||||
### 8.1 `mnote.doc.fetch`
|
||||
|
||||
用途:读取当前文档或局部文档,返回可供 AI 定位和编辑的稳定投影。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"format": "markdown",
|
||||
"detail": "with_ids",
|
||||
"scope": "full",
|
||||
"startBlockId": null,
|
||||
"endBlockId": null,
|
||||
"keyword": null,
|
||||
"sectionTitle": null,
|
||||
"contextBefore": 2,
|
||||
"contextAfter": 2,
|
||||
"maxDepth": 6
|
||||
}
|
||||
```
|
||||
|
||||
字段约束:
|
||||
|
||||
- `format`: `markdown | page_xml | text | json`
|
||||
- `detail`: `simple | with_ids | full`
|
||||
- `scope`: `full | outline | range | keyword | section | selection | block`
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"revision": "rev_12",
|
||||
"conflictDetectionKey": "doc_1:12",
|
||||
"format": "markdown",
|
||||
"detail": "with_ids",
|
||||
"scope": "section",
|
||||
"content": "## 目标 <!-- block:heading_1 -->\n正文 <!-- block:p_1 -->",
|
||||
"blocks": [
|
||||
{ "blockId": "heading_1", "type": "heading", "text": "目标", "depth": 0, "revisionRef": "pageRev:rev_12:block:heading_1:hash:aaa" },
|
||||
{ "blockId": "p_1", "type": "paragraph", "text": "正文", "depth": 1, "revisionRef": "pageRev:rev_12:block:p_1:hash:bbb" }
|
||||
],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] `scope=full` 能返回当前页面正文,且 `with_ids` 包含稳定 `blockId`。
|
||||
- [ ] `scope=outline` 只返回标题/层级和必要 block id。
|
||||
- [ ] `scope=keyword` 返回命中块和前后上下文。
|
||||
- [ ] 返回内容与 `/api/page-aggregate/:id` 的 block snapshot 一致。
|
||||
- [ ] 不依赖浏览器 DOM。
|
||||
|
||||
### 8.2 `mnote.doc.find`
|
||||
|
||||
用途:在当前文档中查找关键词、块类型、标题或引用目标,帮助 AI 先定位再编辑。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"query": "待办",
|
||||
"match": "text",
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"matches": [
|
||||
{
|
||||
"blockId": "todo_1",
|
||||
"type": "todo",
|
||||
"text": "确认待办",
|
||||
"path": ["项目计划", "本周"],
|
||||
"score": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] 可以按文本查找。
|
||||
- [ ] 可以按 block type 查找。
|
||||
- [ ] 返回结果能直接作为 `block.replace/insert_after` 的 anchor。
|
||||
|
||||
### 8.3 `mnote.doc.plan_update`
|
||||
|
||||
用途:只生成变更计划和诊断,不实际写入。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"revision": "rev_12",
|
||||
"conflictDetectionKey": "doc_1:12",
|
||||
"command": "block_replace",
|
||||
"format": "markdown",
|
||||
"blockId": "p_1",
|
||||
"content": "替换后的段落",
|
||||
"dryRun": true
|
||||
}
|
||||
```
|
||||
|
||||
支持命令:
|
||||
|
||||
- `str_replace`
|
||||
- `block_insert_after`
|
||||
- `block_replace`
|
||||
- `block_delete`
|
||||
- `block_move_after`
|
||||
- `append`
|
||||
- `overwrite`
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"dryRun": true,
|
||||
"revision": "rev_12",
|
||||
"plan": [
|
||||
{
|
||||
"op": "replace",
|
||||
"targetBlockId": "p_1",
|
||||
"before": "旧段落",
|
||||
"after": "替换后的段落"
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"requiresConfirmation": true,
|
||||
"risk": "medium"
|
||||
}
|
||||
```
|
||||
|
||||
诊断规则参考 `docs_update_check.go`:
|
||||
|
||||
- 替换目标匹配多个位置时,提示先用 `blockId` 精确定位。
|
||||
- `str_replace` 找不到唯一匹配时,不执行。
|
||||
- 跨多个块的自然语言替换必须转成 `block_delete + block_insert_after` 或 `block_replace`。
|
||||
- `overwrite` 必须标高风险。
|
||||
- 缺少 `revision` 时只允许 dry-run,不允许真实写入。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] 所有写命令都能先 dry-run。
|
||||
- [ ] dry-run 不改变 Convex 内容、不触发 editor 内容变化。
|
||||
- [ ] 返回的 `plan` 可被 UI 折叠展示。
|
||||
- [ ] 明显不安全的编辑返回 `warnings` 或 `blocked=true`。
|
||||
|
||||
### 8.4 `mnote.doc.apply_update`
|
||||
|
||||
用途:执行已经 dry-run 过的文档变更计划。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"revision": "rev_12",
|
||||
"conflictDetectionKey": "doc_1:12",
|
||||
"idempotencyKey": "idem_1",
|
||||
"planId": "plan_1",
|
||||
"command": "block_replace",
|
||||
"format": "markdown",
|
||||
"blockId": "p_1",
|
||||
"content": "替换后的段落",
|
||||
"dryRun": false
|
||||
}
|
||||
```
|
||||
|
||||
执行要求:
|
||||
|
||||
- 必须校验 `revision`。
|
||||
- 必须校验 `idempotencyKey`。
|
||||
- 必须落 Rust runtime / Page Aggregate command。
|
||||
- 成功后必须返回新 revision 和受影响 block id。
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"revision": "rev_13",
|
||||
"changedBlocks": [
|
||||
{ "blockId": "p_1", "op": "replace" }
|
||||
],
|
||||
"audit": {
|
||||
"effect": "write",
|
||||
"commandName": "page.body.apply_update",
|
||||
"commandId": "cmd_1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] 执行后 `/api/page-aggregate/:id` 能读到更新。
|
||||
- [ ] 当前打开页面能自动刷新或通过 editor session reload 看到更新。
|
||||
- [ ] 再次 `mnote.doc.fetch` 能读到更新。
|
||||
- [ ] 重复同一 `idempotencyKey` 不造成重复写入。
|
||||
- [ ] revision 冲突返回 `mnote_tool_conflict`。
|
||||
|
||||
### 8.5 `mnote.block.fetch`
|
||||
|
||||
用途:读取单个块及可选上下文,作为 `tiptapReadSelection` / selection-aware editing 的稳定服务端版本。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "p_1",
|
||||
"revision": "rev_12",
|
||||
"includeChildren": true,
|
||||
"contextBefore": 1,
|
||||
"contextAfter": 1,
|
||||
"format": "json"
|
||||
}
|
||||
```
|
||||
|
||||
字段约束:
|
||||
|
||||
- `blockId` 必须来自 `mnote.doc.fetch/find` 或 Page Aggregate block projection。
|
||||
- `format`: `json | markdown | page_xml | text`
|
||||
- `includeChildren` 默认 `true`,但复杂块可返回 `unsupportedReason`。
|
||||
- `contextBefore/contextAfter` 只返回同父级上下文。
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"revision": "rev_12",
|
||||
"block": {
|
||||
"blockId": "p_1",
|
||||
"type": "paragraph",
|
||||
"text": "正文",
|
||||
"attrs": {},
|
||||
"path": [2],
|
||||
"parentBlockId": null,
|
||||
"order": "00020000",
|
||||
"revisionRef": "pageRev:rev_12:block:p_1:hash:abc",
|
||||
"editable": true
|
||||
},
|
||||
"context": {
|
||||
"before": [],
|
||||
"after": [
|
||||
{ "blockId": "p_2", "type": "paragraph", "text": "下一段" }
|
||||
]
|
||||
},
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] `block.fetch` 能读取 `doc.find` 返回的 block id。
|
||||
- [ ] 返回 `revisionRef`,可被后续写工具用于冲突检测。
|
||||
- [ ] 不存在 block 返回 `mnote_block_not_found`。
|
||||
- [ ] 不可编辑块返回 `editable=false` 与 `unsupportedReason`。
|
||||
|
||||
### 8.6 `mnote.block.replace`
|
||||
|
||||
用途:最小精确块写工具,作为第一批块级写入候选。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "p_1",
|
||||
"revision": "rev_12",
|
||||
"conflictDetectionKey": "doc_1:12",
|
||||
"blockRevisionRef": "pageRev:rev_12:block:p_1:hash:abc",
|
||||
"format": "markdown",
|
||||
"content": "替换后的块内容",
|
||||
"idempotencyKey": "idem_1",
|
||||
"dryRun": true
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] 只替换目标 block,不影响相邻 block。
|
||||
- [ ] block id 稳定,刷新后仍可定位。
|
||||
- [ ] 支持 `dryRun=true`。
|
||||
- [ ] 支持真实页面 smoke。
|
||||
|
||||
### 8.7 `mnote.block.insert_after`
|
||||
|
||||
用途:在指定块后插入一个或多个块。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"anchorBlockId": "p_1",
|
||||
"revision": "rev_12",
|
||||
"conflictDetectionKey": "doc_1:12",
|
||||
"anchorRevisionRef": "pageRev:rev_12:block:p_1:hash:abc",
|
||||
"format": "markdown",
|
||||
"content": "- 新待办",
|
||||
"idempotencyKey": "idem_2",
|
||||
"dryRun": true
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] 插入位置准确。
|
||||
- [ ] 新 block id 由 Rust/runtime 分配或确认,不由 AI 自造。
|
||||
- [ ] 当前页面能看到新块。
|
||||
- [ ] 再次 fetch 能拿到新 block id。
|
||||
|
||||
### 8.8 `mnote.block.move_after`
|
||||
|
||||
用途:移动一个块到同父级 anchor 块后。第一阶段仅作为受限结构性写工具开放。
|
||||
|
||||
入参:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "p_3",
|
||||
"anchorBlockId": "p_1",
|
||||
"revision": "rev_12",
|
||||
"conflictDetectionKey": "doc_1:12",
|
||||
"blockRevisionRef": "pageRev:rev_12:block:p_3:hash:aaa",
|
||||
"anchorRevisionRef": "pageRev:rev_12:block:p_1:hash:bbb",
|
||||
"idempotencyKey": "idem_move_1",
|
||||
"dryRun": true
|
||||
}
|
||||
```
|
||||
|
||||
第一阶段允许:
|
||||
|
||||
- `blockId` 与 `anchorBlockId` 同父级。
|
||||
- 普通 paragraph。
|
||||
- 普通 heading 叶子块。
|
||||
- 普通 todo 叶子块。
|
||||
|
||||
第一阶段阻断:
|
||||
|
||||
- 跨父级移动。
|
||||
- 标题带子块整体移动。
|
||||
- 列表项跨层级移动。
|
||||
- 表格、resource、mindmap、page reference。
|
||||
- 跨页面移动。
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"dryRun": true,
|
||||
"risk": "medium",
|
||||
"diff": [
|
||||
{
|
||||
"op": "move_after",
|
||||
"blockId": "p_3",
|
||||
"anchorBlockId": "p_1",
|
||||
"from": { "parentBlockId": null, "order": "00030000" },
|
||||
"to": { "parentBlockId": null, "afterOrder": "00010000" }
|
||||
}
|
||||
],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] dry-run 不改变页面。
|
||||
- [ ] 正式写入后 moving block id 保持不变。
|
||||
- [ ] 同父级顺序正确。
|
||||
- [ ] 不支持场景返回 `blocked=true` 和明确 warning。
|
||||
- [ ] 再次 `mnote.doc.fetch` 能读回新顺序。
|
||||
|
||||
---
|
||||
|
||||
## 9. 实施阶段
|
||||
|
||||
### Phase A:只读和定位
|
||||
|
||||
目标:
|
||||
|
||||
- 建立 `mnote.doc.fetch` 和 `mnote.doc.find`。
|
||||
- 支持 `format`、`detail`、`scope`。
|
||||
- 让 AI 能稳定读到 block id、标题路径、上下文。
|
||||
|
||||
可做:
|
||||
|
||||
- [ ] 从 Page Aggregate snapshot 生成 PageMarkdown。
|
||||
- [ ] 从 Page Aggregate snapshot 生成 PageXML 最小子集。
|
||||
- [ ] 支持 `scope=full/outline/keyword/block`。
|
||||
- [ ] 支持 `detail=simple/with_ids/full`。
|
||||
- [ ] Hermes tool event UI 展示 fetch/find 摘要。
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] 真实登录页面中,Hermes 调用 `mnote.doc.fetch` 能读出当前页面。
|
||||
- [ ] `with_ids` 返回的 block id 与 DOM `data-block-id` / Page Aggregate 一致。
|
||||
- [ ] `keyword` 读取不会返回整页超大正文。
|
||||
- [ ] 失败时返回权限/不存在/空页面的结构化错误。
|
||||
|
||||
### Phase B:dry-run / plan
|
||||
|
||||
目标:
|
||||
|
||||
- 建立 `mnote.doc.plan_update`。
|
||||
- 写入前先给出可解释 diff、风险、warnings。
|
||||
|
||||
可做:
|
||||
|
||||
- [ ] 支持 `str_replace` dry-run。
|
||||
- [ ] 支持 `block_replace` dry-run。
|
||||
- [ ] 支持 `block_insert_after` dry-run。
|
||||
- [ ] 支持 `append` dry-run。
|
||||
- [ ] 实现静态诊断规则。
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] dry-run 不改变页面内容。
|
||||
- [ ] UI 可折叠展示 plan。
|
||||
- [ ] 多重匹配、不存在 block、缺 revision 都返回明确 warning。
|
||||
|
||||
### Phase C:最小块级写入
|
||||
|
||||
目标:
|
||||
|
||||
- 开放 `mnote.block.replace` 和 `mnote.block.insert_after` 的真实写入。
|
||||
|
||||
前置硬门槛:
|
||||
|
||||
- [ ] Page Aggregate 中每个可编辑块都有稳定 `blockId`。
|
||||
- [ ] Rust runtime 能把 block replace/insert 变更映射到持久化结构。
|
||||
- [ ] Convex-backed 保存链能保存变更并刷新。
|
||||
- [ ] 当前 editor session 能 reload 或 live refresh。
|
||||
- [ ] smoke 覆盖 AI tool -> Rust -> Convex -> 页面可见 -> fetch 回读。
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] AI 替换单个段落后,页面只变这一段。
|
||||
- [ ] AI 在指定块后插入待办后,页面位置正确。
|
||||
- [ ] 刷新页面后内容仍在。
|
||||
- [ ] 版本冲突不会覆盖用户刚刚输入的内容。
|
||||
|
||||
### Phase D:多块和结构性写入
|
||||
|
||||
目标:
|
||||
|
||||
- 开放 `block_delete`、`block_move_after`、`block_copy_insert_after`。
|
||||
|
||||
前置硬门槛:
|
||||
|
||||
- [ ] 块父子关系、排序、缩进、折叠状态稳定。
|
||||
- [ ] 删除/移动能处理子树。
|
||||
- [ ] 冲突检测覆盖移动前后的邻居和父节点。
|
||||
- [ ] 有撤销或可审计回滚策略。
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] 移动标题块时,其子块处理规则明确且测试覆盖。
|
||||
- [ ] 删除块需要确认,并返回被删除范围。
|
||||
- [ ] 复制插入生成新 id,不复用旧 id。
|
||||
|
||||
### Phase E:workflow tools
|
||||
|
||||
目标:
|
||||
|
||||
- 基于 L1 工具做高层 AI 写作工作流。
|
||||
|
||||
可做:
|
||||
|
||||
- [ ] 生成会议纪要。
|
||||
- [ ] 生成周报。
|
||||
- [ ] 重写某一节。
|
||||
- [ ] 提取待办并插入当前页。
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] workflow 只调用 L1 canonical tools。
|
||||
- [ ] 每个 workflow 都能先 plan,再执行。
|
||||
- [ ] 不出现直接整页覆盖用户内容的默认行为。
|
||||
|
||||
---
|
||||
|
||||
## 10. 架构优先级判断
|
||||
|
||||
当前应并行推进,但优先级要明确:
|
||||
|
||||
1. **先稳定 Page Aggregate / block identity / editor save-refresh 链路**:这是块级写工具能否长期可靠的根。
|
||||
2. **同时设计并冻结 L1 canonical tools 合同**:避免继续把临时页面保存扩展成长期能力。
|
||||
3. **先实现只读和 dry-run**:这部分对底层写链依赖小,能立即改善 AI 可靠性。
|
||||
4. **等硬门槛满足后开放最小块写入**:先 `replace` / `insert_after`,不要一口气做完整块操作。
|
||||
5. **最后做 workflow**:工具层不稳定时,workflow 只会放大错误。
|
||||
|
||||
因此,回答“先优化稳定架构,还是继续写工具设计”:
|
||||
|
||||
> **架构稳定是实现复杂写工具的前置;工具设计现在就应该继续,而且必须用于反向约束架构稳定的验收标准。**
|
||||
|
||||
---
|
||||
|
||||
## 11. 近期不做清单
|
||||
|
||||
- [ ] 不让 Hermes 直接调用 Convex mutation。
|
||||
- [ ] 不把 Tiptap JSON 暴露成 AI 长期工具入参。
|
||||
- [ ] 不把 `mnote.page.save` 包装成所有块编辑的长期方案。
|
||||
- [ ] 不先做大量 `workflow.*` 工具。
|
||||
- [ ] 不做无 revision / 无 dry-run / 无 idempotency 的写工具。
|
||||
- [ ] 不在工具里读取浏览器 DOM 来决定写入位置。
|
||||
- [ ] 不用页面当前可见文本做唯一定位依据;必须支持 block id 或唯一匹配诊断。
|
||||
|
||||
---
|
||||
|
||||
## 12. 执行 checklist
|
||||
|
||||
### 12.1 设计冻结
|
||||
|
||||
- [ ] 确认 `mnote.doc.fetch/find/plan_update/apply_update` 命名。
|
||||
- [ ] 确认 `PageMarkdown` 最小语法。
|
||||
- [ ] 确认 `PageXML` 最小语法。
|
||||
- [ ] 确认 `revision` 来源。
|
||||
- [ ] 确认 `blockId` 来源只来自 Page Aggregate / Rust runtime。
|
||||
- [ ] 确认 `warnings` / `risk` / `requiresConfirmation` 返回格式。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] `design/07-ai/process` 中有稳定工具合同。
|
||||
- [ ] `7-6` 不再把 `mnote.page.save` 描述为长期块编辑合同。
|
||||
- [ ] `5-6` 的 Page Aggregate checklist 能引用本稿作为 AI 写入门槛。
|
||||
|
||||
### 12.2 只读工具实现
|
||||
|
||||
- [ ] Rust tool manifest 增加 `mnote.doc.fetch`。
|
||||
- [ ] Rust tool manifest 增加 `mnote.doc.find`。
|
||||
- [ ] Rust tool manifest 增加 `mnote.block.fetch`。
|
||||
- [ ] dispatch 调用 Page Aggregate snapshot,而不是 Convex 私有 shape。
|
||||
- [ ] 支持 `detail=simple/with_ids/full`。
|
||||
- [ ] 支持 `scope=full/outline/keyword/block`。
|
||||
- [ ] 工具结果进入 Hermes tool event UI。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] `cargo test -p mnote-web hermes` 相关测试通过。
|
||||
- [ ] 真实网页登录后,AI 能通过 `mnote.doc.fetch` 读取当前页。
|
||||
- [ ] AI 能通过 `mnote.doc.find` 找到指定文本所在 block id。
|
||||
- [ ] `/api/page-aggregate/:id` 与 tool 返回 block id 一致。
|
||||
|
||||
### 12.3 dry-run 实现
|
||||
|
||||
- [ ] 增加 `mnote.doc.plan_update`。
|
||||
- [ ] 支持 `str_replace` 计划。
|
||||
- [ ] 支持 `block_replace` 计划。
|
||||
- [ ] 支持 `block_insert_after` 计划。
|
||||
- [ ] 返回 `plan/warnings/risk/requiresConfirmation`。
|
||||
- [ ] 静态诊断阻止多重匹配和缺 revision 的真实写入。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] dry-run 不改变页面。
|
||||
- [ ] UI 能显示工具计划。
|
||||
- [ ] 多重匹配返回 warning。
|
||||
- [ ] 不存在 block 返回结构化错误。
|
||||
|
||||
### 12.4 最小块写入实现
|
||||
|
||||
- [ ] 打通 `mnote.block.replace`。
|
||||
- [ ] 打通 `mnote.block.insert_after`。
|
||||
- [ ] 写入统一走 Rust runtime / Page Aggregate command。
|
||||
- [ ] 成功后触发当前 editor session reload 或 live refresh。
|
||||
- [ ] 成功后返回新 revision 和 changedBlocks。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] AI 替换单块,页面立即可见。
|
||||
- [ ] AI 插入新块,位置准确。
|
||||
- [ ] 刷新后内容仍在。
|
||||
- [ ] 再次 fetch 能读回变更。
|
||||
- [ ] revision 冲突被拦截。
|
||||
|
||||
### 12.5 真实网页 smoke
|
||||
|
||||
- [ ] 新建测试页面,写入唯一前缀 `TEST-AI-TOOL-<timestamp>`。
|
||||
- [ ] `mnote.doc.fetch scope=full detail=with_ids` 读取页面。
|
||||
- [ ] `mnote.doc.find` 定位测试段落。
|
||||
- [ ] `mnote.doc.plan_update command=block_replace dryRun=true` 生成计划。
|
||||
- [ ] `mnote.block.replace dryRun=false` 替换段落。
|
||||
- [ ] 页面截图证明内容可见。
|
||||
- [ ] `/api/page-aggregate/:id` 证明内容持久化。
|
||||
- [ ] `mnote.doc.fetch` 再次证明 AI 可读回。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [ ] 证据目录写入 `tmp/hermes-tester/<run-id>/`。
|
||||
- [ ] 失败时记录到 `bugs/07-ai/process/` 或真正 owner 分类。
|
||||
- [ ] 通过后才能把对应 checklist 勾到 done。
|
||||
|
||||
---
|
||||
|
||||
## 13. 与当前页面 AI 的关系
|
||||
|
||||
当前已有工具继续保留:
|
||||
|
||||
- `mnote.page.get`
|
||||
- `mnote.page.save`
|
||||
- `mnote.page.update_title`
|
||||
- `mnote.page.update_options`
|
||||
- `mnote.artifact.*`
|
||||
|
||||
但口径调整为:
|
||||
|
||||
- `mnote.page.get` 是页面级读取,不是长期精确块读取。
|
||||
- `mnote.page.save` 是页面级兜底写入,只适合 append/prepend/replace 等粗粒度操作。
|
||||
- 精确编辑应迁移到 `mnote.doc.*` / `mnote.block.*`。
|
||||
- 页面 AI 面板展示工具时,应把 `page.save` 标为高风险或粗粒度。
|
||||
|
||||
---
|
||||
|
||||
## 14. 迁移完成定义
|
||||
|
||||
本文作为页面/块 AI 工具体系路线图与合同已经归档为 `DONE`;执行验收不再由本文继续承接,而是转入:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
|
||||
当前已成立的 done 边界:
|
||||
|
||||
- [x] `mnote.doc.fetch` / `mnote.doc.find` 已进入 Rust Hermes tool manifest 与 dispatch,并已有最小真实页面 smoke 证据。
|
||||
- [x] `mnote.doc.plan_update` 已进入 dry-run 计划链,可返回 diff、warnings、risk、blocked。
|
||||
- [x] `mnote.block.replace`、`mnote.block.insert_after`、`mnote.block.move_after` 已形成最小块写入闭环。
|
||||
- [x] `mnote.page.save` 已在 UI/manifest 口径中降为页面级兜底工具,不再作为默认精确块编辑入口。
|
||||
- [x] 后续执行项以 `7-10` 跟踪,仍包括 `scope=selection`、`format=page_xml/text`、多块插入、复杂块移动矩阵和持久审阅 UI。
|
||||
Reference in New Issue
Block a user