mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 21:40:13 +00:00
634a622934
* fix media skill profile auth and run events * test bridge run profile context
469 lines
16 KiB
Markdown
469 lines
16 KiB
Markdown
# 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 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 |
|
||
|
||
正常使用不需要配置这些变量。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` 反馈。
|