# 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` 请求。
