866 lines
44 KiB
Markdown
866 lines
44 KiB
Markdown
# 7-15 [process] 页面 AI ACP Agent Runtime 统一抽象层 v1
|
||
|
||
> 创建时间:2026-05-17
|
||
>
|
||
> 当前状态:`PROCESS`
|
||
>
|
||
> 本稿目的:
|
||
> 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/design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md`
|
||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-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 (fast-path 块编辑)
|
||
└─ local_rule planner, 不经过 Hermes
|
||
```
|
||
|
||
**问题:**
|
||
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 (不变)
|
||
└─ fast-path 块编辑
|
||
```
|
||
|
||
ACP 是整个架构的支点——它是一个**开放协议**,不是某个产品的私有接口。
|
||
|
||
---
|
||
|
||
## 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.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 端点。
|
||
|
||
对于 Reasonix 作为 runtime 的场景,需要一个 Reasonix-side 的工具注册包装脚本,将 mnote 工具注册到 `ToolRegistry`:
|
||
|
||
```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。
|
||
|
||
### 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 确立的「两层操作模型」(`mnote.doc.markdown_edit` 主 + `mnote.block.*` 辅)不受影响——工具在 Rust 侧 `hermes_tools.rs` 实现不变。ACP 只是换掉了 driver(从 Hermes 换成 Reasonix),不改 driver 调用的工具。
|
||
|
||
### 7.3 对 `page_ai_workflow.rs` 的影响
|
||
|
||
不影响。`block_edit_workflow` 作为独立 fast-path 与 ACP 无关。
|
||
|
||
---
|
||
|
||
## 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)
|
||
|
||
| Step | 文件 | 状态 | 测试 |
|
||
|------|------|------|------|
|
||
| 3 | `acp_client.rs` | 编译通过,475 行 | 6 单元测试通过 |
|
||
| 4 | `acp_types.rs` | 编译通过,440 行 | 7 单元测试通过 |
|
||
| 5 | `acp_session_manager.rs` | 编译通过,~530 行 | 4 测试通过(含事件派发) |
|
||
| 6 | `acp_runtime.rs` | 编译通过,~330 行 | 5 测试通过,含真实 Hermes 连接测试 |
|
||
| 7 | `acp_bridge.rs` + `hermes_client.rs` 修改 | 编译通过 | 新增 `acp_stream_events()` SSE 端点 |
|
||
| 8 | `lib.rs` 模块注册 | 编译通过 | — |
|
||
| 9 | `AppState` 集成 | 编译通过 | — |
|
||
| 10 | `scripts/reasonix-acp-wrapper.mjs` | 已创建,~250 行 | 需安装 `npm install reasonix` 后测试 |
|
||
| 12 | Profile 扩展 `configured_runtime_for_profile()` | 编译通过 | — |
|
||
| 13 | HTTP proxy 标 `#[deprecated]` | 编译通过(6 个 warning) | — |
|
||
|
||
### 📋 待完成
|
||
|
||
| Step | 工作 | 前置 | 估算 |
|
||
|------|------|------|------|
|
||
| 11 | 前端运行时选择器(AiAgentPanel.tsx) | Step 7 | ~半天 |
|
||
| 14 | e2e 验证:Hermes ACP + Reasonix ACP | Step 10 | ~半天 |
|
||
| 15 | 压力测试:多会话、进程管理 | Step 14 | ~半天 |
|
||
| 16 | 退役旧 HTTP proxy 代码 | Step 14 稳定后 | ~1 天 |
|
||
| 17 | 基准测试:缓存收益量化 | Step 10 | ~半天 |
|
||
|
||
---
|
||
|
||
## 10. 详细执行 Checklist(顺序执行)
|
||
|
||
以下 checklist 按依赖关系排序,每个 step 标注了**参考文件**(可直接读的代码)、**产出文件**、**验证方法**。执行时从 step-1 开始,完成后由 AI 调用 `todo_write` 标记进度后进入下一步。
|
||
|
||
### Step 1:读取参考代码,熟悉 ACP 协议细节
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 确认 ACP JSON-RPC 协议的方法名、字段名、事件类型,确保后续实现与 Hermes/Reasonix 兼容 |
|
||
| **参考** | `reference-code/hermes-vscode-main/src/protocol.ts`(ACP 事件解析)、`reference-code/DeepSeek-Reasonix-main/src/acp/protocol.ts`(ACP 类型定义)、`reference-code/hermes-vscode-main/src/acpClient.ts`(ACP 客户端完整实现) |
|
||
| **产出** | 无代码产出,仅阅读确认 |
|
||
| **验证** | 能在脑中回答:`session/update` 有几种 `sessionUpdate` 变体?每个变体有哪些必选字段? `session/prompt` 的 params 结构是什么? |
|
||
|
||
### Step 2:确认 `run_command` 的 cwd 与项目根一致
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 确保后续所有文件操作路径正确,不再触发 sandbox 偏移 |
|
||
| **操作** | `run_command pwd` 确认输出为 `/mnt/Data1T/mnote` |
|
||
| **验证** | 输出包含 `/mnt/Data1T/mnote` |
|
||
|
||
### Step 3:创建 `acp_client.rs` — ACP JSON-RPC 2.0 客户端
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 实现通用的 ACP 协议传输层:spawn 子进程、读写 NDJSON、请求/响应/通知路由 |
|
||
| **参考** | `reference-code/hermes-vscode-main/src/acpClient.ts`(完整参考,~220 行):spawn 逻辑 (L50-80)、onData 解析 (L120-180)、sendRequest (L95-110)、sendNotification (L115-118) |
|
||
| **路径** | `rust/crates/mnote-web/src/acp_client.rs`(新建) |
|
||
| **结构** | `pub struct AcpClient { child, writer, reader, pending: HashMap<u64, OneshotSender>, next_id }` |
|
||
| **方法** | `spawn(bin, args) → Result`、`request(method, params) → Result<R>`、`notification(method, params)`、`on_notification(handler)`、`close()` |
|
||
| **参考字段映射** | JSON-RPC `id` → `pending` key;响应匹配 `id`;通知匹配 `method` 字段 |
|
||
| **细节** | stdin 用 `BufWriter`(行缓冲),stdout 用 `BufReader` + `lines()` 逐行读;后台 tokio task 处理 incoming 行;`request()` 返回 `oneshot::Receiver`;超时处理用 `tokio::time::timeout`(默认 5min) |
|
||
| **验证** | 单元测试:mock stdin/stdout 子进程,发送 `session/new` 请求,验证收到响应;发送 notification,验证 handler 被调用 |
|
||
|
||
### Step 4:创建 ACP 协议类型定义 — `acp_types.rs`
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | ACP 协议的 Rust 类型定义,序列化/反序列化用 serde |
|
||
| **参考** | `reference-code/DeepSeek-Reasonix-main/src/acp/protocol.ts`(ACP 类型定义,~80 行)、`reference-code/hermes-vscode-main/src/protocol.ts`(解析逻辑,~120 行) |
|
||
| **路径** | `rust/crates/mnote-web/src/acp_types.rs`(新建) |
|
||
| **结构** | `InitializeParams/Result`、`SessionNewParams/Result`、`SessionPromptParams/Result`、`SessionCancelParams`、`SessionUpdateParams`、`ContentBlock`(text/resource/image/audio)、`SessionUpdateKind`(enum 6 种变体) |
|
||
| **字段注意** | Hermes 和 Reasonix 都使用 **camelCase** JSON 字段(`sessionUpdate`、`toolCallId`),对应 `#[serde(rename_all = "camelCase")]` |
|
||
| **验证** | 单元测试:JSON 反序列化 `acp/dispatch.ts` 中的 `session/update` 案例、`session/prompt` 的请求/响应序列化-反序列化往返 |
|
||
|
||
### Step 5:创建 `acp_session_manager.rs` — 会话生命周期管理
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 管理 ACP 会话生命周期:session/new → session/prompt → session/cancel,将 `session/update` 事件派发给 mnote-web 各模块 |
|
||
| **参考** | `reference-code/hermes-vscode-main/src/sessionManager.ts`(完整参考,~294 行):SessionManager 类 (L37-294)、handleUpdate (L130-280)、sendPrompt (L80-128)、cancel (L280-294) |
|
||
| **路径** | `rust/crates/mnote-web/src/acp_session_manager.rs`(新建) |
|
||
| **结构** | `pub struct AcpSessionManager { runtimes: HashMap<String, AcpRuntimeConfig>, sessions: HashMap<String, AcpSession>, active_profile: String }`、`struct AcpSession { id, client, state, run_handle, page_context }` |
|
||
| **方法** | `create_session(profile, page_context) → sessionId`(调 ACP `session/new`)、`run_prompt(session_id, prompt_blocks, on_event) → JoinHandle`(调 ACP `session/prompt`,注册 `session/update` handler)、`cancel(session_id)`(调 ACP `session/cancel`)、`switch_runtime(profile_name)`(切换 active_profile,关闭旧 sessions,创建新 runtime)、`SessionUpdateKind` 到 `AcpSessionEvent` 的映射 |
|
||
| **事件映射** | `agent_message_chunk` → `AcpSessionEvent::TextDelta { text }`;`agent_thought_chunk` → `AcpSessionEvent::ThoughtDelta { text }`;`tool_call` → `AcpSessionEvent::ToolCall { id, title, kind, status }`;`tool_call_update` → `AcpSessionEvent::ToolCallUpdate { id, status, content? }`;`usage_update` → `AcpSessionEvent::UsageUpdate { used, size }`;`session_info_update` → `AcpSessionEvent::SessionInfo { title }` |
|
||
| **去重逻辑** | 参考 `hermes-vscode-main/src/protocol.ts` 的 `deduplicateChunk()` 函数——ACP 会重发完整文本作为可靠性 fallback,需检测并丢弃重复 |
|
||
| **验证** | 单元测试:构造 mock `AcpClient`,`create_session` → 验证发送 `session/new`;`run_prompt` → 验证发送 `session/prompt`;模拟 `session/update` notification,验证 `on_event` 回调被正确调用 |
|
||
|
||
### Step 6:创建 `acp_runtime.rs` — 运行时管理
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 管理 agent runtime 进程的 spawn、健康检查、自动重启 |
|
||
| **路径** | `rust/crates/mnote-web/src/acp_runtime.rs`(新建) |
|
||
| **结构** | `pub struct AcpRuntimeManager { runtimes: HashMap<String, AcpRuntimeConfig>, active: Mutex<Option<String>> }`、`pub struct AcpRuntimeConfig { name, bin, args, env }` |
|
||
| **方法** | `register_runtime(config)`、`spawn_runtime(name) → AcpClient`、`health_check(name) → bool`(spawn 进程 + 发送 `initialize` 请求,超时 5s)、`shutdown_runtime(name)`、`switch_to(name) → Result`(先 shutdown 当前 active,再 spawn 新的) |
|
||
| **配置来源** | 从环境变量 / 配置文件读取(`MNOTE_WEB_ACP_RUNTIMES` JSON),当前固定配置:`hermes` → `{ bin: "hermes", args: ["acp"] }`、`reasonix` → `{ bin: "node", args: ["reasonix-acp-wrapper.mjs"] }` |
|
||
| **profile 扩展** | 当前 `configured_upstream_for_profile()` 返回 Hermes HTTP URL;改为返回 `AcpRuntimeConfig`。已有 profile 系统(`active_profile_name`, `configured_upstream_for_profile`) 保持接口不变,内部实现切换 |
|
||
| **验证** | 运行 `hermes acp`(需本地安装),发送 `session/new` 验证返回 `sessionId`;无 Hermes 环境时 mock 子进程验证健康检查逻辑 |
|
||
|
||
### Step 7:集成 ACP Session Manager 到 Hermes routes
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 让 `hermes_client.rs` 的现有端点可以选择使用 ACP 而非 HTTP proxy |
|
||
| **参考** | `hermes_client.rs` 中现有 `create_run` / `stream_events` / `abort_run` |
|
||
| **操作** | 在 `hermes_client.rs` 中导入 `AcpRuntimeManager` 和 `AcpSessionManager`;当当前 profile 的 `runtime_type == "acp"` 时走 ACP 路径,否则走原有 HTTP proxy 路径;`create_run` → `acp_session_manager.run_prompt()`(替代 Hermes HTTP `POST /api/hermes/runs`);`stream_events` → 从 `AcpSessionManager` 的 `on_event` 回调中发出 SSE(替代 Hermes HTTP `GET /api/hermes/runs/{id}/events`);`abort_run` → `acp_session_manager.cancel()`(替代 Hermes HTTP `POST /api/hermes/runs/{id}/abort`) |
|
||
| **SSE 桥接函数** | 新增 `fn acp_event_to_sse(event: AcpSessionEvent) → Option<SseEvent>`。映射表:`TextDelta("msg")` → `{ event: "message.delta", data: { delta: "msg" } }`;`ThoughtDelta("t")` → `{ event: "thought.delta", data: { delta: "t" } }`(新增);`ToolCall("id","title","kind","pending")` → `{ event: "tool.started", data: { tool: "title", preview: null } }`;`ToolCallUpdate("id","completed",content)` → `{ event: "tool.completed", data: { tool: "...", duration: null } }`;`UsageUpdate(used,size)` → `{ event: "usage.updated", data: { used, size } }`(新增);`SessionInfo(title)` → 忽略(mnote 前端不需要) |
|
||
| **验证** | 用 `hermes acp`(本地已安装)做 e2e 测试:前端发消息 → ACP session/prompt → 收到 SSE 流 → 显示工具调用 → 显示最终回复 |
|
||
|
||
### Step 8:编辑全局 Router 添加 ACP 模块
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 让 ACP 模块被编译,各模块之间可引用 |
|
||
| **参考** | `rust/crates/mnote-web/src/routes/mod.rs` 中当前 Hermes routes 的注册方式 (nest at `hermes_base_path`) |
|
||
| **操作** | 在 `mod.rs` 中添加 `mod acp_client;`、`mod acp_types;`、`mod acp_session_manager;`、`mod acp_runtime;`;初始化时创建 `AcpRuntimeManager`,注册 Hermes runtime 和 Reasonix runtime(如果配置存在);将 `AcpRuntimeManager` 放入 `AppState` 或 `Extension` |
|
||
| **验证** | `cargo build` 通过 |
|
||
|
||
### Step 9:AppState 改造 — 加入 AcpRuntimeManager
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 让路由 handler 可以访问运行时管理器 |
|
||
| **参考** | `rust/crates/mnote-web/src/app.rs` 中的 `AppState` 结构 |
|
||
| **操作** | 在 `AppState` 中添加 `acp_runtime: Arc<AcpRuntimeManager>` 字段;`AppState::new()` 中根据配置注册 `hermes` 和/或 `reasonix` runtime |
|
||
| **验证** | `cargo build` 通过;health endpoint 返回 ACP runtime 状态 |
|
||
|
||
### Step 10:创建 Reasonix ACP wrapper 脚本
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 实现 Reasonix ACP server,让 mnote-web 可以 `spawn("node", ["reasonix-acp-wrapper.mjs"])` 连接 |
|
||
| **参考** | `reference-code/DeepSeek-Reasonix-main/src/cli/commands/acp.ts`(完整参考,~339 行):acpCommand() (L195-339)、loadMcpServers() (L88-193) |
|
||
| **路径** | `scripts/reasonix-acp-wrapper.mjs`(新建) |
|
||
| **结构** | import `AcpServer` from `reasonix/acp/server`、import `DeepSeekClient`, `CacheFirstLoop`, `ToolRegistry` from `reasonix`;从环境变量读取 `DEEPSEEK_API_KEY`、`MNOTE_WEB_URL`(默认为 `http://127.0.0.1:3000`);创建 `ToolRegistry`,注册 `mnote.doc.fetch`、`mnote.doc.markdown_edit` 工具(工具实现通过 HTTP 调用 `MNOTE_WEB_URL/api/hermes/tools/mnote/call`);`AcpServer` + `onRequest("session/new")` → 创建 `CacheFirstLoop`(参考 acp.ts L220-260);`onRequest("session/prompt")` → `loop.run()` + `dispatchKernelEvent()`(参考 acp.ts L260-330);`onNotification("session/cancel")` → `aborter.abort()`(参考 acp.ts L330-339);启动后 `server.done()` 等待 stdin 关闭 |
|
||
| **MNOTE_WEB_URL 寻址** | wrapper 脚本在本地运行,通过 `http://127.0.0.1:3000` 调 mnote-web 的 tool API——因为 `hermes_tools.rs` 的 tool 实现在 Rust 侧,wrapper 不重复实现工具逻辑 |
|
||
| **验证** | 手动测试:`node scripts/reasonix-acp-wrapper.mjs` 启动后,用标准 ACP client 发送 `session/new` + `session/prompt`,验证返回正常;工具调用可正确通过 mnote-web 读写文档 |
|
||
|
||
### Step 11:添加运行时选择器的前端支持
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 在页面 AI 面板中增加运行时切换能力 |
|
||
| **参考** | `hermes-vscode-main/src/chatPanel.ts`(ACP 事件 → UI 渲染)、`hermes-vscode-main/src/webview/main.ts`(webview 事件处理) |
|
||
| **前端路径** | `wolai-frontend/src/components/ai-agent/AiAgentPanel.tsx` |
|
||
| **操作** | 扩展 profile 获取接口 `GET /api/hermes/client/profiles`,解析 `runtimeType` 字段;增加 `<select>` 下拉框显示可用 runtime("Hermes / Reasonix");切换时调用 `PUT /api/hermes/client/profiles/active`;切换后自动刷新当前会话 |
|
||
| **新增 Thought Delta 渲染** | 在 AiAgentPanel 中处理新增的 `thought.delta` SSE 事件,渲染在对话气泡的独立区域(灰色小字或可折叠的 reasoning 面板,参考 hermes-vscode-main webview 对 `agent_thought_chunk` 的渲染) |
|
||
| **验证** | 切换 runtime → 发送消息 → 确认 AI 回复流畅;Reasonix 模式下确认缓存指标显示在 header 中 |
|
||
|
||
### Step 12:profile 扩展 — 从 upstream URL 改为 runtime 配置
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 让 profile 不再持有 `upstream_url`(Hermes HTTPS),而是持有 `runtime_name`(ACP 通用) |
|
||
| **参考** | `hermes_client.rs` 中 `configured_upstream_for_profile()`、`profile_gateway_status()` |
|
||
| **操作** | 扩展 profile 数据结构:添加 `runtime_type: Option<String>`("hermes_http"|"acp")、`runtime_name: Option<String>`(RuntimeConfig 的 name);新增 `configured_runtime_for_profile(profile) → Option<&AcpRuntimeConfig>`;向后兼容:profile 如果只有 `upstream_url` 但没有 `runtime_type`,视为 `hermes_http`(旧行为);profile 如果有 `runtime_type: "acp"`,则走 ACP Session Manager |
|
||
| **health check 改造** | `gateway_health()` 当前只 probe Hermes HTTP upstream;改为:如果 profile 是 `acp` 类型,则调用 `acp_runtime.health_check()`,否则继续 probe HTTP upstream |
|
||
| **profile 默认值** | 新增环境变量 `MNOTE_WEB_ACP_DEFAULT_RUNTIME`:默认 `hermes`;设为 `reasonix` 则默认使用 Reasonix |
|
||
| **验证** | 不改变现有 `hermes_http` 行为;新增 `acp` 类型 profile 的 health check 正常返回 |
|
||
|
||
### Step 13:Hermes HTTP proxy 代码标为 deprecated
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 标记旧代码,避免新开发继续依赖 |
|
||
| **操作** | 在 `hermes_client.rs` 中 HTTP proxy 相关函数(`proxy_json`、`proxy_stream`、`configured_upstream_for_profile` 等)添加 `#[deprecated(note = "迁移到 ACP Session Manager")]`;不影响编译,只是 IDE 和 CI 提示 |
|
||
| **验证** | `cargo build` 无 warning(deprecated 函数被自身使用时默认不 warn) |
|
||
|
||
### Step 14:前端运行时切换验证 e2e
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 同时验证 Hermes ACP 和 Reasonix ACP 两条路径都能正常走通 |
|
||
| **环境准备** | 启动 mnote-web (`cargo run`)、确保 `hermes` 命令可用、确保 `node` 和 `reasonix` npm 包已安装、确保 Reasonix wrapper 脚本就绪 |
|
||
| **测试路径** | 在浏览器页面 AI 面板中选择 "Hermes" → 发送编辑请求 → 确认工具调用和回复正常;切换到 "Reasonix" → 发送同样的编辑请求 → 确认工具调用和回复正常(且 header 显示缓存命中率);块编辑 fast-path (`/api/page-ai/block-edit-workflow`) 独立测试,不受 ACP 切换影响 |
|
||
| **测试账号** | 使用默认测试账号 `mnote.e2e@example.com`,见项目记忆 |
|
||
| **验证** | 两种 runtime 都能正常读写文档;Reasonix 模式下 `message.delta` 流式响应速度不慢于 Hermes |
|
||
|
||
### Step 15:压力测试 — 确认同时多会话稳定性
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 确保多个 ACP session 并发时不出现进程冲突、内存泄漏 |
|
||
| **操作** | 通过 Playwright 或手动测试:同时打开 3 个页面 AI 面板,分别发送不同的编辑请求;观察所有会话是否独立完成;检查 `AcpSessionManager` 的 sessions map 是否在会话结束后正确清理;检查子进程数量是否失控(每个 runtime 应有进程上限,可在 `AcpRuntimeConfig` 中添加 `max_concurrent_sessions`,默认 10) |
|
||
| **验证** | 所有会话都能正常完成;无僵尸子进程残留;`top` 确认 Reasonix Node.js 进程数可控 |
|
||
|
||
### Step 16:退役旧的 Hermes HTTP proxy 代码
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **前提** | 所有 production profile 都已迁移到 ACP;灰度观察期至少 1 周无回退 |
|
||
| **操作** | 删除 `hermes_client.rs` 中所有 `#[deprecated]` 的函数(`proxy_json`、`proxy_stream`、`configured_upstream_for_profile` 等);删除环境变量 `MNOTE_WEB_HERMES_UPSTREAM_URL` 的解析代码;统一所有 profile 为 `runtime_type: "acp"`;不再依赖 `hermes` 命令的 HTTP gateway 模式 |
|
||
| **验证** | `cargo build`、常规 e2e 测试全部通过 |
|
||
|
||
### Step 17:基准测试 — Reasonix 缓存收益量化
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| **目的** | 收集 Reasonix prefix cache 的实际收益数据,作为后续切流的决策依据 |
|
||
| **操作** | 设计 3 轮测试:**场景 A(同一文档连续编辑 5 次)**——对同一文档连续发 5 次 `mnote.doc.markdown_edit`,记录每次的 `prompt_cache_hit_tokens` 和 `prompt_cache_miss_tokens`。**场景 B(不同文档交替编辑 5 次)**——交替编辑 5 个不同文档,统计 cache hit rate。**场景 C(长会话 10 轮对话)**——在同一 session 中连续发 10 条消息,统计 cache hit rate 变化趋势 |
|
||
| **指标** | `cache_hit_rate = hit_tokens / (hit + miss)`;Reasonix 的 `CacheFirstLoop` 每轮迭代后暴露 `ctx.prefixHash` 和 `ctx.stats` |
|
||
| **对比基线** | 同样 3 个场景下 Hermes ACP 的 cache hit rate(理论上接近 0%) |
|
||
| **产出** | 记录到 `benchmarks/reasonix-cache-report.md` |
|
||
| **通过标准** | Reasonix 场景 A 的 cache hit rate ≥ 80%(Reasonix 自述 ~90%+);场景 B ≥ 50%;场景 C ≥ 60% |
|
||
|
||
---
|
||
|
||
## 附录:文件依赖关系图
|
||
|
||
```
|
||
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` 更新进度
|