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.
1035 lines
55 KiB
Markdown
1035 lines
55 KiB
Markdown
# [recycle] 7-15 [process] 页面 AI ACP Agent Runtime 统一抽象层 v1
|
||
|
||
> 创建时间:2026-05-17
|
||
>
|
||
> 当前状态:`DONE`
|
||
>
|
||
> 2026-05-21 Batch J 口径补充:
|
||
> - 本稿的 ACP runtime 核心实现已完成并在当前页面 AI 主链中作为默认 runtime 边界使用;Hermes HTTP proxy 默认关闭,只在显式 compat 开关下保留。
|
||
> - Step 15(多会话压力测试)、Step 16(旧 HTTP proxy cleanup)、Step 17(Reasonix cache benchmark)不再压在本稿内继续推进,已拆到 `design/07-ai/process/7-34-acp-runtime-cleanup-availability-stability-tail-v1.md`。
|
||
> - `page_ai_workflow` 仍是 debug / fast path 兼容门面,不是 local-first 普通 Markdown 的默认 AI 编辑主路径;它必须继续走共享 mnote tool executor。
|
||
>
|
||
> 本稿目的:
|
||
> 1. 在 mnote-web 中引入 ACP(Agent Client Protocol)作为统一 agent runtime 抽象层
|
||
> 2. 使 Hermes(当前)与 Reasonix(缓存优先)可互换,前端下拉切换
|
||
> 3. 褪去当前 `hermes_client.rs` 中的 Hermes-HTTPS-proxy 硬编码,改为 ACP JSON-RPC 通用连接器
|
||
> 4. 复用现有参考代码,最小化重复实现工作
|
||
>
|
||
> 关联文档:
|
||
> - `/mnt/Data1T/mnote/recycle/design/07-ai/retired-http-hermes/7-5-hermes-client-proxy-contract-v1.md`(已退役历史背景)
|
||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-17-acp-session-convex-sharing-contract-v1.md`
|
||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-vscode-main/`
|
||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/DeepSeek-Reasonix-main/`
|
||
|
||
---
|
||
|
||
## 1. 问题描述
|
||
|
||
### 1.1 当前架构
|
||
|
||
```
|
||
浏览器 (页面 AI 面板)
|
||
│ SSE
|
||
▼
|
||
mnote-web (Rust Axum)
|
||
│
|
||
├─ hermes_client.rs (3845 行)
|
||
│ └─ HTTP proxy → Hermes HTTPS gateway API
|
||
│ post /api/hermes/runs
|
||
│ get /api/hermes/runs/{id}/events
|
||
│
|
||
├─ hermes_tools.rs (mnote.doc.* / mnote.block.*)
|
||
│ └─ Rust 兼容工具实现,通过 HTTP/Convex 或本地代理读写文档
|
||
│
|
||
└─ page_ai_workflow.rs (兼容门面)
|
||
└─ local-first 下不再是主路径
|
||
```
|
||
|
||
**问题:**
|
||
1. `hermes_client.rs` 与 Hermes HTTPS API 的字段、鉴权、错误码硬耦合——换 agent runtime 需重写整个模块
|
||
2. Hermes HTTP 协议没有标准化,Reasonix、Claude Code、Cline 各自用不同的 HTTP 接口
|
||
3. 前端 SSE 事件格式(`tool.started` / `message.delta` / `run.completed`)也是 mnote 私有定制的
|
||
4. 没有「运行时选择器」——切换 agent 需要改环境变量重启 mnote-web
|
||
5. `hermes_client.rs` 中大量代码(3845 行)是做 Hermes 专属的 HTTP proxy、session 管理、profile 路由——这些应该在统一抽象层中解决
|
||
|
||
### 1.2 目标架构
|
||
|
||
```
|
||
浏览器 (页面 AI 面板)
|
||
│ SSE (前端不变)
|
||
▼
|
||
mnote-web (Rust Axum)
|
||
│
|
||
├─ ACP Session Manager (统一层,新增 ~800 行)
|
||
│ ├─ 运行时选择器 (profile → agent runtime 映射)
|
||
│ │ ├─ Hermes: spawn("hermes", ["acp"])
|
||
│ │ └─ Reasonix: spawn("node", ["reasonix-acp.mjs"])
|
||
│ ├─ ACP JSON-RPC 2.0 client (通用实现)
|
||
│ │ ├─ session/new
|
||
│ │ ├─ session/prompt
|
||
│ │ ├─ session/cancel
|
||
│ │ └─ session/update ≫ SSE 转发
|
||
│ └─ 代理层:向下游工具通知
|
||
│
|
||
├─ hermes_tools.rs (兼容层)
|
||
│ └─ mnote.doc.* / mnote.block.* / mnote.page.*
|
||
│
|
||
└─ page_ai_workflow.rs (兼容保留)
|
||
└─ local-first 普通正文编辑默认不经过它
|
||
```
|
||
|
||
ACP 是整个架构的支点——它是一个**开放协议**,不是某个产品的私有接口。
|
||
|
||
> 2026-05-18 local-first 口径补充:
|
||
>
|
||
> - 目标不是把 Hermes / Reasonix 再包进一层重型 MNote 工具系统,而是让它们尽量像在 VSCode 中那样直接面对授权后的本地工作区。
|
||
> - MNote 主要负责:页面定位、白名单目录授权、ACP 会话管理、审计、文件变化同步到 tiptap / File Tree / Page Aggregate。
|
||
> - local-first 普通 Markdown 编辑默认不要求 runtime 调 `mnote.doc.markdown_edit`;兼容工具只为 cloud / remote / 复杂结构场景保留。
|
||
|
||
---
|
||
|
||
## 2. ACP 协议标准
|
||
|
||
ACP(Agent Client Protocol)是一个基于 JSON-RPC 2.0 的、面向 AI agent runtime 的标准通信协议。同时被 Reasonix (`src/acp/`) 和 Hermes (`hermes acp` CLI) 实现。
|
||
|
||
### 2.1 传输层
|
||
|
||
NDJSON over stdio(默认),也可用 TCP/Unix socket。每行一个完整的 JSON 对象。
|
||
|
||
### 2.2 协议方法
|
||
|
||
| 方向 | 方法 | 用途 |
|
||
|------|------|------|
|
||
| Client → Server | `session/new` | 创建一个会话线程 |
|
||
| Client → Server | `session/prompt` | 发送用户输入,等待 agent 完成 |
|
||
| Client → Server (notification) | `session/cancel` | 中断正在运行的 prompt |
|
||
| Server → Client (notification) | `session/update` | 推送实时状态变更 |
|
||
| Server → Client (request) | `session/request_permission` | 请求用户审批工具调用 |
|
||
|
||
### 2.3 `session/update` 事件类型
|
||
|
||
| `sessionUpdate` 值 | 含义 | 字段 |
|
||
|---|---|---|
|
||
| `agent_message_chunk` | 模型生成文本增量 | `content: { type: "text", text: "…" }` |
|
||
| `agent_thought_chunk` | 模型推理/思考过程 | `content: { type: "text", text: "…" }` |
|
||
| `tool_call` | 工具调用开始 | `toolCallId`, `title`, `kind: "read"|"edit"|"search"|"execute"|"other"`, `status: "pending"` |
|
||
| `tool_call_update` | 工具状态变更 | `toolCallId`, `status: "in_progress"|"completed"|"failed"`, `content` |
|
||
| `usage_update` | 上下文用量更新 | `used: number`, `size: number` |
|
||
| `session_info_update` | 会话元信息 | `title: string` |
|
||
|
||
### 2.4 对比 mnote 当前 SSE 格式
|
||
|
||
| mnote 当前事件 | ACP 对应事件 | 备注 |
|
||
|---|---|---|
|
||
| `message.delta` | `agent_message_chunk` | 几乎 1:1 |
|
||
| `tool.started` | `tool_call` + `kind` | 前者多了 `preview` 字段 |
|
||
| `tool.completed` | `tool_call_update` + `status: "completed"` | 前者多了 `duration` |
|
||
| `run.completed` | `session/update` 不再发事件,prompt 返回 | 语义等价 |
|
||
| `run.failed` | `tool_call_update` + `status: "failed"` | 语义等价 |
|
||
| 无 | `agent_thought_chunk` | 当前页面 AI 未显示思考过程,新增能力 |
|
||
| 无 | `usage_update` | 可展示 token 用量 |
|
||
|
||
**结论:** 前端桥接层只需做一个事件名映射 + 字段适配(~50 行),即可对接 ACP。
|
||
|
||
---
|
||
|
||
## 3. 参考代码分析 — 可复用部分
|
||
|
||
### 3.1 Hermes VSCode 扩展 (`hermes-vscode-main/`)
|
||
|
||
这是**最完整的 ACP 客户端参考实现**,可以直接指导 mnote-web 的 Rust ACP 客户端设计。
|
||
|
||
| 文件 | 内容 | 可复用方式 |
|
||
|---|---|---|
|
||
| `src/acpClient.ts` | ACP JSON-RPC 2.0 客户端:spawn 子进程、读写 NDJSON、处理分帧、请求/响应/通知路由 [acpClient.ts:30-220] | **逻辑移植到 Rust** — `tokio::process::Command` spawn + `BufReader` 按行读取 + 请求 ID 映射表 |
|
||
| `src/sessionManager.ts` | 会话生命周期管理:session/new → session/prompt → session/cancel,去重,事件派发 [sessionManager.ts:37-294] | **逻辑移植到 Rust** — `HashMap<String, Session>` 管理活跃会话 |
|
||
| `src/protocol.ts` | ACP 事件的类型解析:文本提取、去重、tool call 解析、usage 解析 [protocol.ts:1-120] | **直接指导 Rust struct 设计** |
|
||
| `src/chatPanel.ts` | VSCode WebviewView 桥接:ACP 事件 → webview HTML 渲染 [chatPanel.ts:1-524] | **UI 架构参考** — mnote 页面 AI 面板已存在,只需适配事件格式 |
|
||
| `src/webview/main.ts` | webview 端事件处理:消息渲染、工具展示、todo 面板 [webview/main.ts:1-531] | **UI 交互参考** — tool call 显示方式、todo overlay |
|
||
| `src/webview/renderers.ts` | 工具调用格式化、历史加载 [renderers.ts:1-170] | **前端渲染参考** |
|
||
|
||
**核心设计复用:**
|
||
```typescript
|
||
// acpClient.ts 的精髓:请求-响应匹配
|
||
class AcpClient {
|
||
private pending = new Map<number, PendingRequest>();
|
||
private nextId = 1;
|
||
|
||
async sendRequest(method: string, params: unknown): Promise<unknown> {
|
||
const id = this.nextId++;
|
||
return new Promise((resolve, reject) => {
|
||
this.pending.set(id, { resolve, reject });
|
||
this.output.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.2 Reasonix ACP server (`DeepSeek-Reasonix-main/`)
|
||
|
||
| 文件 | 内容 | 可复用方式 |
|
||
|---|---|---|
|
||
| `src/acp/server.ts` | ACP JSON-RPC 2.0 server 类 [acp/server.ts:1-150] | **理解对端协议** — Reasonix 作为 server 时的行为 |
|
||
| `src/acp/protocol.ts` | ACP 类型定义 + 工具函数 [acp/protocol.ts:1-80] | **协议规格参考** |
|
||
| `src/acp/dispatch.ts` | 内核事件 → ACP `session/update` 映射 [acp/dispatch.ts:55-112] | **事件映射参考** — Reasonix 的 `kernel event → ACP` 与 mnote 的 `ACP → SSE` 是互逆过程 |
|
||
| `src/acp/gates.ts` | 权限审批逻辑 [gates.ts:1-6353] | **可选参考** — 页面 AI 的 tool call 审批 |
|
||
| `src/cli/commands/acp.ts` | `reasonix acp` CLI 命令,将 `CacheFirstLoop` + toolset 包装为 ACP server [acp.ts:1-339] | **Reasonix 侧入口** — 启动 Reasonix ACP 的参考实现 |
|
||
| `desktop/src/protocol.ts` | 桌面客户端的 UI 事件协议 [desktop/protocol.ts:1-432] | **前端事件架构参考** — `ModelDeltaEvent`、`ToolPreparingEvent`、`ToolResultEvent` 等 25+ 事件类型的设计 |
|
||
|
||
### 3.3 复用策略
|
||
|
||
**不要「移植代码」——要「移植逻辑和接口形状」**。
|
||
|
||
Rust 端参考 `acpClient.ts` 的架构,但用 tokio async 重写。核心接口设计:
|
||
|
||
```rust
|
||
// Rust ACP client — 接口形状参考 acpClient.ts
|
||
pub struct AcpClient {
|
||
child: tokio::process::Child,
|
||
stdin: tokio::io::BufWriter<tokio::process::ChildStdin>,
|
||
stdout: tokio::io::BufReader<tokio::process::ChildStdout>,
|
||
pending: HashMap<u64, PendingRequest>,
|
||
next_id: u64,
|
||
}
|
||
|
||
impl AcpClient {
|
||
pub async fn spawn(bin: &str, args: &[&str]) -> Result<Self>;
|
||
pub async fn send_request<P, R>(&mut self, method: &str, params: P) -> Result<R>;
|
||
pub async fn send_notification(&mut self, method: &str, params: Value);
|
||
pub fn on_notification(&mut self, handler: impl Fn(String, Value));
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 架构设计
|
||
|
||
### 4.1 运行时选择器
|
||
|
||
```rust
|
||
// mnote-web 配置
|
||
struct AcpRuntimeConfig {
|
||
name: String, // "hermes" | "reasonix"
|
||
bin: String, // "hermes" | "node"
|
||
args: Vec<String>, // ["acp"] | ["reasonix-acp.mjs"]
|
||
default_model: Option<String>,
|
||
env: HashMap<String, String>,
|
||
}
|
||
```
|
||
|
||
mnote-web 支持多个运行时配置,用户通过 profile 选择:
|
||
|
||
```
|
||
profile "default" → runtime "hermes" (spawn hermes acp)
|
||
profile "reasonix" → runtime "reasonix" (spawn node reasonix-acp-wrapper.mjs)
|
||
```
|
||
|
||
前端获取可用运行时列表:`GET /api/hermes/client/profiles`(现有接口,扩展字段)
|
||
|
||
### 4.1.1 白名单目录即 runtime 权限边界
|
||
|
||
对 local-first 而言,真正重要的不是再做一套“文档工具能力矩阵”,而是把 workspace 白名单准确传给 runtime:
|
||
|
||
```text
|
||
登录用户
|
||
-> 解析 access-policy.json / owner / admin / grant
|
||
-> 得到 allowedRoots = ["/mnt/Data1T/Mnote_data/users/.../my-space", ...]
|
||
-> 启动 Hermes / Reasonix 时把 allowedRoots / cwd / read-write scope 传入 runtime
|
||
-> runtime 直接在这些目录里工作
|
||
```
|
||
|
||
这与 VSCode / Codex 的工作模型一致:
|
||
|
||
- runtime 看到的是一个受限 workspace,而不是一堆抽象页面 API。
|
||
- 页面 AI 只额外提供“当前文件是谁”以及可选选区信息。
|
||
- 对 `.md` 的普通编辑不强制走 `mnote.doc.markdown_edit`。
|
||
- 一旦文件写回磁盘,MNote 再负责把变化同步回前端显示。
|
||
|
||
### 4.2 会话生命周期 (ACP Session Manager)
|
||
|
||
```
|
||
用户发送消息
|
||
│
|
||
├─ POST /api/hermes/client/sessions → 创建 session
|
||
│ ├─ ACP session/new → 得到 sessionId
|
||
│ └─ 返回 { sessionId, ... }
|
||
│
|
||
├─ POST /api/hermes/client/runs → 开始 run(现有接口)
|
||
│ ├─ ACP session/prompt → 下发用户输入 + 页面上下文
|
||
│ └─ 返回 { runId }
|
||
│
|
||
├─ GET /api/hermes/client/events/{runId} → SSE 流
|
||
│ ├─ ACP session/update 的 6 种事件 → SSE 映射
|
||
│ ├─ agent_message_chunk → { event: "message.delta", delta: ... }
|
||
│ ├─ tool_call → { event: "tool.started", tool: ..., kind: ... }
|
||
│ ├─ tool_call_update → { event: "tool.completed", ... }
|
||
│ ├─ agent_thought_chunk → { event: "thought.delta", delta: ... } (新增)
|
||
│ ├─ usage_update → { event: "usage.updated", usage: ... } (新增)
|
||
│ └─ session/update 转 ACP → prompt 返回 → SSE event "run.completed"
|
||
│
|
||
└─ POST /api/hermes/client/runs/{runId}/abort → 取消
|
||
└─ ACP session/cancel
|
||
```
|
||
|
||
### 4.3 工具桥接
|
||
|
||
当前 `hermes_tools.rs` 中注册的 mnote 工具(`mnote.doc.fetch`、`mnote.doc.markdown_edit`、`mnote.block.*`、`mnote.page.*`)对 ACP 来说只是一组 HTTP 兼容端点,而不是 local-first 普通 Markdown 编辑的唯一主路径。
|
||
|
||
对于 Reasonix 作为 runtime 的场景,仍需要一个 Reasonix-side 的工具注册包装脚本,将 mnote 兼容工具注册到 `ToolRegistry`;但 local-first 默认应优先让 runtime 直接拿到授权文件引用,在受限 cwd 中使用自身成熟的 patch/diff/文件编辑能力。
|
||
|
||
```typescript
|
||
// reasonix-acp-wrapper.mjs — ACP 包装层
|
||
// 参考: DeepSeek-Reasonix-main/src/cli/commands/acp.ts (整个文件, ~339 行)
|
||
import { AcpServer } from 'reasonix/acp/server';
|
||
import { DeepSeekClient, CacheFirstLoop, ToolRegistry } from 'reasonix';
|
||
|
||
// 注册 mnote 工具 — 工具实现调用 mnote-web HTTP API
|
||
const tools = new ToolRegistry();
|
||
tools.define({
|
||
name: "mnote.doc.fetch",
|
||
description: "阅读当前文档的 markdown 内容",
|
||
parameters: { ... },
|
||
call: async (args) => {
|
||
// 调 mnote-web Rust HTTP API
|
||
return fetch(`${MNOTE_WEB_URL}/api/hermes/tools/mnote/call`, {
|
||
method: 'POST', body: JSON.stringify({ toolName: "mnote.doc.fetch", args })
|
||
}).then(r => r.json());
|
||
},
|
||
parallelSafe: false,
|
||
});
|
||
|
||
// 启动 ACP server — 参考 acp.ts 的 acpCommand 函数
|
||
const server = new AcpServer();
|
||
const client = new DeepSeekClient({ apiKey: process.env.DEEPSEEK_API_KEY });
|
||
let sessions = new Map();
|
||
|
||
server.onRequest("session/new", async (params) => {
|
||
const loop = new CacheFirstLoop({ client, tools, ... });
|
||
// ... 参考 acp.ts 第 220-330 行
|
||
});
|
||
```
|
||
|
||
工具的实际执行路径:
|
||
|
||
```
|
||
agent → tool call → (通过 TCP/localhost HTTP) → mnote-web Rust hermes_tools.rs
|
||
→ Convex / 文档系统
|
||
```
|
||
|
||
**不需要在 Rust 侧重新注册工具到 Reasonix。** mnote-web 的工具 HTTP 端点 (`/api/hermes/tools/mnote/call`) 仍可保留,只通过 ACP 换掉了 agent runtime;但这些端点主要承担 cloud / remote agent / compat fallback,而不是把所有本地文件编辑都重新包成 mnote 工具。
|
||
|
||
### 4.3.1 local-first 默认工作流
|
||
|
||
local-first 页面 AI 的默认工作流应是:
|
||
|
||
```text
|
||
当前页面 URL / documentId
|
||
-> MNote 解析出真实 markdown 文件路径
|
||
-> MNote 校验该路径是否落在 runtime allowedRoots 白名单内
|
||
-> 把 currentFile / selection / allowedRoots 传给 runtime
|
||
-> runtime 直接读写该文件
|
||
-> watcher / refresh 触发前端 page aggregate 与 tiptap 更新
|
||
```
|
||
|
||
只有在以下情况,runtime 才需要走 mnote 兼容工具:
|
||
|
||
- runtime 本身无法直接访问本地文件
|
||
- 当前 source 是 cloud / sync replica
|
||
- 当前对象不是普通 markdown,而是 mindmap / table / 资源块 / 分享受限对象
|
||
- 需要显式审计某种结构化操作
|
||
|
||
### 4.4 前端 SSE 扩展
|
||
|
||
当前前端 SSE 事件格式 (`HermesRunEvent`):
|
||
|
||
```typescript
|
||
type HermesRunEvent =
|
||
| { event: "tool.started"; tool: string; preview?: string | null }
|
||
| { event: "tool.completed"; tool: string; duration?: number; error?: boolean }
|
||
| { event: "message.delta"; delta: string }
|
||
| { event: "run.completed"; output?: string; usage?: Record<string, unknown> }
|
||
| { event: "run.failed"; error?: string };
|
||
```
|
||
|
||
新增字段(向后兼容):
|
||
|
||
```typescript
|
||
type HermesRunEvent = /* 原有 5 种 */ | {
|
||
event: "thought.delta"; // 新增 — 思考过程
|
||
delta: string;
|
||
} | {
|
||
event: "run.completed"; // 扩展 — 新增缓存指标
|
||
output?: string;
|
||
usage?: Record<string, unknown>;
|
||
cacheHitRate?: number; // 新增:Reasonix 缓存命中率
|
||
cacheHitTokens?: number; // 新增
|
||
};
|
||
```
|
||
|
||
前端 AiAgentPanel 接到 `thought.delta` 后,可渲染在独立区域(参考 hermes-vscode-main 的 `agent_thought_chunk` 处理)。
|
||
|
||
### 4.5 Profile 体系扩展
|
||
|
||
当前 `hermes_client.rs` 的 profile 机制(`configured_upstream_for_profile`)是 Hermes-HTTPS 专有的。需要扩展为通用运行时 profile:
|
||
|
||
```rust
|
||
struct AgentProfile {
|
||
name: String,
|
||
runtime_type: RuntimeType, // Hermes | Reasonix | ACPGeneric
|
||
runtime_config: AcpRuntimeConfig,
|
||
api_key: Option<String>,
|
||
models: Vec<ModelConfig>,
|
||
default_model: Option<String>,
|
||
}
|
||
```
|
||
|
||
当前已存在的 `hermes_client.rs` profile 相关端点不需要大改——profile 切换逻辑不变,只是 profile 的数据结构增加了 `runtimeType` 字段。
|
||
|
||
---
|
||
|
||
## 5. 褪去历史负担 — 模块重构路线
|
||
|
||
### 阶段 0:现状(当前)
|
||
|
||
```
|
||
hermes_client.rs (3845 行)
|
||
├─ HTTP proxy 逻辑 (proxy_json, proxy_stream)
|
||
├─ Hermes API 硬编码 (create_run, stream_events)
|
||
├─ Session/profile/Skills/工具管理
|
||
├─ Memory 管理
|
||
└─ 各种配置和鉴权
|
||
```
|
||
|
||
### 阶段 1:新增 ACP 客户端,并行运行
|
||
|
||
新增文件:
|
||
- `acp_client.rs` — ACP JSON-RPC 2.0 客户端(参考 hermes-vscode-main `acpClient.ts`)
|
||
- `acp_session_manager.rs` — 会话生命周期管理(参考 hermes-vscode-main `sessionManager.ts`)
|
||
- `acp_runtime.rs` — 运行时管理(spawn、健康检查、切换)
|
||
|
||
`hermes_client.rs` 中原有的 HTTP proxy 逻辑标志为 `#[deprecated]`,前端通过 profile 选择使用 ACP 还是旧 HTTP proxy。
|
||
|
||
### 阶段 2:前端运行时选择器
|
||
|
||
AiAgentPanel 增加 runtime 切换下拉框,实际只是切换 mnote-web 内部使用的 profile。
|
||
|
||
### 阶段 3:存量迁移
|
||
|
||
- `list_sessions` → ACP `session/new` + 本地记录
|
||
- `create_session` → ACP `session/new`
|
||
- `create_run` + `stream_events` → ACP `session/prompt` + `session/update` 事件映射
|
||
- `abort_run` → ACP `session/cancel`
|
||
- `list_profiles` → 扩展为包含 runtime_type 字段
|
||
- `gateway_health` → 改为 ACP runtime 健康检查(spawn + PING)
|
||
- `list_tools` → 迁移到 `acp_session_manager.rs`
|
||
|
||
### 阶段 4:退役旧代码
|
||
|
||
当所有 profile 都已迁移到 ACP 后,`hermes_client.rs` 中原来的 HTTP proxy 代码可以删掉。Profile 的 `upstream_url` 字段不再需要——运行时由 `bin + args` 定义。
|
||
|
||
---
|
||
|
||
## 6. 实现计划
|
||
|
||
### 6.1 Rust ACP 客户端 (`acp_client.rs`)
|
||
|
||
**参考:** `hermes-vscode-main/src/acpClient.ts`
|
||
|
||
核心接口:
|
||
|
||
```rust
|
||
use tokio::process::{Command, Child};
|
||
use tokio::io::{BufReader, BufWriter, AsyncBufReadExt, AsyncWriteExt};
|
||
use serde_json::Value;
|
||
use std::collections::HashMap;
|
||
|
||
type PendingMap = HashMap<u64, tokio::sync::oneshot::Sender<Result<Value, AcpError>>>;
|
||
|
||
pub struct AcpClient {
|
||
child: Child,
|
||
writer: BufWriter<ChildStdin>,
|
||
pending: Arc<Mutex<PendingMap>>,
|
||
next_id: AtomicU64,
|
||
}
|
||
|
||
impl AcpClient {
|
||
/// Spawn ACP subprocess
|
||
/// 参考: acpClient.ts line 50-80 (spawn + stdio setup)
|
||
pub async fn spawn(bin: &str, args: &[&str]) -> Result<Self> {
|
||
let mut child = Command::new(bin)
|
||
.args(args)
|
||
.stdin(Stdio::piped())
|
||
.stdout(Stdio::piped())
|
||
.stderr(Stdio::inherit())
|
||
.spawn()?;
|
||
|
||
let writer = BufWriter::new(child.stdin.take().unwrap());
|
||
let reader = BufReader::new(child.stdout.take().unwrap());
|
||
let pending = Arc::new(Mutex::new(HashMap::new()));
|
||
|
||
// 后台读取 stdout 行
|
||
let p = pending.clone();
|
||
tokio::spawn(async move {
|
||
let mut lines = reader.lines();
|
||
while let Ok(Some(line)) = lines.next_line().await {
|
||
if line.trim().is_empty() { continue; }
|
||
if let Ok(msg) = serde_json::from_str::<Value>(&line) {
|
||
// 参考 acpClient.ts line 120-180 (onData 解析逻辑)
|
||
if let Some(id) = msg.get("id").and_then(|v| v.as_u64()) {
|
||
// 响应 → 匹配 pending
|
||
if let Some(tx) = p.lock().unwrap().remove(&id) {
|
||
let _ = tx.send(Ok(msg));
|
||
}
|
||
} else if let Some(method) = msg.get("method").and_then(|v| v.as_str()) {
|
||
// 通知 → 调用 onNotification
|
||
}
|
||
}
|
||
}
|
||
});
|
||
Ok(Self { child, writer, pending, next_id: AtomicU64::new(1) })
|
||
}
|
||
|
||
/// Send JSON-RPC request, await response
|
||
/// 参考: acpClient.ts line 95-110 (sendRequest)
|
||
pub async fn request<P: Serialize, R: DeserializeOwned>(
|
||
&self, method: &str, params: P
|
||
) -> Result<R> {
|
||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||
self.pending.lock().unwrap().insert(id, tx);
|
||
|
||
let req = json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params });
|
||
self.writer.write_all(format!("{}\n", serde_json::to_string(&req)?).as_bytes()).await?;
|
||
self.writer.flush().await?;
|
||
|
||
match rx.await {
|
||
Ok(Ok(val)) => serde_json::from_value(val).map_err(Into::into),
|
||
_ => Err(AcpError::Timeout),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**工作量:** ~200 行 Rust。核心逻辑直接映射自 hermes-vscode-main 的 `acpClient.ts`。
|
||
|
||
### 6.2 ACP Session Manager (`acp_session_manager.rs`)
|
||
|
||
**参考:** `hermes-vscode-main/src/sessionManager.ts`
|
||
|
||
```rust
|
||
struct AcpSession {
|
||
id: String,
|
||
runtime: AcpRuntimeConfig,
|
||
client: AcpClient,
|
||
state: SessionState,
|
||
run_handle: Option<JoinHandle<()>>,
|
||
}
|
||
|
||
pub struct AcpSessionManager {
|
||
runtimes: HashMap<String, AcpRuntimeConfig>,
|
||
sessions: HashMap<String, AcpSession>,
|
||
active_profile: String,
|
||
}
|
||
```
|
||
|
||
接口:
|
||
|
||
| 方法 | 对应 ACP | 参考 |
|
||
|---|---|---|
|
||
| `create_session(profile, page_context)` | `session/new` | sessionManager.ts `start()` / `sendPrompt()` |
|
||
| `run_prompt(session_id, prompt)` | `session/prompt` | sessionManager.ts `sendPrompt()` |
|
||
| `cancel(session_id)` | `session/cancel` | sessionManager.ts `cancel()` |
|
||
| `on_update(handler)` | `session/update` | sessionManager.ts `handleUpdate()` |
|
||
| `switch_runtime(profile)` | — | 切换 `active_profile`,刷新 runtime |
|
||
|
||
**工作量:** ~300 行 Rust。
|
||
|
||
### 6.3 Reasonix ACP wrapper (`reasonix-acp-wrapper.mjs`)
|
||
|
||
**参考:** `DeepSeek-Reasonix-main/src/cli/commands/acp.ts`
|
||
|
||
```typescript
|
||
// 参考 acp.ts 的 acpCommand() 函数 — ~147 行核心逻辑
|
||
// 1. 创建 AcpServer
|
||
// 2. 注册 mnote 工具 (调 mnote-web HTTP)
|
||
// 3. onRequest("session/new") → 创建 CacheFirstLoop
|
||
// 4. onRequest("session/prompt") → loop.run() + dispatchKernelEvent
|
||
// 5. onNotification("session/cancel") → aborter.abort()
|
||
```
|
||
|
||
**工作量:** ~150 行 TypeScript。直接改编自 `acp.ts` 已有代码。
|
||
|
||
### 6.4 桥接层 — ACP → SSE
|
||
|
||
**参考:** `hermes-vscode-main/src/sessionManager.ts` 的 `handleUpdate()` 方法
|
||
|
||
当前 `stream_events` 输出 SSE。改为 ACP 后:
|
||
|
||
```rust
|
||
// 在 new AcpSessionManager().on_update() 中
|
||
fn on_acp_update(update: SessionUpdateParams) -> Option<SseEvent> {
|
||
match update.update.sessionUpdate {
|
||
"agent_message_chunk" => Some(SseEvent {
|
||
event: "message.delta",
|
||
data: json!({ "delta": extract_text(&update) }),
|
||
}),
|
||
"tool_call" => Some(SseEvent {
|
||
event: "tool.started",
|
||
data: json!({ "tool": update.title, "kind": update.kind }),
|
||
}),
|
||
// ... 其余事件映射
|
||
}
|
||
}
|
||
```
|
||
|
||
**工作量:** ~60 行 Rust。
|
||
|
||
### 6.5 前端运行时选择器
|
||
|
||
AiAgentPanel 增加下拉框 + 切换逻辑:
|
||
|
||
```tsx
|
||
<select value={activeProfile} onChange={switchProfile}>
|
||
<option value="default">Hermes(默认)</option>
|
||
<option value="reasonix">Reasonix(缓存优先)</option>
|
||
</select>
|
||
```
|
||
|
||
`switchProfile` 调 `PUT /api/hermes/client/profiles/active`(已有接口)。
|
||
|
||
**工作量:** ~50 行 TypeScript。
|
||
|
||
---
|
||
|
||
## 7. 与现有设计的关系
|
||
|
||
### 7.1 对 7-5 (Hermes client proxy 合同) 的影响
|
||
|
||
7-5 规定的路由路径不变:
|
||
|
||
| 路由 | 当前实现 | ACP 后 |
|
||
|------|----------|--------|
|
||
| `GET /client/sessions` | Hermes HTTP proxy | ACP `session/new` 历史 |
|
||
| `POST /client/sessions` | 本地生成 sessionId | 本地生成 + ACP `session/new` |
|
||
| `POST /client/runs` | Hermes HTTP run | ACP `session/prompt` |
|
||
| `GET /client/events/{runId}` | Hermes SSE 流 | ACP `session/update` → SSE |
|
||
| `POST /client/runs/{runId}/abort` | Hermes HTTP abort | ACP `session/cancel` |
|
||
|
||
前端看到的 HTTP 接口不变,后端实现透明切换。
|
||
|
||
### 7.2 对 7-14 (markdown 编辑收敛) 的影响
|
||
|
||
7-14 的最新口径是:local-first 普通 Markdown 编辑优先走“授权文件引用 + agent 原生 patch/diff”,`mnote.doc.markdown_edit` / `mnote.block.*` 退到兼容与辅助层。ACP 只是换掉 driver(从 Hermes 换成 Reasonix),不改变这个权限与执行边界。
|
||
|
||
### 7.3 对 `page_ai_workflow.rs` 的影响
|
||
|
||
最新口径下,`block_edit_workflow` 只保留为兼容门面;local-first 普通正文编辑不应再依赖它。ACP 主要服务 agent runtime 选择、权限隔离、事件桥接和审计。
|
||
|
||
---
|
||
|
||
## 8. 风险与缓解
|
||
|
||
| 风险 | 概率 | 缓解 |
|
||
|------|------|------|
|
||
| Reasonix ACP server 的 tool call 调用 mnote-web HTTP 有延迟 | 中 | 工具调用走 localhost TCP,延迟 <1ms |
|
||
| Reasonix `CacheFirstLoop` 与 mnote 页面上下文的兼容性 | 低 | ACP `session/new` 传 `pageContext`,system prompt 在 Reasonix wrapper 中注入 |
|
||
| 两个运行时并行维护增加心智负担 | 中 | 过渡期后退役旧 Hermes HTTP proxy,只保留 ACP |
|
||
| ACP 协议字段差异(Hermes vs Reasonix camelCase/snake_case) | 低 | 在桥接层做一次字段映射即可 |
|
||
| Task 7-12 说「mnote 不再建设独立 AI agent runtime」 | 不冲突 | ACP 层不是 agent runtime,是 runtime 抽象接口。mnote 仍然不建设 runtime,只是可以选接不同的 runtime |
|
||
|
||
---
|
||
|
||
## 9. 开放问题
|
||
|
||
- Reasonix 的 `DeepSeekClient` 需要的 API key 如何注入?环境变量?mnote-web 配置?
|
||
- **已决定**:通过 `reasonix-acp-wrapper.mjs` 的环境变量 `DEEPSEEK_API_KEY` 注入
|
||
- 本地 `.md` 文件的 tool 实现(`mnote.doc.fetch` 的本地变体)是否也在同一套 ACP 中?
|
||
- Phase C(流式 review/apply)的审批事件(`session/request_permission`)是否需要先加入 ACP 层?当前跳过,等 Phase C 再扩展。
|
||
|
||
---
|
||
|
||
## 11. 当前实现状态
|
||
|
||
### ✅ 已完成(2026-05-17)
|
||
|
||
#### Phase A — Rust ACP 基础设施
|
||
|
||
| Step | 文件 | 状态 | 测试 |
|
||
|------|------|------|------|
|
||
| 3 | `acp_client.rs` | 编译通过,~487 行 | 6 单元测试通过,含 `initialize` 握手 |
|
||
| 4 | `acp_types.rs` | 编译通过,显式按 `sessionUpdate` 判别 | 8 序列化/反序列化测试通过,含 `agent_thought_chunk` 不误判为 message |
|
||
| 5 | `acp_session_manager.rs` | 编译通过,~530 行 | 5 测试通过(含事件派发、文本去重、thought → `ThoughtDelta`) |
|
||
| 6 | `acp_runtime.rs` | 编译通过,~330 行 | 6 测试通过(含真实 Hermes CLI 连接) |
|
||
| 7 | `acp_bridge.rs` | 编译通过,~260 行 | ACP→SSE 事件映射,含 `ThoughtDelta` → `thought.delta` 单测 |
|
||
| 8 | `lib.rs` 模块注册 | 编译通过 | 5 个 ACP 模块声明 |
|
||
| 9 | `AppState` 集成 | 编译通过 | `AcpRuntimeManager` 挂入 `AppState` |
|
||
|
||
#### Phase B — 后端集成
|
||
|
||
| Step | 文件 | 改动 | 验证 |
|
||
|------|------|------|------|
|
||
| 7 | `hermes_client.rs` | `create_run` ACP 分支、`acp_stream_events()` SSE 端点、`is_acp_profile()` 检测 | e2e confirmed |
|
||
| 12 | `hermes_client.rs` | Profile 扩展 `configured_runtime_for_profile()` | — |
|
||
| 12 | `hermes_client.rs` | `/api/hermes/client/profiles` 返回 `acpRuntimes` 数组(含 model/preset/apiKeyConfigured) | API confirmed |
|
||
| 13 | `hermes_client.rs` | `configured_upstream_for_profile()` 标 `#[deprecated]`(后因 caller warning 移除) | — |
|
||
|
||
#### Phase C — Reasonix ACP Wrapper
|
||
|
||
| Step | 文件 | 状态 | 说明 |
|
||
|------|------|------|------|
|
||
| 10 | `scripts/reasonix-acp-wrapper.mjs` | ~400 行 | 自包含 NDJSON JSON-RPC 2.0 服务器,无依赖 `AcpServer` |
|
||
| — | API key 加载 | `loadApiKey()` 先读 `DEEPSEEK_API_KEY`,再读 `~/.reasonix/config.json` 的 `apiKey`,最后兼容 `~/.reasonix/config.yaml` | 对齐 Reasonix CLI 当前配置路径,同时保留旧 fallback |
|
||
| — | 工具注册修复 | ToolRegistry 使用 `fn` 字段;Reasonix 工具名用 `mnote_doc_fetch` / `mnote_doc_markdown_edit` 安全别名,再映射到 mnote-web 的 `mnote.doc.*` HTTP 工具 | 对齐 `DeepSeek-Reasonix-main/src/tools.ts`,避免 dotted tool name 与错误 `call` 字段导致工具不可调 |
|
||
| — | 事件处理修复 | `ev.role` 替代 `ev.type` | CacheFirstLoop 的 LoopEvent 使用 `role` 字段(`assistant_delta`/`assistant_final`/`done`/`tool_call_delta`/`tool_start`/`tool`/`error`/`warning`/`status`) |
|
||
| — | Reasoning / final 分流 | `reasoningDelta` 只发 `agent_thought_chunk`,不计入 assistant 正文输出;`assistant_final.content` 仍可在无 delta 正文时补发 | 避免只收到 reasoning 后吞掉最终正文 |
|
||
|
||
#### Phase D — 前端集成(Rust SSR)
|
||
|
||
| 改动 | 文件 | 说明 |
|
||
|------|------|------|
|
||
| ACP 下拉选择器 | `layout.rs` | Agent 标签页新增 `<select data-page-ai-acp-runtime>`;2026-05-18 起只保留 ACP · Hermes / ACP · Reasonix,移除“默认 (Hermes HTTP)”选项 |
|
||
| 状态存储 | `layout.rs` | `pageAiAcpRuntime` + `pageAiAcpRuntimes` 从 `/api/hermes/client/profiles` 加载 |
|
||
| 运行时切换 | `layout.rs` | `acpRuntime` 只表示运行时/传输层;Hermes ACP 继续保留当前 Hermes profile,Reasonix ACP 使用 `profile=reasonix` |
|
||
| UI 自适应 | `layout.rs` | ACP Hermes 模式下继续显示 Hermes profile 下拉;ACP Reasonix 模式下隐藏 Hermes profile 下拉;Agent 面板显示 ACP 配置 |
|
||
| Subtitle 更新 | `layout.rs` | 选择 ACP 后标题栏显示 `ACP · Reasonix` 或 `ACP · Hermes` |
|
||
|
||
#### Phase E — 浏览器验证 Skill
|
||
|
||
| 文件 | 状态 | 说明 |
|
||
|------|------|------|
|
||
| `/home/lix/.codex/skills/page-ai-browser-verify/SKILL.md` | ✅ 已创建 | 固化页面 AI 浏览器验证流程,后续遇到 ACP Hermes / ACP Reasonix / 回复质量问题时复用 |
|
||
| `/home/lix/.codex/skills/page-ai-browser-verify/scripts/verify_mnote_page_ai_acp.js` | ✅ 已创建 | 真实登录、创建临时页、切换 ACP Hermes/Reasonix、捕获 `/api/hermes/client/runs`、截图,并断言 assistant 正文去空白后严格等于 marker |
|
||
|
||
验证命令:
|
||
|
||
```bash
|
||
node /home/lix/.codex/skills/page-ai-browser-verify/scripts/verify_mnote_page_ai_acp.js
|
||
```
|
||
|
||
最新证据(2026-05-17):
|
||
|
||
| 项 | 路径 |
|
||
|---|---|
|
||
| 结构化结果 | `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/result.json` |
|
||
| ACP Hermes 截图 | `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/01-acp-hermes-reply.png` |
|
||
| ACP Reasonix 截图 | `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/02-acp-reasonix-reply.png` |
|
||
|
||
验收标准已收紧:不能只判断 UI “包含 marker”;必须确认最终 assistant 正文是干净最终答案,不能出现推理解释、工具说明、`Theuserisasking...` 这类 glued reasoning 文本。
|
||
|
||
### 🐛 问题与解决状态(2026-05-17)
|
||
|
||
#### 问题 1:ACP Reasonix SSE 流返回空(`stop_reason=Error`)
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **状态** | ✅ 已修复;后端 SSE 探针与前端真实浏览器验证均通过 |
|
||
| **原现象** | `ACP prompt completed: stop_reason=Error`,耗时 ~300ms,SSE 流无任何 `message.delta` 事件 |
|
||
| **已排除** | ✅ API key 有效(`CacheFirstLoop.step("hi")` 直接调用正常,LLM 返回中文回复) |
|
||
| **已排除** | ✅ Wrapper 语法正确(`node --check` 通过,`initialize` + `session/new` 验证通过) |
|
||
| **已排除** | ✅ SSE 转发管道 race condition(频道建立已移至 prompt 启动前) |
|
||
| **已排除** | ✅ `message` 字段名匹配(`payload.get("message")` 修正) |
|
||
| **根因** | `scripts/reasonix-acp-wrapper.mjs` 与 Reasonix 当前 API 不匹配:`ToolRegistry.register()` 需要 `fn` 而不是 `call`;`CacheFirstLoop.step()` 产出的是 `ev.role`,不是旧的 `ev.type`;本机 Reasonix API key 存在 `~/.reasonix/config.json`,旧 wrapper 只读 YAML |
|
||
| **已修复** | wrapper 改为读取 `config.json`,工具注册改为 `fn`,工具名改为 Reasonix 安全别名,事件映射覆盖 `assistant_delta`、`assistant_final`、`done`、`tool_call_delta`、`tool_start`、`tool`、`error`、`warning`、`status`;`reasoningDelta` 只作为 thought,不作为 assistant 正文输出计数 |
|
||
| **验证** | `node --check scripts/reasonix-acp-wrapper.mjs` 通过;`cargo test -p mnote-web acp -- --nocapture` 29 个测试通过;直接 JSON-RPC 探针 `initialize → session/new → session/prompt` 返回 `stopReason=end_turn`;3000 后端 SSE 探针 `ACP Reasonix` 收到 `message.delta` + `run.completed`;真实浏览器验证见 `tmp/page-ai-acp-browser-UAYwyM/` |
|
||
|
||
#### 问题 1b:ACP Hermes profile 选择丢失
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **状态** | ✅ 已修复,后端真实 SSE 探针已通过 |
|
||
| **原现象** | 选择 `ACP · Hermes` 后,前端把 `pageAiAcpRuntime` 当成 `profile` 发送,导致 profile 固定为 `hermes`,无法沿用既有 Hermes profile 选择 |
|
||
| **根因** | `profile` 与 `acpRuntime` 两个概念混用:`profile` 应表示 Hermes agent/profile(如 `default`、`mnoteai`),`acpRuntime` 才表示运行时传输层(`hermes` / `reasonix`) |
|
||
| **已修复** | 前端 `create_run` 发送 `{ profile: pageAiRunProfile(), acpRuntime }`;ACP Hermes 保留 profile 下拉;后端按 `acpRuntime` 进入 ACP 分支,并按本次 `profile` 启动 `hermes -p <profile> acp` |
|
||
| **兼容处理** | ACP Hermes subprocess 会从所选 profile 的 `model.api_key` / `providers.<provider>.api_key` / `key_env` 注入 provider key 环境,避免旧 gateway 能读 profile key、ACP subprocess 却读不到的问题 |
|
||
| **验证** | 3000 后端 SSE 探针:`ACP Hermes + default profile`、`ACP Hermes + mnoteai profile` 均收到 `message.delta` + `run.completed`,没有 `run.failed`;浏览器请求体确认 Hermes 为 `{ profile: "mnoteai", acpRuntime: "hermes" }` |
|
||
|
||
#### 问题 1c:浏览器截图显示 reasoning / thought 被当作最终回复
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **状态** | ✅ 已修复,严格浏览器验证通过 |
|
||
| **原现象** | 旧截图 `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-mp9ofvbc/01-acp-hermes-reply.png` 中,AI 气泡显示 `Theusersentabrowserverificationstring...`;Reasonix 旧 `result.json` 也出现 `Theuserisaskingmetorespond...ACP_REASONIX...`,说明上次只检查“包含 marker”的验收标准不合格 |
|
||
| **根因** | Rust `SessionUpdate` 使用 `#[serde(untagged)]`,但 `AgentMessageChunk` 与 `AgentThoughtChunk` 字段形状相同(`sessionUpdate: String` + `content`),serde 会按枚举顺序先匹配 `AgentMessageChunk`,导致 `agent_thought_chunk` 被误转成 `message.delta` |
|
||
| **已修复** | `acp_types.rs` 改为自定义 `Deserialize`,显式读取 `sessionUpdate` 后匹配 `agent_message_chunk` / `agent_thought_chunk` / tool / usage / plan 等变体;`acp_session_manager.rs` 补 `agent_thought_chunk` → `ThoughtDelta` 单测;`acp_bridge.rs` 补 `ThoughtDelta` → `thought.delta` 单测 |
|
||
| **验证** | `cargo test -p mnote-web acp -- --nocapture`:29 passed;浏览器 skill 严格断言 Hermes / Reasonix 最新 assistant 正文分别严格等于 `ACP_HERMES_BROWSER_OK_mp9pfqf6`、`ACP_REASONIX_BROWSER_OK_mp9pfqf6`;截图见 `tmp/page-ai-acp-browser-UAYwyM/` |
|
||
| **后续规则** | 页面 AI 浏览器验证必须同时看截图与正文断言;不能只用 DOM 包含 marker 作为通过条件 |
|
||
|
||
#### 问题 2:Hermes HTTP 路径 502
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **现象** | `Hermes upstream 连接失败: error sending request for url (http://127.0.0.1:8644/v1/runs)` |
|
||
| **判断** | 这是旧 Hermes HTTP proxy 路径的环境/profile 配置问题,不是 ACP Reasonix wrapper 问题 |
|
||
| **原因** | `configured_upstream_for_profile()` 优先读取 `MNOTE_WEB_HERMES_UPSTREAM_URL` 或 profile 的 `API_SERVER_PORT`,当前解析到了 8644;实际 Hermes gateway 端口应与本机服务一致(设计稿预期为 8642) |
|
||
| **解决** | 只读确认当前 mnote-web 启动环境和 Hermes profile 配置;将 `MNOTE_WEB_HERMES_UPSTREAM_URL` 或对应 profile `API_SERVER_PORT` 调整到实际 gateway 端口;不要在 ACP runtime 层硬编码端口 |
|
||
|
||
#### 问题 3:ACP 模式 Skills 面板内容区分(次要项)
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **状态** | 🟡 次要项;不阻塞当前首要目标(ACP Hermes / ACP Reasonix 正常回复) |
|
||
| **纠正** | Reasonix skills 不是空态;本机真实目录包含 `/home/lix/.reasonix/skills` |
|
||
| **当前处理** | 后端 Reasonix skills 源应按 runtime 维度扫描:`/mnt/Data1T/mnote/.reasonix/skills`、`/mnt/Data1T/mnote/.agents/skills`、`/home/lix/.reasonix/skills`、`/home/lix/.agents/skills` |
|
||
| **边界** | Skills 展示只是可见信息;当前还不能据此认为这些 skills 都已经注入 Reasonix ACP runtime 的工具系统 |
|
||
| **下一步** | 后续若要把 Reasonix skills 变成可执行能力,需要明确 Reasonix skill schema → ToolRegistry 注册规则;当前优先保持只读展示与不误导 |
|
||
|
||
#### 问题 4:ACP Hermes 未复用 soul/user/memory 可编辑 UI
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **用户反馈** | ACP Hermes 页面中看不到 Hermes HTTP 已有的 `SOUL.md` / `USER.md` / `MEMORY.md` 编辑卡片 |
|
||
| **根因** | `layout.rs` 在 `isAcp` 分支把 Agent 面板替换成 runtime info 卡片,并且 `if (agentPanel && !isAcp)` 才渲染 memory 编辑器;因此 ACP Hermes 被误归入 Reasonix 风格的 runtime 信息态 |
|
||
| **参考结论** | `hermes-vscode-main` 本身没有 soul/user/memory 模块;这部分应复用 mnote 现有 Hermes HTTP BFF:`/api/hermes/client/profile-memory` |
|
||
| **已修复** | ACP Hermes (`pageAiAcpRuntime === "hermes"`) 继续显示与 Hermes HTTP 相同的三张可编辑 memory 卡片;ACP Reasonix 仍显示 runtime info;Hermes profile 下拉继续保留 |
|
||
| **边界** | 未额外把 memory 文本硬塞入 ACP prompt,避免和 `hermes -p <profile> acp` 自身 profile 加载逻辑重复;当前需求以 UI 可见、可编辑、可保存为准 |
|
||
|
||
#### 问题 5:页面 AI 会话历史刷新丢失
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **用户反馈** | 当前会话历史刷新后没有了,不清楚真源在哪里 |
|
||
| **根因** | 前端运行期真源是 `pageUiState.pageAiSessions/pageAiMessages`;旧 `localStorage` 只保存 `activeSessionId/activeProfileName`,不保存 `messages`;ACP run 也未落 Hermes HTTP session export |
|
||
| **参考结论** | `hermes-vscode-main/src/sessionStore.ts` 把 `ChatSession[]`、`messages`、`acpSessionId` 存在 VSCode `workspaceState`,最多 20 个 session、每 session 300 条消息 |
|
||
| **已修复** | 短期真源明确为“按文档隔离的浏览器 localStorage”:`hermes_page_ai_session:<documentId>` 现在保存 `version/activeSessionId/activeProfileName/activeAcpRuntime/sessions/messages`,每 session 最多 300 条消息 |
|
||
| **验证方法** | 浏览器验证 skill 已增加 Hermes 回复后 reload,再打开页面 AI,确认 marker 回复仍可见 |
|
||
| **长期方案** | 后续应把页面 AI 会话统一落后端 session store,并让 ACP session id 支持 `session/load` 恢复;localStorage 只保留本地缓存与离线恢复 |
|
||
|
||
#### 问题 6:ACP 工具调用信息和流式输出不完整
|
||
|
||
| 项 | 详情 |
|
||
|---|---|
|
||
| **用户反馈** | 与 `hermes-vscode-main` 相比,缺工具调用信息(需可折叠、默认折叠),缺流式输出 |
|
||
| **根因** | ACP `message.delta` 已到前端,但旧 UI 只累加到局部 `assistantText`,stream 完成后才 push assistant message;工具事件虽然会生成 tool message,但卡片是普通 div,未折叠,且 ACP bridge 丢掉了 `rawInput/content/status` |
|
||
| **参考结论** | `hermes-vscode-main` 的关键是协议层 text dedup、UI 层 `pendingText/currentAgentText` 流式占位、`data-tool-id` 原地更新工具状态;参考实现本身不是折叠 UI,因此 mnote 在其基础上新增 `<details>` |
|
||
| **已修复** | 前端收到首个 `message.delta` 时创建 `streaming=true` assistant 消息并持续更新;完成后标记为普通 assistant 消息并持久化;工具卡改为 `<details>`,默认折叠,summary 显示工具名/状态/kind/call id,展开显示参数/结果/trace |
|
||
| **后端补齐** | `AcpSessionEvent::ToolCall` 保留 `raw_input`,`ToolCallUpdate` 保留 `content`;`acp_bridge` SSE 输出 `status/input/output`,让前端可展示工具参数和结果摘要 |
|
||
| **验证** | `cargo test -p mnote-web acp -- --nocapture` 已覆盖 `acp_tool_events_keep_detail_for_collapsible_ui`;浏览器验证 skill 会记录 `sawStreaming` 并保留截图 |
|
||
|
||
### 📋 待完成
|
||
|
||
| Step | 工作 | 前置 | 估算 |
|
||
|------|------|------|------|
|
||
| — | 旧 Hermes HTTP proxy 502 环境配置确认 | 非 ACP 路径,仅影响旧 gateway | ~15min |
|
||
| — | ACP 模式 Skills 可执行注入设计(Reasonix skill schema → ToolRegistry) | 问题3 | ~半天 |
|
||
| 15 | 压力测试:多会话并发、进程管理稳定性 | Step 14 | ~半天 |
|
||
| 16 | 退役旧 HTTP proxy 代码 | Step 14 稳定后 | ~1 天 |
|
||
| 17 | 基准测试:Reasonix cache hit rate vs Hermes | Step 10 | ~半天 |
|
||
|
||
---
|
||
|
||
## 10. 详细执行 Checklist(顺序执行)
|
||
|
||
以下 checklist 按依赖关系排序,每个 step 标注了**参考文件**(可直接读的代码)、**产出文件**、**验证方法**。执行时从 step-1 开始,完成后由 AI 调用 `todo_write` 标记进度后进入下一步。
|
||
|
||
### [x] Step 1:读取参考代码,熟悉 ACP 协议细节
|
||
|
||
> ✅ 完成。已阅读 `acpClient.ts`(spawn/request/notification 模式)、`protocol.ts`(6 种 session/update 变体)、`acp.ts`(CacheFirstLoop + Eventizer 集成)。确认 ACP 使用 camelCase JSON 字段、NDJSON 流式传输。
|
||
|
||
| **参考** | `reference-code/hermes-vscode-main/src/acpClient.ts`、`reference-code/DeepSeek-Reasonix-main/src/acp/protocol.ts`、`reference-code/hermes-vscode-main/src/protocol.ts` |
|
||
|
||
### [x] Step 2:确认 `run_command` 的 cwd 与项目根一致
|
||
|
||
> ✅ 完成。cwd = `/mnt/Data1T/mnote`,所有路径相对此目录。
|
||
|
||
### [x] Step 3:创建 `acp_client.rs` — ACP JSON-RPC 2.0 客户端
|
||
|
||
> ✅ 完成。`rust/crates/mnote-web/src/acp_client.rs`(~487 行)。实现了 `AcpClient` 结构体,含 `spawn()`、`request()`、`notification()`、`on_notification()`、`close()`。后台 tokio task 处理 NDJSON 行读取,`pending: HashMap<u64, oneshot::Sender>` 路由响应。**6 个单元测试通过**(含 `initialize` 握手 mock)。
|
||
|
||
| **产出** | `rust/crates/mnote-web/src/acp_client.rs` |
|
||
|
||
### [x] Step 4:创建 ACP 协议类型定义 — `acp_types.rs`
|
||
|
||
> ✅ 完成。`rust/crates/mnote-web/src/acp_types.rs`(~440 行)。定义了 `InitializeParams/Result`、`SessionNewParams/Result`、`SessionPromptParams/Result`、`ContentBlock`(4 变体)、`SessionUpdate`(7 变体含 Unknown 兜底)。全部 camelCase JSON。**7 个序列化往返测试通过**。
|
||
|
||
| **产出** | `rust/crates/mnote-web/src/acp_types.rs` |
|
||
|
||
### [x] Step 5:创建 `acp_session_manager.rs` — 会话生命周期管理
|
||
|
||
> ✅ 完成。`rust/crates/mnote-web/src/acp_session_manager.rs`(~530 行)。`AcpSessionManager` 含 `create_session()` → ACP `session/new`、`run_prompt()` → ACP `session/prompt` + callback、`cancel()` → `session/cancel`。文本去重(4 种模式匹配)、事件枚举(7 种 variants)。**5 个单元测试通过**(含事件派发、去重逻辑、thought chunk 映射)。
|
||
|
||
| **产出** | `rust/crates/mnote-web/src/acp_session_manager.rs` |
|
||
|
||
### [x] Step 6:创建 `acp_runtime.rs` — 运行时管理
|
||
|
||
> ✅ 完成。`rust/crates/mnote-web/src/acp_runtime.rs`(~330 行)。`AcpRuntimeManager` 支持 `from_env()` 从环境变量配置、`switch_to(name)` 切换运行时、`health_check()` 5s 超时检测。Reasonix wrapper 路径通过 `CARGO_MANIFEST_DIR` 自动解析绝对路径。**6 个单元测试通过**(含真实 Hermes CLI 连接)。
|
||
|
||
| **产出** | `rust/crates/mnote-web/src/acp_runtime.rs` |
|
||
|
||
### [x] Step 7:集成 ACP Session Manager 到 Hermes routes
|
||
|
||
> ✅ 完成。`hermes_client.rs` 中:
|
||
> - `create_run`:新增 ACP 分支——`is_acp_profile()` 检测 → `register_acp_runtime()` → 存储 payload → 返回本地 runId
|
||
> - `stream_events`:新增 `acp_stream_events()` 函数——`AcpRuntimeManager::switch_to()` 激活运行时 → `AcpSessionManager::create_session()` + `run_prompt()` → broadcast → mpsc → SSE `Body`
|
||
> - `is_acp_profile("reasonix" | "hermes")` 返回 true
|
||
> - SSE 桥接:`acp_event_to_sse()` 映射 7 种 ACP 事件到 SSE 格式
|
||
> - 修复:`payload.get("message")` 替代错误的 `payload.get("input")`
|
||
> - 修复:SSE 转发管道先于 prompt 建立(消除 race condition)
|
||
|
||
| **产出** | `acp_bridge.rs`(~260 行)+ `hermes_client.rs` 修改 |
|
||
|
||
### [x] Step 8:编辑全局 Router 添加 ACP 模块
|
||
|
||
> ✅ 完成。`lib.rs` 中注册 `pub mod acp_client/ acp_types/ acp_session_manager/ acp_runtime/ acp_bridge`。
|
||
|
||
### [x] Step 9:AppState 改造 — 加入 AcpRuntimeManager
|
||
|
||
> ✅ 完成。`app.rs` 中 `AppState` 新增 `acp_runtime: Arc<AcpRuntimeManager>` 字段,`AppState::new()` 中初始化。
|
||
|
||
### [x] Step 10:创建 Reasonix ACP wrapper 脚本
|
||
|
||
> ✅ 完成。`scripts/reasonix-acp-wrapper.mjs`(~400 行)。自包含 NDJSON JSON-RPC 2.0 服务器(无依赖 `AcpServer`)。使用 Reasonix 公开 API:`CacheFirstLoop`、`DeepSeekClient`、`ToolRegistry`、`ImmutablePrefix`。注册 `mnote.doc.fetch` 和 `mnote.doc.markdown_edit` 工具(工具调用 HTTP mnote-web tool API)。
|
||
>
|
||
> 关键修复:
|
||
> - `ev.role` 替代错误的 `ev.type`(CacheFirstLoop 使用 `role` 字段)
|
||
> - `ToolRegistry.register()` 使用 `fn` 字段,不能使用旧 wrapper 里的 `call`
|
||
> - Reasonix 工具名使用安全别名 `mnote_doc_fetch` / `mnote_doc_markdown_edit`,再映射到 mnote-web 的 dotted tool name
|
||
> - `loadApiKey()` 优先从 `DEEPSEEK_API_KEY` / `~/.reasonix/config.json` 读取 API key,并兼容 `~/.reasonix/config.yaml`
|
||
> - `reasoningDelta` 只发 `agent_thought_chunk`,不计入 assistant 正文输出,避免 thought 先到后吞掉最终 `assistant_final.content`
|
||
> - 无输出检测——LLM 静默失败时返回友好错误消息
|
||
>
|
||
> 验证:`node --check` 通过,`initialize` + `session/new` + `session/prompt` ACP 探针验证通过;探针返回 `stopReason=end_turn`,产生 `agent_message_chunk`;浏览器验证中 ACP Reasonix 最终 assistant 正文严格等于 marker。
|
||
|
||
| **产出** | `scripts/reasonix-acp-wrapper.mjs` |
|
||
|
||
### [x] Step 11:添加运行时选择器的前端支持
|
||
|
||
> ✅ 完成(Rust SSR 侧)。`layout.rs` 中:
|
||
> - Agent 标签页新增 `<select data-page-ai-acp-runtime>` 下拉框(3 选项:默认/ACP·Hermes/ACP·Reasonix)
|
||
> - 状态:`pageAiAcpRuntime` + `pageAiAcpRuntimes`(从 `/api/hermes/client/profiles` 的 `acpRuntimes` 字段加载)
|
||
> - `acpRuntime` 与 `profile` 分离:ACP Hermes 保留 Hermes profile 下拉,ACP Reasonix 隐藏 Hermes profile 下拉
|
||
> - ACP Hermes 复用 Hermes HTTP 的 `SOUL.md` / `USER.md` / `MEMORY.md` 可编辑 UI;ACP Reasonix 保留 runtime info 卡片
|
||
> - 页面 AI 会话历史短期真源为按文档隔离的 localStorage,保存 session 列表、messages、active profile、active ACP runtime
|
||
> - `message.delta` 进入正在输出的 assistant bubble;工具调用卡片使用 `<details>` 默认折叠展示参数/结果/trace
|
||
> - 标题栏更新为 `ACP · Reasonix` 或 `ACP · Hermes`
|
||
> - `create_run` 同时发送 `profile` 与 `acpRuntime`;后端用 `acpRuntime` 判断是否走 ACP,用 `profile` 选择 Hermes profile
|
||
>
|
||
> 注:Thought Delta 可视化渲染尚未实现,当前阶段允许页面不展示 thought;硬要求是 `agent_thought_chunk` 不能进入最终 assistant 正文。
|
||
|
||
| **产出** | `layout.rs` 修改 |
|
||
|
||
### [x] Step 12:profile 扩展 — 从 upstream URL 改为 runtime 配置
|
||
|
||
> ✅ 部分完成。
|
||
> - `acpRuntime` payload 字段识别 `"reasonix"` 和 `"hermes"` 两个 ACP runtime;旧的 `profile=reasonix/hermes` 仍兼容
|
||
> - `configured_runtime_for_profile(profile)` 返回对应的 runtime 名称
|
||
> - `/api/hermes/client/profiles` 响应新增 `acpRuntimes` 数组(含 model/preset/apiKeyConfigured/description)
|
||
> - 向后兼容:非 ACP profile 继续使用原有的 Hermes HTTP proxy 路径
|
||
> - ACP Hermes 按本次选择的 profile 启动 `hermes -p <profile> acp`,并注入 profile provider key 环境
|
||
>
|
||
> 待完成:`gateway_health` 适配 ACP runtime health check、`MNOTE_WEB_ACP_DEFAULT_RUNTIME` 环境变量支持。
|
||
|
||
| **产出** | `hermes_client.rs` 修改 |
|
||
|
||
### [x] Step 13:Hermes HTTP proxy 代码标为 deprecated
|
||
|
||
> ✅ 完成。`configured_upstream_for_profile()` 添加了 `#[deprecated]`,后因调用处 warning 过多而移除标记(待 Step 16 时一次性删除)。
|
||
|
||
### [x] Step 14:前端运行时切换验证 e2e
|
||
|
||
> ✅ 已通过。后端真实 SSE 探针与浏览器 UI 严格验证均通过。
|
||
>
|
||
> 已通过的验证:
|
||
> - `POST /api/hermes/client/runs` with `{ profile: "default", acpRuntime: "hermes" }` → ACP Hermes 路径返回 `runId`,SSE 收到 `message.delta`
|
||
> - `POST /api/hermes/client/runs` with `{ profile: "mnoteai", acpRuntime: "hermes" }` → ACP Hermes 按 mnoteai profile 启动并收到 `message.delta`
|
||
> - `POST /api/hermes/client/runs` with `{ profile: "reasonix", acpRuntime: "reasonix" }` → ACP Reasonix 路径返回 `runId`,SSE 收到 `message.delta`
|
||
> - `CacheFirstLoop.step("hi")` 直接调用 → LLM 返回正确中文回复
|
||
> - Wrapper `initialize` + `session/new` → 握手成功
|
||
> - Wrapper `session/prompt` → 返回 `stopReason=end_turn`,产生 `agent_message_chunk`
|
||
> - 浏览器验证 skill:创建临时页面,切换 `ACP · Hermes` + `mnoteai` profile,发送 marker prompt,最终 assistant 正文严格等于 `ACP_HERMES_BROWSER_OK_mp9pfqf6`
|
||
> - 浏览器验证 skill:切换 `ACP · Reasonix`,发送 marker prompt,最终 assistant 正文严格等于 `ACP_REASONIX_BROWSER_OK_mp9pfqf6`
|
||
> - 2026-05-17 追加:浏览器验证 skill 已扩展,检查 ACP Hermes memory 编辑器、Hermes 回复 reload 后历史恢复,并记录流式占位证据 `sawStreaming`
|
||
> - 2026-05-17 追加验证通过:ACP Hermes / ACP Reasonix 回复均严格等于 marker;两者 `sawStreaming=true`;Hermes 回复刷新后仍可见
|
||
>
|
||
> 证据:
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/result.json`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/01-acp-hermes-reply.png`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/02-acp-reasonix-reply.png`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/result.json`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/00-acp-hermes-memory-ui.png`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/01-acp-hermes-reply.png`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/01b-acp-hermes-history-after-reload.png`
|
||
> - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/02-acp-reasonix-reply.png`
|
||
>
|
||
> 仍未纳入本阶段:`thought.delta` 可视化渲染、`usage.updated` 展示。当前页面不显示 thought 是可接受行为;关键是 thought 不能误进 `message.delta`。
|
||
|
||
### [ ] Step 15:压力测试 — 确认同时多会话稳定性
|
||
|
||
> 待 Step 14 通过后执行。
|
||
|
||
### [ ] Step 16:退役旧的 Hermes HTTP proxy 代码
|
||
|
||
> 2026-05-18 已开始执行第一阶段退役:
|
||
>
|
||
> - 页面 AI 前端默认 `acpRuntime=reasonix`,不再以空 runtime 表示“默认 Hermes HTTP”;用户仍可在下拉中切换到 `ACP · Hermes`。
|
||
> - mnote-web 服务端默认把 `/api/hermes/client/runs` 的空 `acpRuntime` 归入 ACP 默认 runtime(默认 `reasonix`),避免继续落到 `configured_upstream_for_profile()` 的 HTTP proxy 分支。
|
||
> - `GET /api/hermes/client/gateway/health` 在 ACP 默认路径下返回 ACP transport 状态,不再探测 `8642/8644` HTTP gateway。
|
||
> - 旧 Hermes HTTP proxy 合同与 `/v1/runs` 默认主链设计稿已移入 `recycle/design/07-ai/retired-http-hermes/`,避免干扰后续 ACP 主线判断。
|
||
>
|
||
> 剩余工作:删除或进一步隔离 `hermes_client.rs` 内的 HTTP proxy 兼容分支;当前阶段只保留显式兼容开关,避免一次性删除影响 profile/memory/tools 管理能力。
|
||
|
||
### [ ] Step 17:基准测试 — Reasonix 缓存收益量化
|
||
|
||
> 待 Step 14 通过后执行。测试场景已设计,指标定义明确。
|
||
|
||
---
|
||
|
||
## 附录:文件依赖关系图
|
||
|
||
```
|
||
step-3 acp_client.rs
|
||
│ depends on: none
|
||
▼
|
||
step-4 acp_types.rs step-6 acp_runtime.rs
|
||
│ depends on: none │ depends on: serde_json
|
||
▼ ▼
|
||
step-5 acp_session_manager.rs ←───────────┘
|
||
│ depends on: acp_client, acp_types, acp_runtime
|
||
▼
|
||
step-7 hermes_client.rs 改造
|
||
│ depends on: acp_session_manager
|
||
▼
|
||
step-8 mod.rs 添加模块
|
||
│ depends on: step-3,4,5,6
|
||
▼
|
||
step-9 AppState 改造
|
||
│ depends on: acp_runtime, acp_session_manager
|
||
▼
|
||
step-10 reasonix-acp-wrapper.mjs (独立, 可并行)
|
||
│ depends on: npm reasonix 包
|
||
▼
|
||
step-11 AiAgentPanel.tsx 修改 step-12 profile 扩展
|
||
│ depends on: step-7 │ depends on: hermes_client.rs
|
||
▼ ▼
|
||
step-13 标 deprecated (与 step-10 可并行)
|
||
│
|
||
▼
|
||
step-14 e2e 验证
|
||
│
|
||
├── step-15 压力测试
|
||
│
|
||
└── step-16 退役旧代码
|
||
│
|
||
└── step-17 基准测试
|
||
```
|
||
|
||
### 关键并行路径
|
||
|
||
```
|
||
step-3 ─→ step-5 ─→ step-7 ─→ step-11 ─→ step-14
|
||
↗
|
||
step-10 (Reasonix wrapper, 可并行)
|
||
|
||
step-4 ─→ step-5
|
||
|
||
step-6 ─→ step-5, step-9
|
||
```
|
||
|
||
### 每次 AI 执行前必须确认
|
||
|
||
1. `run_command pwd` → `/mnt/Data1T/mnote`(确认 cwd)
|
||
2. 写文件路径用相对路径 `rust/crates/...` 而非 `/mnt/Data1T/mnote/...`
|
||
3. 先 `search_content` / `read_file` 确认目标文件最新内容,避免 edit_file SEARCH 不匹配
|
||
4. 每个 step 完成后 `todo_write` 更新进度
|