Files
hermes-web-ui/docs/cli-chat-sessions.md
ekko 634a622934 [codex] fix media skill profile auth and run events (#965)
* fix media skill profile auth and run events

* test bridge run profile context
2026-05-24 12:52:14 +08:00

469 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLI/Bridge Chat Sessions 实现文档
> 状态:本文档描述当前 `main` 中 Web UI 聊天会话的 API Server / Bridge(beta) 双路径实现。
## 概述
当前实现把原来的聊天通道统一到 Socket.IO namespace `/chat-run`。前端仍使用同一套 `ChatPanel + MessageList + ChatInput`,通过会话的 `source` 字段区分运行方式:
| source | 运行路径 | 说明 |
|--------|----------|------|
| `api_server` | Web UI Server → Hermes Gateway `/v1/responses` | 默认聊天路径 |
| `cli` | Web UI Server → Python agent bridge → `AIAgent` | Bridge(beta),在 Web UI 服务端子进程里直接运行 Hermes Agent |
Bridge 会话不是一个独立 UI 面板,而是普通会话的一种来源。用户通过“新建聊天”下拉菜单选择 `API``Bridge (beta)`
Bridge 模式支持:
- 流式文本输出
- reasoning/thinking 增量
- tool started/completed 事件
- 工具审批请求与响应
- abort 中断
- per-session 队列
- profile 隔离
- 从 DB resume 会话
- 与 API Server 路径共用上下文压缩逻辑
当前不再支持旧文档里的独立 `/cli-chat-run` namespace、`CliChatPanel.vue``cli-chat.ts` 和独立 `command` / `steer` socket 事件。CLI/Bridge 会话中的 slash command 现在通过统一 `/chat-run``run` payload 进入后端解析;当前支持 `/usage``/status``/abort``/queue``/clear``/title``/compress``/steer``/destroy`
---
## 整体架构
```text
ChatPanel.vue
├─ MessageList.vue
└─ ChatInput.vue
│ Socket.IO /chat-run
ChatRunSocket (Node.js)
├─ source=api_server → Hermes Gateway /v1/responses
└─ source=cli → AgentBridgeClient
│ TCP/Unix socket, newline JSON
hermes_bridge.py
│ in-process import
AIAgent (hermes-agent)
```
### 分流规则
`ChatRunSocket.resolveRunSource()` 决定本轮运行走哪个后端:
1. `run` payload 中 `source === 'cli'` 时走 bridge。
2. `source === 'api_server'` 时走 gateway。
3. 未显式传 `source` 时,如果 DB 中已有 session 的 `source``cli`,继续走 bridge。
4. 其他情况默认走 `api_server`
---
## 主要文件
### 前端
| 文件 | 说明 |
|------|------|
| `packages/client/src/components/hermes/chat/ChatPanel.vue` | 统一聊天面板;新建菜单包含 `API``Bridge (beta)`;渲染审批条 |
| `packages/client/src/components/hermes/chat/MessageList.vue` | 统一消息列表;展示文本、reasoning、tool 消息等 |
| `packages/client/src/components/hermes/chat/ChatInput.vue` | 统一输入框;发送、停止、附件上传入口 |
| `packages/client/src/api/hermes/chat.ts` | `/chat-run` Socket.IO 客户端;注册 session 事件处理器;发送 run/abort/approval |
| `packages/client/src/stores/hermes/chat.ts` | 会话状态、发送流程、resume、队列、审批、消息映射 |
### 后端
| 文件 | 说明 |
|------|------|
| `packages/server/src/services/hermes/run-chat/index.ts` | `/chat-run` Socket.IO 入口;按 `source` 分流 API Server 与 Bridge 运行 |
| `packages/server/src/services/hermes/run-chat/handle-api-run.ts` | API Server 路径;调用 Hermes Gateway `/v1/responses` 并消费流式响应 |
| `packages/server/src/services/hermes/run-chat/handle-bridge-run.ts` | Bridge 路径;调用 Agent Bridge 并写入本地会话库 |
| `packages/server/src/services/hermes/run-chat/session-command.ts` | CLI/Bridge slash command 解析与处理 |
| `packages/server/src/services/hermes/agent-bridge/client.ts` | Node 端 bridge 客户端;通过 socket 请求 Python bridge |
| `packages/server/src/services/hermes/agent-bridge/manager.ts` | Python bridge 子进程生命周期管理 |
| `packages/server/src/services/hermes/agent-bridge/hermes_bridge.py` | Python bridge 服务;创建并复用 `AIAgent` 实例 |
| `packages/server/src/services/hermes/agent-bridge/index.ts` | bridge 模块导出 |
| `packages/server/src/index.ts` | 启动 `AgentBridgeManager``ChatRunSocket` |
| `packages/server/src/services/shutdown.ts` | 关闭时停止 chat socket 和 bridge 子进程 |
| `packages/server/src/controllers/hermes/sessions.ts` | 会话列表和详情读取,包含 `source` 信息 |
| `packages/server/src/controllers/hermes/profiles.ts` | profile 管理接口;按 URL/body 中的 profile 做权限校验 |
### 已移除的旧文件
| 文件 | 状态 |
|------|------|
| `packages/client/src/api/hermes/cli-chat.ts` | 已删除 |
| `packages/client/src/components/hermes/chat/CliChatPanel.vue` | 已删除 |
| `packages/server/src/services/hermes/cli-chat-run-socket.ts` | 已删除 |
---
## 前端流程
### 新建会话
`ChatPanel.vue` 中的新建按钮使用下拉菜单:
- `API`:调用 `chatStore.newChat()`,创建默认 `api_server` 会话。
- `Bridge (beta)`:调用 `chatStore.newCliSession()`,创建 `source: 'cli'` 会话。
Bridge 会话 ID 使用类似 `YYYYMMDD_HHMMSS_xxxxxx` 的格式,便于与 Hermes CLI 风格的 session ID 对齐。
### 发送消息
1. `ChatInput.vue` 触发 store 的发送逻辑。
2. `chat.ts` 根据 active session 组装输入内容,附件会被转为 `ContentBlock[]`
3. 调用 `startRunViaSocket()`
4. 前端向 `/chat-run` emit
```ts
socket.emit('run', {
session_id,
input,
instructions,
model,
queue_id,
source, // api_server 或 cli
})
```
5. 前端注册本 session 的事件 handler,通过 `session_id` 隔离多会话并发事件。
### Resume
切换会话、页面恢复可见、或刷新后,前端通过:
```ts
socket.emit('resume', { session_id })
```
服务端返回:
```ts
{
session_id,
messages,
isWorking,
isAborting,
events,
inputTokens,
outputTokens,
queueLength,
}
```
如果服务端发现该 session 仍在运行,前端会重新注册 handler,并允许继续 abort。
### 审批
Bridge 工具需要人工确认时,服务端会发 `approval.requested`,前端 store 记录为 `activePendingApproval``ChatPanel.vue` 在输入框上方显示审批条。
前端响应审批:
```ts
socket.emit('approval.respond', {
session_id,
approval_id,
choice, // once | session | always | deny
})
```
---
## `/chat-run` Socket.IO 协议
### 客户端 → 服务端
| 事件 | 数据 | 说明 |
|------|------|------|
| `run` | `{ session_id, input, model?, instructions?, queue_id?, source? }` | 启动一轮运行;`source` 决定 API Server 或 Bridge |
| `resume` | `{ session_id }` | 加入 session room 并恢复状态 |
| `abort` | `{ session_id }` | 中断当前运行 |
| `cancel_queued_run` | `{ session_id, queue_id }` | 取消等待队列中的一条 run |
| `approval.respond` | `{ session_id, approval_id, choice }` | 响应 Bridge 工具审批 |
客户端不再发送独立 `command``steer` Socket.IO 事件;slash command 作为普通 `run.input` 进入 `/chat-run`,由服务端在 `source=cli` 时解析。
### 服务端 → 客户端
| 事件 | 说明 |
|------|------|
| `resumed` | 返回 DB 消息、运行状态、队列长度和最近事件 |
| `run.started` | 运行开始 |
| `run.queued` | 当前 session 已有运行,新请求进入队列 |
| `message.delta` | 文本增量 |
| `reasoning.delta` | reasoning 增量 |
| `thinking.delta` | thinking 增量 |
| `reasoning.available` | reasoning 内容可用 |
| `tool.started` | 工具调用开始 |
| `tool.completed` | 工具调用结束 |
| `approval.requested` | Bridge 工具请求人工审批 |
| `approval.resolved` | 审批完成或超时 |
| `compression.started` | 上下文压缩开始 |
| `compression.completed` | 上下文压缩结束 |
| `usage.updated` | token 用量更新 |
| `abort.started` | 中断开始 |
| `abort.completed` | 中断结束 |
| `session.command` | slash command 的执行结果或错误反馈 |
| `run.completed` | 运行完成 |
| `run.failed` | 运行失败 |
### 认证
`/chat-run` 使用 Socket.IO auth token
```ts
io(`${baseUrl}/chat-run`, {
auth: { token },
query: { profile },
})
```
如果未设置 `AUTH_DISABLED=1`,服务端会与 Web UI token 比对。
---
## ChatRunSocket 后端行为
### API Server 路径
`source=api_server` 时:
1. 写入用户消息到 Web UI 本地 session DB。
2. 通过 `buildCompressedHistory()` 构建上下文。
3. 请求当前 profile 的 Hermes Gateway
```text
POST <upstream>/v1/responses
```
4. 读取 SSE frame,映射为统一的 `/chat-run` 事件。
5. 完成后写入 assistant/tool 消息,更新 usage。
### Bridge 路径
`source=cli` 时:
1. 写入用户消息到 Web UI 本地 session DB。
2. 复用同一套 `buildCompressedHistory()` 构建压缩上下文。
3. 调用:
```ts
this.bridge.chat(session_id, input, history, instructions, profile)
```
4. 轮询 `AgentBridgeClient.streamOutput(run_id)`
5. 将 Python bridge 的 delta 和 events 映射成统一事件。
6. 将 assistant 文本、reasoning、tool 调用结果 flush 回 DB。
### 队列
同一个 `session_id` 同时只能有一个 active run。新的 `run` 到达时:
- 如果当前 session 正在运行,则放入 `state.queue`
- 发送 `run.queued` 更新队列长度。
- 当前 run 结束或 abort 完成后,自动执行下一条 queued run。
---
## Python Agent Bridge
### 通信协议
Node 和 Python bridge 之间使用本地 socket 的单行 JSON 协议:
```json
{ "action": "chat", "session_id": "xxx", "message": "hello" }
```
响应也是单行 JSON
```json
{ "ok": true, "run_id": "xxx", "session_id": "xxx", "status": "running" }
```
### Endpoint
默认 endpoint 按平台选择:
| 平台 | 默认 endpoint |
|------|---------------|
| Windows | `tcp://127.0.0.1:18765` |
| macOS/Linux | `ipc:///tmp/hermes-agent-bridge.sock` |
Windows 使用 TCP 是因为部分 Python/Windows 环境没有 Unix domain socket 支持。
### 当前实际使用的 action
| Action | 说明 |
|--------|------|
| `chat` | 启动一轮 `AIAgent.run_conversation()` |
| `get_output` | 通过 `cursor``event_cursor` 获取增量文本与事件 |
| `interrupt` | 调用 agent 中断当前运行 |
| `approval_respond` | 响应工具审批 |
| `destroy_all` | 维护动作;仅用于明确的全量清理/进程关闭场景,普通 profile 切换不会调用 |
bridge 代码里还保留了一些调试/维护 action,例如 `ping``get_result``get_history``destroy``list``shutdown``steer`。当前 `/chat-run` 前端路径不会直接暴露这些 action;需要的能力由 Node `/chat-run` 层封装,例如 `/steer` slash command 会调用 `steer` action。
旧的 `command` action 已移除,Python bridge 不再直接解析 `/new``/undo``/retry``/branch` 等旧斜杠命令;当前 CLI/Bridge slash command 支持范围以 Node `/chat-run``session-command.ts` 为准。
### 会话和 profile
`AgentPool` 维护 `session_id -> AgentSession`
- 每个 session 持有独立 `AIAgent` 实例。
- session 按请求中的 profile 创建和复用;前端切换 Hermes Profile 只改变后续请求使用的 profile,不会影响其他 bridge 内存 session。
- `HERMES_HOME` 会在创建 agent 时临时切到 profile home。
- `SessionDB` 按 profile 的 `state.db` 路径缓存。
- 空闲 session 会被 bridge GC,默认 30 分钟无运行后销毁内存态。
### 工具和审批事件
bridge 从 `AIAgent` 回调中收集事件:
- `stream.delta`
- `reasoning.delta`
- `thinking.delta`
- `tool.started`
- `tool.completed`
- `tool.progress`
- `approval.requested`
- `approval.resolved`
- `turn.boundary`
- `status`
`ChatRunSocket` 会把这些事件转换为前端统一事件,并负责 DB 落盘。
审批默认等待 60 秒,超时自动 `deny`
---
## AgentBridgeClient
`AgentBridgeClient` 是 Node 端本地 socket 客户端。
行为:
- 支持 `ipc://``tcp://` endpoint。
- 每次请求新建 socket,发送一行 JSON,读取一行 JSON。
- 请求通过内部 lock 串行化。
- 默认请求响应超时为 `120000ms`
- `streamOutput()` 每 100ms 轮询一次 `get_output`
示例:
```ts
const started = await bridge.chat(sessionId, input, history, instructions, profile)
for await (const chunk of bridge.streamOutput(started.run_id)) {
// chunk.delta
// chunk.events
// chunk.done
}
```
注意:目前 socket connect 阶段没有独立 connect timeout,主要依赖系统连接错误和请求响应 timeout。
---
## AgentBridgeManager
`AgentBridgeManager` 负责启动和停止 Python bridge。
启动流程:
1. 定位 `hermes_bridge.py`
2. 发现 `hermes-agent` 根目录。
3. 选择 Python 解释器。
4. 以子进程启动:
```text
python hermes_bridge.py --endpoint <endpoint> --agent-root <root> --hermes-home <home>
```
5. 监听 stdout,等待:
```json
{ "event": "ready", "endpoint": "..." }
```
6. 默认 ready 超时为 `120000ms`
Python 选择优先级:
1. `HERMES_AGENT_BRIDGE_PYTHON`
2. `agentRoot/venv``agentRoot/.venv`
3. installed `hermes` 命令 shebang
4. `uv run --project <agentRoot> python`
5. 系统 `python3` / `python`
关闭时先发 `SIGTERM`1.5 秒后仍未退出则 `SIGKILL`
---
## 启动与关闭
### 启动
`bootstrap()` 中会先尝试启动 bridge
```ts
agentBridgeManager = await startAgentBridgeManager()
```
bridge 启动失败不会阻止 Web UI 启动,但 Bridge(beta) 会话后续运行会失败。
随后创建统一的 chat socket
```ts
chatRunServer = new ChatRunSocket(groupChatServer.getIO())
chatRunServer.init()
```
### 关闭
服务关闭时会清理:
- `/chat-run` Socket.IO 状态
- Python agent bridge 子进程
- 其他 WebSocket/Socket.IO 服务
---
## 环境变量
| 变量 | 说明 |
|------|------|
| `HERMES_AGENT_BRIDGE_ENDPOINT` | Bridge endpointWindows 默认 `tcp://127.0.0.1:18765`macOS/Linux 默认 `ipc:///tmp/hermes-agent-bridge.sock` |
| `HERMES_AGENT_BRIDGE_TIMEOUT_MS` | Node 等待 bridge 请求响应的超时,默认 `120000` ms |
| `HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS` | Node 连接 bridge socket 失败时的短重试窗口,默认 `5000` ms |
| `HERMES_AGENT_BRIDGE_STARTUP_TIMEOUT_MS` | Node 等待 Python bridge ready 的超时,默认 `120000` ms |
| `HERMES_AGENT_BRIDGE_AUTO_RESTART` | bridge broker 意外退出后是否自动重启;设为 `0`/`false`/`no`/`off` 可关闭,默认开启 |
| `HERMES_AGENT_BRIDGE_RESTART_DELAY_MS` | bridge broker 自动重启基础延迟,默认 `1000` ms,连续失败时最多退避到 `30000` ms |
| `HERMES_AGENT_BRIDGE_PYTHON` | 指定 Python 解释器路径 |
| `HERMES_AGENT_ROOT` | hermes-agent 安装目录 |
| `HERMES_AGENT_BRIDGE_UV` | 指定 uv 可执行文件路径 |
| `HERMES_AGENT_BRIDGE_PLATFORM` | bridge 传给 Hermes Agent 的平台标识,默认 `cli` |
| `HERMES_BRIDGE_PROVIDER` | 覆盖 bridge 使用的 provider |
| `HERMES_BRIDGE_MAX_TURNS` | 覆盖 bridge 最大轮数 |
| `UV` | uv 可执行文件路径 fallback |
正常使用不需要配置这些变量。Bridge 支持多个用户/多个 profile 的运行并存;Web UI 的 Hermes Profile 切换不会重启 bridge 或销毁其他正在运行的任务。Windows 下如果默认 TCP 端口被旧 bridge/broker/worker 占用,新 bridge 会先按端口杀掉旧进程树,再用同一个 endpoint 重建。
Windows 首次启动慢时可以临时放大:
```powershell
$env:HERMES_AGENT_BRIDGE_STARTUP_TIMEOUT_MS = "300000"
$env:HERMES_AGENT_BRIDGE_TIMEOUT_MS = "300000"
```
---
## 当前限制
- Bridge(beta) 仍依赖 Python bridge 成功启动;启动失败时 Web UI 可用,但 bridge 会话不可用。
- bridge socket connect 阶段还没有单独 connect timeout。
- 旧 CLI 独立面板和独立 `/cli-chat-run` namespace 已移除。
- 旧 bridge `command/steer` socket 控制层已移除;CLI/Bridge slash command 现在通过统一 `/chat-run``run.input` 解析并以 `session.command` 反馈。