# 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 切换/管理时清理 bridge 内存会话 | ### 已移除的旧文件 | 文件 | 状态 | |------|------| | `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 /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 内存 session | 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 创建,profile 改变时会重建对应 agent。 - `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 --agent-root --hermes-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 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 endpoint;Windows 默认 `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 | 正常使用不需要配置这些变量。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` 反馈。