feat: align local-first workspace direction

Document the VSCode-like local-first product shape, demote Convex to a control-plane role, and retire stale architecture drafts.

Add local workspace migration/export references plus smoke coverage for no-Convex managed workspace startup, local markdown title/body/options persistence, asset upload behavior, and Convex fixture export.

Verification: git diff --cached --check; node scripts/check-local-first-convex-guard.js --staged; node scripts/task444-convex-workspace-export-local-fixture-smoke.js; node scripts/task166-local-first-managed-workspace-no-convex-smoke.js; node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
This commit is contained in:
lix-2026
2026-05-19 08:11:58 +08:00
parent 68d321e297
commit cdff672aa5
67 changed files with 5242 additions and 932 deletions
@@ -1,243 +0,0 @@
# 7-5 [done] Hermes client proxy 合同 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/chat.ts`、`packages/client/src/api/hermes/sessions.ts`、`packages/server/src/routes/hermes/proxy-handler.ts`、`packages/server/src/services/hermes/chat-run-socket.ts`
## 1. 边界
- [x] 浏览器只访问 mnote-web 同源 `/api/hermes/client/*`
- [x] Hermes API key / gateway token 只存在 mnote-web 服务端环境变量。
- [x] mnote-web proxy 只做 auth、同源安全、页面上下文注入、trace 注入和错误码标准化。
- [x] mnote-web proxy 不保存 Hermes session、message、tool event、usage、model 真相。
- [x] `pageContext` 是 run 输入上下文,不是 Hermes session 的长期事实源。
- [x]`/api/ai-agent/run` 在并存期只作为 legacy endpoint,不是新 Hermes 面板主代理。
## 2. 路由
### `GET /api/hermes/client/sessions`
请求:
```http
GET /api/hermes/client/sessions?workspaceId=ws_1&documentId=doc_1&limit=20
```
响应:
```json
{
"ok": true,
"traceId": "trace_1",
"sessions": [
{
"sessionId": "mnote_doc_1_20260514",
"title": "当前页问答",
"preview": "请总结当前页面",
"messageCount": 2,
"toolCallCount": 1,
"updatedAt": 1778712000,
"model": "hermes-agent"
}
]
}
```
### `POST /api/hermes/client/sessions`
请求:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"traceId": "trace_1",
"title": "当前页问答"
}
```
响应:
```json
{
"ok": true,
"sessionId": "mnote_doc_1_trace_1",
"traceId": "trace_1",
"persistence": "hermes_on_first_run"
}
```
说明:Hermes Web UI 参考实现没有独立 session create HTTP route,客户端生成 session id,首次 run 时由 Hermes 持久化。mnote 保留本 route 是为了同源客户端合同稳定,但不得在 mnote 保存聊天历史。
### `GET /api/hermes/client/sessions/{session_id}`
请求:
```http
GET /api/hermes/client/sessions/mnote_doc_1_trace_1?workspaceId=ws_1&documentId=doc_1
```
响应:
```json
{
"ok": true,
"sessionId": "mnote_doc_1_trace_1",
"traceId": "trace_1",
"messages": [
{
"messageId": "42",
"role": "assistant",
"content": "当前页标题是...",
"toolCallId": null,
"toolName": null,
"timestamp": 1778712000,
"reasoning": null
}
],
"usage": {
"inputTokens": 120,
"outputTokens": 30,
"totalTokens": 150
}
}
```
### `POST /api/hermes/client/runs`
请求:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"sessionId": "mnote_doc_1_trace_1",
"message": "概括当前页面标题和第一段",
"model": "hermes-agent",
"pageContext": {
"title": "项目计划",
"outline": [],
"pageOptions": { "wideLayout": true }
},
"selectedBlockId": null,
"selectedText": null,
"traceId": "trace_1"
}
```
响应:
```json
{
"ok": true,
"sessionId": "mnote_doc_1_trace_1",
"runId": "run_123",
"messageId": null,
"events": [],
"traceId": "trace_1"
}
```
### `GET /api/hermes/client/events/{run_id}`
请求:
```http
GET /api/hermes/client/events/run_123?sessionId=mnote_doc_1_trace_1
```
响应:`text/event-stream`
```text
data: {"event":"message.delta","run_id":"run_123","session_id":"mnote_doc_1_trace_1","delta":"当前页"}
data: {"event":"run.completed","run_id":"run_123","session_id":"mnote_doc_1_trace_1","output":"当前页...","usage":{"input_tokens":120,"output_tokens":30,"total_tokens":150}}
```
### `POST /api/hermes/client/runs/{run_id}/abort`
请求:
```json
{
"workspaceId": "ws_1",
"documentId": "doc_1",
"sessionId": "mnote_doc_1_trace_1",
"traceId": "trace_1"
}
```
响应:
```json
{
"ok": true,
"runId": "run_123",
"sessionId": "mnote_doc_1_trace_1",
"traceId": "trace_1",
"events": [
{ "event": "abort.started", "run_id": "run_123" }
]
}
```
### `GET /api/hermes/client/models`
响应:
```json
{
"ok": true,
"traceId": "trace_1",
"defaultModel": "hermes-agent",
"models": [
{ "id": "hermes-agent", "label": "Hermes Agent", "provider": "hermes" }
]
}
```
### `GET /api/hermes/client/tools`
响应:
```json
{
"ok": true,
"traceId": "trace_1",
"tools": [
{ "name": "mnote.page.get", "scope": "page.read", "schemaVersion": "mnote.hermes_tool.v1" }
]
}
```
## 3. 错误响应
统一错误体:
```json
{
"ok": false,
"code": "hermes_client_unconfigured",
"message": "Hermes client proxy 未配置 upstream",
"traceId": "trace_1",
"requestId": "req_1"
}
```
稳定错误码:
- [x] `hermes_client_unauthorized`:未登录或缺少有效 mnote 会话。
- [x] `hermes_client_unconfigured`:未配置 `MNOTE_WEB_HERMES_UPSTREAM_URL`
- [x] `hermes_client_bad_request`:请求 JSON 或必要字段错误。
- [x] `hermes_client_upstream_unauthorized`Hermes upstream 拒绝服务端 token。
- [x] `hermes_client_upstream_rate_limited`Hermes upstream 429。
- [x] `hermes_client_upstream_unavailable`Hermes upstream 连接失败或 5xx。
## 4. 并存期规则
- [x] 新页面 AI 面板只允许请求 `/api/hermes/client/*`
- [x] `/api/ai-agent/run` 保留为 legacy guard,不再承载 Hermes 页面 AI 主链。
- [x] `provider=hermes` 不再通过旧 `/api/ai-agent/run` 表达。
- [x] 所有新 smoke 应断言页面 AI 主链没有 `/api/ai-agent/run` 请求。
@@ -1,404 +0,0 @@
# 7-8 [process] 页面 AI Hermes Runtime BFF 下一阶段设计 v1
> 更新时间:2026-05-15
>
> 当前状态:`PROCESS`。本文承接 `7-3`、`7-4`、`7-5`、`7-6`、`7-7` 已完成的 Hermes 页面内客户端主线,写入对 `hermes-web-ui-0.5.18` Chat Runtime 架构的吸收结论。
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/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/process/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`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-7-page-ai-mini-hermes-control-surface-v1.md`
---
## 1. 结论
`hermes-web-ui-0.5.18` 对 mnote 有用,但不能原样照搬。
可吸收的是:
- Web 端应采用 `Browser -> BFF -> Hermes gateway`,不是浏览器直接 ACP。
- BFF 应承担 runtime 管理:active run registry、resume、queue、abort、event normalization、gateway manager、错误规整。
- 前端只展示 message、tool event、状态和输入,不持有 AI 编排真相。
不可吸收的是:
- 不把 mnote 的页面 AI 聊天真相改为 Convex、SQLite 或 mnote 自建 chat DB。
- 不把 Hermes 降级成 `/v1/responses store=false` 的纯模型执行器。
- 不让 mnote-web 根据自有数据库长期组装完整 `conversation_history`
当前冻结方向:
> **mnote 学 `hermes-web-ui` 的 BFF/runtime 架构思想,但会话真相继续归 Hermes;mnote 只保存业务事实、audit、artifact、edge、page/body/title/options 结果。**
---
## 2. 当前实现定位
当前页面 AI 接入是:
```text
Leptos 页面 AI 面板
-> mnote-web /api/hermes/client/*
-> Hermes gateway /v1/runs
-> Hermes gateway /v1/runs/{run_id}/events
```
同时:
- `profiles / skills / memory` 等设置能力由 `mnote-web` 本地 BFF 通过 Hermes CLI 或 `~/.hermes` 文件读写补齐。
- `mnote.*` 工具由 mnote Rust runtime 承接,Hermes 只通过 skill/plugin/tool 调用。
- 页面 AI 不再走旧 `/api/ai-agent/run`
这已经满足最小闭环,但仍只是“最小 runtime 接入”,缺少 `hermes-web-ui` 已经具备的完整 Chat Runtime 能力。
---
## 3. ACP、gateway、responses 的取舍
### 3.1 ACP
ACP 适合:
- VSCode 插件
- Tauri / Electron 桌面端
- 本地 IDE host
- 需要由宿主启动 `hermes acp` 并通过 stdio JSON-RPC 深度交互的场景
ACP 不适合作为 mnote 页面 AI 第一主链:
- 浏览器不能直接 `spawn hermes acp`
- 若 Web 使用 ACP,仍需 mnote-web 作为 ACP bridge 管理 stdio 子进程。
- 这会引入长期进程、session、backpressure、并发、恢复和崩溃清理复杂度。
- 当前 Hermes gateway 已提供 Web 更自然的 `/v1/*` 与 stream 能力。
结论:
> **ACP 后续只作为桌面端或高级 host bridge 候选,不作为页面 AI Web 主链。**
### 3.2 `/v1/runs`
`/v1/runs` 适合当前 mnote 主线:
- Hermes 持有 run / session / event 语义。
- mnote 页面 AI 只是 Hermes client。
- mnote 不需要长期组装 `conversation_history`
- 与“AI 会话归 Hermesmnote 能力归 Rust kernel”一致。
风险:
- mnote-web 需要补 runtime 状态管理,否则刷新、断线、队列、abort 体验弱。
- Hermes gateway 的非 `/v1/*` 管理面不足,需要 BFF 补 profiles/skills/memory/gateway manager。
结论:
> **短期继续以 `/v1/runs` 为默认页面 AI 主链,并在 mnote-web 上补 Runtime BFF。**
### 3.3 `/v1/responses`
`hermes-web-ui` 的产品化 Chat Runtime 使用 `/v1/responses stream=true`,典型形态是:
```text
Browser Vue UI
-> Socket.IO /chat-run
-> hermes-web-ui server
-> /v1/responses stream=true
-> server 从本地 DB 组装 conversation_history
-> server 把 response.* 事件转成 run/message/tool 事件
-> server 写入 SQLite session/messages
```
它适合“自建 Chat Runtime”:
- 后端自有 session/messages 数据库。
- 后端自行压缩上下文。
- 后端自行映射 tool event。
- `store=false` 时 Hermes 不再是会话真相。
这与 mnote 当前页面 AI 方向有冲突。除非后续明确改变设计,把“AI 会话真相”从 Hermes 迁到 mnote,否则不能把 `/v1/responses store=false + mnote conversation_history` 写成默认主链。
允许使用 `/v1/responses` 的条件:
- 只作为特定 runtime 实验或 fallback。
- 必须证明 Hermes session 仍能作为用户可见会话真相,或者另开设计稿明确改变会话真相。
- 不得把 mnote page aggregate / Convex 页面数据变成聊天消息数据库。
结论:
> **`/v1/responses` 先作为参考实现和可评估候选,不作为当前页面 AI 默认主链。**
---
## 4. 目标架构
下一阶段目标不是改成 ACP,也不是直接改成 `/v1/responses`,而是在当前 gateway 主线上补一个 mnote-web Runtime BFF
```text
Leptos 页面 AI 面板
-> mnote-web Hermes Runtime BFF
- active run registry
- session/run resume
- queue
- abort
- event normalization
- gateway/profile/skill/memory BFF
- trace/audit 串联
-> Hermes gateway /v1/runs
-> Hermes session storage
-> mnote skill/plugin/tool
-> Rust runtime / kernel
```
真相边界:
- HermesAI session、message、run、tool event、usage、model、profile。
- mnote:页面、树、正文、artifact、edge、projection、audit、权限、idempotency。
- mnote-web Runtime BFF:运行时连接和事件编排,不保存长期聊天真相。
---
## 5. Hermes Web UI 参考索引
参考根目录:
```text
/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18
```
只参考下表,不整站搬运:
| 参考主题 | 文件 | 搜索点 | mnote 中吸收什么 | mnote 中不采用什么 |
| --- | --- | --- | --- | --- |
| Socket runtime 主链 | `packages/server/src/services/hermes/chat-run-socket.ts` | `ChatRunSocket``onConnection``resumeSession``handleRun``dequeueNextQueuedRun``handleAbort` | active run registry、resume、queue、abort、前端刷新不丢运行态 | 不照搬 Socket.IOmnote 可用 SSE / WebSocket / HTTP 状态轮询 |
| `/v1/responses` 事件映射 | `packages/server/src/services/hermes/chat-run-socket.ts` | `/v1/responses``applyResponseStreamEvent``response.output_text.delta``response.output_item.done``response.completed` | 学习如何把上游事件标准化为 `message.delta``tool.started``tool.completed``run.completed` | 不把 `/v1/responses store=false` 写成默认主链 |
| 后端 session store | `packages/server/src/db/hermes/session-store.ts` | `HermesSessionRow``HermesMessageRow``createSession``addMessage``getSessionDetail` | 理解 Hermes Web UI 如何建完整 chat DB,以及字段形态 | 不在 mnote 建第二套 chat DB;不把 Convex 当 AI 会话真相 |
| 前端 chat store | `packages/client/src/stores/hermes/chat.ts` | `resumeServerWorkingRun``queueLengths``abortState``case 'message.delta'``case 'tool.started'``case 'run.completed'` | 前端状态机、队列提示、abort UI、tool event 归并 | 不复用 Vue/Pinia;不把前端 state 当真相 |
| 前端 API | `packages/client/src/api/hermes/chat.ts` | `startRunViaSocket``resumeSession``registerSessionHandlers``RunEvent` | 客户端事件合同与 runtime event 类型 | 不让浏览器绕过 mnote-web 直连 Hermes |
| VSCode ACP tool UI | `design/05-editor-mainline/reference-code/hermes-vscode-main/src/sessionManager.ts``src/protocol.ts``src/chatPanel.ts``src/webview/main.ts` | `tool_call``tool_call_update``parseToolCall``parseToolCallUpdate``case 'toolCall'``data-tool-id` | 学习按 `toolCallId` 追加工具行、再用后续 update 原地更新状态的 UI 模型 | 不采用 ACP 作为 Web 主链;只参考 tool event UI 状态机 |
| gateway 管理 | `packages/server/src/services/hermes/gateway-manager.ts``packages/server/src/services/gateway-bootstrap.ts` | `getUpstream``start``stop``health` | profile 对应 gateway 的发现、启动、健康检查、错误提示 | 不让页面 AI 抽屉变成完整 gateway 管理台 |
| proxy handler | `packages/server/src/routes/hermes/proxy-handler.ts` | `resolveUpstream``SSE_EVENTS_PATH``streamSSE``/v1/runs` | 路由改写、SSE 拦截、upstream 错误规整 | 不把全量 proxy catch-all 暴露给浏览器 |
| skills 来源 | `packages/server/src/controllers/hermes/skills.ts` | `.bundled_manifest``.hub/lock.json``.usage.json``source``modified` | skill 来源与生成/安装/本地分类 | 不复制完整 skill 管理台 |
快速定位命令:
```bash
cd /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18
rg -n "ChatRunSocket|resumeSession|handleRun|dequeueNextQueuedRun|handleAbort|/v1/responses|applyResponseStreamEvent|response\\.output_text\\.delta|response\\.output_item\\.done|response\\.completed|HermesSessionRow|startRunViaSocket|gateway-manager" packages/server/src packages/client/src
```
---
## 6. RuntimeEvent 合同
mnote-web BFF 对前端暴露的事件应稳定为 `mnote.hermes_runtime_event.v1`
最小事件集:
```json
{ "event": "run.started", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "traceId": "trace_1" }
```
```json
{ "event": "message.delta", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "delta": "当前页", "traceId": "trace_1" }
```
```json
{ "event": "tool.started", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "toolCallId": "call_1", "toolName": "mnote.page.get", "traceId": "trace_1" }
```
```json
{ "event": "tool.completed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "toolCallId": "call_1", "toolName": "mnote.page.get", "summary": "读取当前页面", "auditId": "audit_1", "traceId": "trace_1" }
```
```json
{ "event": "run.completed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "output": "总结文本", "usage": { "inputTokens": 0, "outputTokens": 0 }, "traceId": "trace_1" }
```
```json
{ "event": "run.failed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "code": "hermes_client_upstream_error", "message": "Hermes upstream 返回错误", "traceId": "trace_1" }
```
```json
{ "event": "abort.started", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "traceId": "trace_1" }
```
```json
{ "event": "abort.completed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "synced": true, "traceId": "trace_1" }
```
如果上游是 `/v1/runs/{run_id}/events`BFF 只做字段规整;如果后续试验 `/v1/responses`BFF 负责把 `response.*` 映射为上述 RuntimeEvent。前端不直接依赖上游原始事件格式。
---
## 7. 下一阶段 checklist
### A. Runtime 状态注册表
- [x]`mnote-web` 建立内存级 `sessionId -> active run state` registry。
- [x] 记录 `sessionId``runId``profile``documentId``traceId``status``startedAt``lastEventAt`
- [x] registry 只保存运行态和最近事件摘要,不保存完整聊天历史。
- [x] 服务重启后允许 registry 丢失,但页面必须能从 Hermes session detail 恢复已完成消息。
验收标准:
- [x] 页面刷新时,若 run 仍在 registry 中,前端能看到 `running/tool_calling` 状态。
- [x] 关闭页面再打开,不会把 mnote local storage 当作聊天真相。
### B. Resume
- [x] 增加 `POST /api/hermes/client/sessions/{session_id}/resume` 或等价同源接口。
- [x] 返回 Hermes session detail 中的 messages,并附带 registry 中当前运行态。
- [x] 若 session 不在 registry 中,只返回 Hermes 已持久化状态。
验收标准:
- [x] run 过程中刷新页面,UI 能恢复 run 状态或明确显示“正在同步 Hermes 状态”。
- [x] run 完成后刷新页面,消息来自 Hermes session detail / export,不来自 mnote 自建聊天表。
验证证据:
- `cargo test -p mnote-web hermes_client_ -- --nocapture`:通过 9 个 Hermes client 单测,覆盖 registry 不保存 `messages`、tool event 摘要和 `/resume` 返回 runtime。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js`:通过。13000 是本轮临时启动的新 `mnote-web`,用于避免打断当前 3000;验证后已关闭。
- 当前 3000 运行的是本轮修改前的旧 `mnote-web` 二进制,`POST /api/hermes/client/sessions/{id}/resume` 返回 404;需要重启 3000 后再用同一 smoke 复验主入口。
### C. Queue
- [x] 同一 session 已有 active run 时,后续输入进入后端 queue。
- [x] queue item 只保存待发送 input、profile、context snapshot 摘要和 trace,不保存长期聊天真相。
- [x] 当前 run 结束后自动触发下一条。
- [x] 前端显示 queue length 和可取消项。
验收标准:
- [x] 连续发送两条消息时,第二条显示 queued,而不是并发打到同一 Hermes session。
- [x] 取消 queued item 后,不触发 Hermes run。
验证证据:
- `cargo test -p mnote-web hermes_client_ -- --nocapture`:通过 12 个 Hermes client 单测,新增覆盖 active run 时第二条入队、queue item 不返回完整 `messages/pageContext`、后端取消 queued item、终态后用 mock Hermes upstream 自动启动下一条 queued run。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-queue-smoke.js`:通过。验证连续发送两条消息时第二条走 `queued` 响应,UI 显示队列长度,并能从 Runtime 面板取消 queued item。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js`:通过。确认 resume / tool / 完成态基础链路未被 queue 改动破坏。
- 13000 是本轮临时启动的新 `mnote-web`,验证后已关闭;当前 3000 仍需重启后复验。
### D. Abort
- [x] abort 由 BFF 统一执行,调用 Hermes `/v1/runs/{run_id}/stop` 或中断当前 upstream request。
- [x] abort 前后发出 `abort.started``abort.completed`
- [x] abort 后清理 registry 状态,并处理队列后续项。
验收标准:
- [x] 点击停止后,UI 不再继续追加该 run 的 assistant delta。
- [x] 若 abort 后有队列项,后端按顺序继续处理或明确要求用户确认。
验证证据:
- `cargo test -p mnote-web hermes_client_ -- --nocapture`:通过 13 个 Hermes client 单测,新增覆盖 abort 响应返回 `abort.started/abort.completed`,并在 abort 后继续处理 queued run。
- 临时 `mnote-web``MNOTE_WEB_HERMES_UPSTREAM_URL=http://127.0.0.1:3999` 启动后,执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-runtime-controls-smoke.js`:通过。验证停止 run 后 UI 状态进入 `aborted`,并且后续 `message.delta` 不再追加到聊天区。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-queue-smoke.js``MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js`:通过。确认 queue / resume 基础链路未被 abort 改动破坏。
- 13000 是本轮临时启动的新 `mnote-web`,验证后已关闭;当前 3000 仍需重启后复验。
### E. Event normalization
- [x] mnote-web 将 Hermes 上游事件规整为 `mnote.hermes_runtime_event.v1`
- [x] 前端只消费标准事件,不直接分支处理多套 Hermes 原始事件。
- [x] tool event 展示独立于 assistant 文本。
- [x] 当前 UI 已能在聊天区输出 `工具` 消息并显示工具名,例如 `mnote.page.get`;下一阶段必须升级为按 `toolCallId` 合并的 tool event card。
- [x] `tool.started` 创建或更新同一个工具卡,显示工具名、运行中状态、参数摘要。
- [x] `tool.completed` 原地更新同一个工具卡,显示完成状态、结果摘要、audit/trace id。
- [x] `tool.failed` 原地更新同一个工具卡,显示失败状态、错误码和可复制详情。
- [x] Runtime 页的最近 tool call 摘要继续保留,但不能替代聊天区内联工具卡。
验收标准:
- [x] `message.delta``tool.started``tool.completed``run.completed``run.failed` 均有 smoke 覆盖。
- [x] `tool.started -> tool.completed` 不产生两条互不关联的纯文本工具消息;同一 `toolCallId` 在 UI 中表现为同一张工具卡状态变化。
- [x] 工具卡至少展示 `toolName``toolCallId`、状态、参数摘要、结果摘要、`traceId/auditId`
- [x] tool result 不混入正文编辑器,不写入 page body,除非 Hermes 明确调用 `mnote.page.save` 等写工具。
验证证据:
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js`:通过。smoke 发送同一 `toolCallId=call_smoke_page_get``tool.started -> tool.completed`,断言聊天区只有 1 张 completed 工具卡;同时发送 `toolCallId=call_smoke_page_save``tool.started -> tool.failed`,断言 failed 工具卡显示 `permission_denied`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-runtime-controls-smoke.js``MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-queue-smoke.js`:通过。确认工具卡改动未破坏 abort / queue。
- `cargo test -p mnote-web hermes_client_ -- --nocapture`:通过 14 个 Hermes client 单测,新增覆盖 `assistant_message``tool.failed` 等上游事件被规整为 `mnote.hermes_runtime_event.v1`
- mnote-web `/client/events/{run_id}` 输出 `event: <canonical>``schemaVersion: "mnote.hermes_runtime_event.v1"`;前端消费路径只处理 `message.delta``run.completed``run.failed``run.aborted``tool.*` 标准事件名。
### F. Gateway manager
- [x] BFF 能检查当前 Hermes gateway health。
- [x] BFF 能识别 profile 对应 gateway / model 状态。
- [x] 缺 model/default 或 API key 时,错误提示指向 Hermes 设置,而不是只显示 502。
验收标准:
- [x] `hermes_client_upstream_error` 场景能显示具体修复建议,例如“当前 profile 缺 model.default”。
- [x] profile/skill/settings API 不再错误代理到 Hermes gateway 非 `/v1/*` 路径。
验证证据:
- `mnote-web` 新增 `GET /api/hermes/client/gateway/health?profile=<name>`,只做只读探测,不启动/停止 gateway;返回 `gateway.configured/upstream/status/httpStatus``profile.modelDefault/provider/apiKeyConfigured``suggestions`
- `profile_detail_payload()` 与 fallback profiles 会从 Hermes profile `config.yaml` 读取 `model.default``model.provider`、provider `api_key/key_env` 摘要,但不返回 API key 原文。
- `upstream_error()``hermes_client_upstream_error` 增加 Hermes 设置修复建议;当上游错误正文包含 `model.default``API key``profile/provider` 等线索时,错误 message 会指向 Hermes 设置。
- Runtime 面板增加 gateway/profile 状态区;页面请求失败时前端显示后端 `message`,不再只显示错误 code。
- `cargo test -p mnote-web hermes_client_ -- --nocapture`:通过 17 个 Hermes client 单测,新增覆盖 gateway 未配置、profile 缺 model/API key、`hermes_client_upstream_error` 错误建议。
- 临时 `mnote-web``MNOTE_WEB_HERMES_UPSTREAM_URL=http://127.0.0.1:3999` 启动到 `127.0.0.1:13000` 后,`task-hermes-page-ai-smoke.js``task-hermes-page-ai-runtime-controls-smoke.js``task-hermes-page-ai-queue-smoke.js` 均通过;验证后已关闭 13000。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-agent-skill-smoke.js`:通过。确认新增 gateway health 请求不影响 agent/profile 选择、SOUL.md 保存、skill toggle 与 run profile 透传。
### G. `/v1/responses` 评估,不默认切换
- [x] 增加只读 spike 文档或测试,验证当前 Hermes gateway 的 `/v1/responses stream=true` 事件格式。
- [x] 明确 `/v1/responses` 是否能在不迁移会话真相的前提下复用 Hermes session。
- [x] 若不能保持 Hermes session 真相,则不得进入默认页面 AI 主链。
验收标准:
- [x] 有明确结论:继续 `/v1/runs`,或另开设计稿说明为什么改变会话真相。
- [x] 任何 `/v1/responses` 实验都不得把 mnote Convex / page aggregate 变成聊天消息真相。
只读 spike 结论:
- 参考 `hermes-web-ui-0.5.18/packages/server/src/services/hermes/chat-run-socket.ts`,其 `/v1/responses` 主链在发送前从后端 DB 组装 `conversation_history`,随后设置 `body.stream = true``body.store = false`,再请求 `${upstream}/v1/responses`
- 同一文件的 `applyResponseStreamEvent()` 表明 responses 事件格式主要是 `response.created``response.output_text.delta``response.output_item.added``response.output_item.done``response.completed``response.failed`,由上层 BFF 映射为 `run.started``message.delta``tool.started``tool.completed``run.completed``run.failed`
- `response.output_item.done``function_call` 会被映射为 `tool.started``function_call_output` 会被映射为 `tool.completed`。这对 mnote 的 tool card 状态机有参考价值,但不改变主链。
- 本机只读探测 `http://127.0.0.1:3999/health``/v1/models``OPTIONS /v1/responses` 时,当前没有 gateway 监听,无法取得 live `/v1/responses stream=true` 事件样本;因此本轮不把 live event capture 写成验收依据。
- 基于参考实现,`/v1/responses store=false + conversation_history` 的顺滑体验依赖上层 BFF/SQLite 自建 chat session 真相。除非后续确认 Hermes gateway 支持把 `/v1/responses` 直接绑定到 Hermes session storage,否则 mnote 不应把它切成默认主链。
当前决策:
> **继续 `/v1/runs` 作为页面 AI 默认主链;`/v1/responses` 只作为事件标准化与 tool card 的参考实现。若未来要切换,必须另开设计稿证明 Hermes session 仍是 AI 会话真相,或明确批准迁移会话真相。**
---
## 8. 非目标
- 不在本阶段引入 ACP 主链。
- 不在本阶段把页面 AI 主链切到 `/v1/responses store=false`
- 不在 mnote 建 Hermes Web UI 那样的 SQLite chat DB。
- 不把 Convex 作为 AI 会话真相。
- 不把页面 AI 抽屉扩成完整 Hermes 管理台。
- 不让 mnote plugin 直接写 Convex;写入仍回到 Rust runtime / kernel。
---
## 9. 最终验收口径
本稿完成时,应能同时成立:
- [x] 页面 AI 仍是 Hermes 页面内客户端。
- [x] Hermes 仍是 AI session/message/tool event/usage/model/profile 真相。
- [x] mnote-web 有 Runtime BFF 能力,支持 run resume、queue、abort 和事件标准化。
- [x] 页面刷新或断线不导致 active run UI 状态丢失;完成后的消息从 Hermes 恢复。
- [x] mnote 不保存完整聊天历史,只保存 mnote 业务写入结果和 audit。
- [x] `/v1/responses` 是否进入主链已有单独评估结论,且没有绕过 Hermes session 真相。
一句话:
> **补齐 Web Chat Runtime 的工程能力,但不改变“AI 会话归 Hermesmnote 事实归 Rust kernel”的长期边界。**