# Agent 问答页面设计

> 文档目的：为 agent-management 项目新增一个独立的"agent 问答页面"，让用户以自然语言与 Hermes Agent 直接对话，过程复用现有工作流运行的思考过程展示能力。
> 设计时点：2026-08-03
> 状态：草案，待评审

---

## 0. 一句话定位

提供一个 **类似 ChatGPT/Claude 的对话页面**：用户输入问题、可选选择模型与技能，后端调用 Hermes Bridge 直接执行（不经过工作流编排），前端复用现有 `agent-thinking/*` 组件实时展示思考过程、工具调用、待办清单、用户确认等中间状态，并支持多轮上下文对话与历史会话回看。

---

## 1. 关键设计决策（已确认）

| 决策点 | 选择 | 说明 |
|---|---|---|
| "智能体执行"的语义 | **Hermes 自主执行** | 不区分"回答/技能执行/智能体执行"三种模式，统一调 Hermes Bridge `/run`：用户可选模型 + 可选技能（作为系统提示词），让 Hermes Agent 自主决策如何响应。Hermes Agent 本身具备 40+ 工具与 `skill_view` 能力，能根据问题自动调用合适工具或发现并加载技能 |
| 是否多轮对话 | **需要** | 同一会话内多轮问答，复用 Hermes Agent 的会话状态（`session_id`） |
| 是否持久化历史 | **需要** | 关闭页面后可在侧栏切换历史会话回看，类似现有"运行历史" |
| 持久化策略 | **分层结构化存储** | 主表 `chat_message`（消息元数据 + 最终文本）+ 子表 `chat_message_log`（按行结构化存思考/工具/todo），便于编辑、查询、统计；详见 §4.1 |
| 是否支持"重新生成回复" | **需要** | 基于"消息树"实现：每次重新生成创建新的 assistant 分支节点，旧分支保留可切换。详见 §4.1.4 + §6.4 |
| 是否支持"消息编辑" | **需要** | 编辑用户消息后从该点重新发起 run，生成新的子分支；与"重新生成"共用同一套树结构 |
| 会话标题 | **自动提取** | 创建时标题留空；首次 user 消息完成后，后端取前 30 字作为标题写入；用户可在侧栏双击编辑 |
| 是否支持文件上传 | **暂缓** | 第一版不实现。架构上预留 `chat_message.attachment_id` 字段，后续接入 |
| 是否支持导出对话 | **需要** | 后端 `GET /api/chat/sessions/{id}/export?format=md\|json`；前端"导出"按钮 |
| 是否支持多会话并行运行 | **需要** | 全局 run 池 `useGlobalRuns` 管理所有进行中的 run；会话切换不中断 SSE 流；侧栏显示 loading 指示；详见 §5.7 + §6.7 |
| 多层记忆模式 | **复用上游 + UI 视图** | 不在外层重复实现压缩，直接依赖 Hermes Agent 的 `compress_context`；前端提供"工作记忆 / 短期 / 中期 / 长期"四个读取视图；详见 §6.6 |

---

## 2. 用户故事

### 2.1 典型流程

1. 用户从侧栏进入"Agent 对话"页面。
2. 左侧"会话列表"展示历史会话（按时间倒序），点击新建会话按钮开始新对话。
3. 右侧"对话区"是消息流（用户消息 / 助手回复交替）+ 底部输入区。
4. 输入区上方有"模型选择器"和"技能选择器"两个下拉（均可留空）：
   - 不选模型 → 用系统默认模型
   - 不选技能 → Hermes 凭自身系统提示与 `skill_view` 工具自主决策
   - 选了技能 → 把该技能的名称作为系统提示词注入，如“使用 readme-gen 技能，完成任务。”
5. 用户输入问题、点击发送：
   - 用户消息立即出现在对话区
   - 助手回复区出现一个"思考中"占位卡片，卡片内实时展示思考过程（5 区域：思考流、实时状态、当前工具、工具历史、待办清单）
   - 若 Agent 中途发起 clarify（请求用户确认），卡片内出现 AskforPanel，用户点选项或自定义输入后继续
   - 完成后，思考过程折叠为一行摘要，最终答案展开显示
6. 用户可基于当前回复继续提问，会话上下文累积。
7. 关闭页面后再次进入，会话列表仍可见，点击任一会话可回看完整对话。

### 2.2 边界场景

- **多会话并行（核心场景）**：会话 A 的 run 进行中，用户切到会话 B 发新消息——A 的 SSE 流不中断，B 的 run 也正常启动；侧栏会话列表中 A、B 同时显示 loading 圈圈；用户切回 A 仍能看到实时流式输出（详见 §5.7 + §6.7）。
- **会话切换**：用户从会话 A 切到会话 B，前端只是把 A 的 SSE 监听移交给全局 run 池（不关闭），让 A 在后台继续；切回 A 时从全局池恢复监听。
- **会话进行中刷新页面**：通过 SSE 断线重连机制（`GET /api/chat/runs/{runId}/stream?lastEventId=N`）恢复。
- **会话进行中关闭浏览器再打开**：通过持久化的 `WorkflowPause` + `GET /api/chat/runs/{runId}/state` 轮询恢复 clarify 等待态。
- **跨标签页**：同一会话不允许在多个标签页同时发新消息（前端用 `localStorage` 简单锁，避免 session_id 冲突）。
- **重新生成回复**：用户对某条 assistant 消息点"重新生成" → 后端从该 user 消息分支创建新的 assistant 子节点 → 旧节点 `is_active=0` 仍可查看；用户可用"上一版 / 下一版"按钮在分支间切换。
- **消息编辑**：用户对某条 user 消息编辑后重发 → 后端创建新的 user 子节点（同 parent_id 的兄弟分支）→ 触发新一轮 assistant run；编辑前的对话分支保留。
- **记忆压缩**：用户在某轮对话中触发 Token 阈值（默认 50%），Hermes Agent 自动调用 `compress_context` 把早期消息归档为摘要（`active=0, compacted=1`）；前端在消息流中插入一条"系统摘要"分隔条作为视觉提示。

---

## 3. 系统架构

### 3.1 整体调用链路

```
[前端 ChatPage.vue]
  POST /api/chat/sessions              新建会话 → 返回 sessionId
  POST /api/chat/sessions/{id}/runs    发起新一轮问答（携带 userMessage + 可选 modelId/skillFolderName）
    │                                     ↓ 返回 runId
    │ GET /api/chat/runs/{runId}/stream?lastEventId=N    订阅 SSE
    ↓
[后端 ChatController]
  - 创建 ChatMessage(user) 记录
  - 调 ChatService.startRun(...)
    │
    ↓
[ChatService]
  - 解析 modelId → AiModelConfig（可选）
  - 解析 skillFolderName → systemPrompt（可选，读 SKILL.md）
  - 注册 SseEventBus.register(runId)
  - 异步调 HermesBridgeClient.run(systemPrompt, userMessage, sessionId, ..., modelConfig, sink)
    │                                                  ↓ 接收 Python Bridge 的 SSE 流
    │                                                  ↓ 解析为 ExecutionLog + WorkflowRunEvent
    │                                                  ↓ 通过 sink 推到 SseEventBus
    ↓
  - HermesBridgeClient.run 返回 finalText 时，写入 ChatMessage(assistant) + 更新 ChatSession.lastMessageAt
[前端 SSE 处理]
  - 复用 useChat composable 解析事件 → 更新当前消息的 5 区域状态
  - workflow_complete 时把 finalText 写入 ChatMessage(assistant).content
```

### 3.2 与现有工作流执行的关系

| 维度 | 工作流运行 | Agent 问答 |
|---|---|---|
| 入口 | `POST /api/workflows/{name}/run` | `POST /api/chat/sessions/{id}/runs`（新建） |
| 编排 | DAG，多节点按 level 调度 | 无编排，单次 Bridge `/run` 调用 |
| 节点 | userInput/llm/agent/skill/output 等多种 | 隐式单一"agent"节点 |
| 复用 WorkflowEngine | 是 | **否**（绕过，避免 DAG / 工作目录 / NodeWorkspace 等无关开销） |
| 复用 HermesBridgeClient | 是（通过 HermesAgentExecutor） | **是**（直接调用，不经 Executor） |
| 复用 SseEventBus | 是 | **是** |
| 复用 WorkflowPause | 是（clarify 暂停） | **是** |
| 复用 WorkflowRun 持久化 | 是 | **否**（新建 ChatSession / ChatMessage，语义更清晰） |
| 复用 agent-thinking 组件 | 是 | **是**（核心复用点） |

**核心设计原则**：Agent 问答是工作流执行的一个"特例"——单节点、无 DAG、无工作目录。绕过 WorkflowEngine 直接调 Bridge，但保留所有可复用的基础设施（SSE 总线、clarify 持久化、断线重连、思考过程展示组件）。

---

## 4. 后端设计

### 4.1 数据模型

> **设计原则**：分层结构化存储，便于查询、统计、编辑、版本管理。
> - 主表 `chat_message`：消息元数据（role/status/model/技能）+ 最终文本（user 原文 / assistant 回复）
> - 子表 `chat_message_log`：每条 assistant 消息的思考过程日志按"行"结构化存储，与 `WorkflowRunNode.logs` 元素一一对应
> - 通过 `parent_id` + `is_active` 字段形成"消息树"，原生支持"重新生成"与"消息编辑"

#### 4.1.1 `chat_session`（会话主表）

```sql
CREATE TABLE chat_session (
    id                  BIGINT PRIMARY KEY AUTO_INCREMENT,
    session_uuid        VARCHAR(36) NOT NULL UNIQUE,        -- 对外暴露的 UUID
    title               VARCHAR(255),                       -- 会话标题（首次 user 消息完成后自动取前 30 字，可编辑）
    model_id            BIGINT,                             -- 当前会话默认模型（可切换）
    skill_folder        VARCHAR(255),                       -- 当前会话默认技能 folderName（可切换）
    hermes_session      VARCHAR(64),                        -- Hermes Bridge 的 session_id（多轮复用 Agent 实例）
    status              VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',  -- ACTIVE / ARCHIVED
    last_summarized_at  TIMESTAMP,                          -- 上游最后一次压缩时间戳（来自压缩事件的 `_previous_summary` 时间字段），用于"中期记忆"视图的边界
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    last_message_at     TIMESTAMP,                          -- 用于按最新活动排序
    updated_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_chat_session_last_msg ON chat_session(status, last_message_at DESC);
```

> **为什么单独建表而非复用 WorkflowRun**：WorkflowRun 表语义绑定 `workflowId`，inputs/outputs 是工作流级别的 JSON Map。对话有独特的"消息流"结构（多轮 user/assistant 交替 + 消息树 + 历史版本），强行塞入 WorkflowRun 会让两类数据互相污染，查询/管理都需要伪值过滤。新建独立表语义清晰，未来扩展（点赞点踩、Token 统计）也方便。

#### 4.1.2 `chat_message`（消息主表）

```sql
CREATE TABLE chat_message (
    id                  BIGINT PRIMARY KEY AUTO_INCREMENT,
    session_id          BIGINT NOT NULL,                    -- 关联 chat_session.id
    parent_id           BIGINT,                             -- 父消息 id（首条消息为 NULL）；用于消息树（重新生成/编辑）
    is_active           BOOLEAN NOT NULL DEFAULT TRUE,      -- 当前分支是否激活（同 parent_id 下仅一条为 TRUE）；非激活的旧版本仍可查看
    branch_order        INT NOT NULL DEFAULT 0,             -- 同 parent_id 下的兄弟排序（用户切换"上一版/下一版"用）
    run_id              VARCHAR(64),                        -- 关联本轮 Bridge run（仅 ASSISTANT 消息有；USER 消息为 NULL）
    role                VARCHAR(16) NOT NULL,               -- USER / ASSISTANT / SYSTEM（压缩摘要用 SYSTEM）
    content             TEXT NOT NULL,                      -- USER 原文 / ASSISTANT 最终回复 / SYSTEM 摘要文本
    status              VARCHAR(16) NOT NULL,               -- USER: 'SENT' / ASSISTANT: 'RUNNING'/'SUCCESS'/'FAILED' / SYSTEM: 'SUMMARY'
    model_id            BIGINT,                             -- 实际使用的模型（冗余存储，便于回看当时配置）
    skill_folder        VARCHAR(255),                       -- 实际使用的技能
    error               TEXT,                               -- FAILED 时的错误信息
    attachment_id       BIGINT,                             -- 预留：未来支持文件上传时关联附件表
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    completed_at        TIMESTAMP,
    FOREIGN KEY (session_id) REFERENCES chat_session(id) ON DELETE CASCADE,
    FOREIGN KEY (parent_id) REFERENCES chat_message(id) ON DELETE CASCADE
);
CREATE INDEX idx_chat_message_session ON chat_message(session_id, created_at);
CREATE INDEX idx_chat_message_parent ON chat_message(parent_id, is_active);
CREATE INDEX idx_chat_message_run ON chat_message(run_id);
```

**字段设计要点**：

- `parent_id` + `is_active` + `branch_order`：构成 ChatGPT 风格的消息树
  - 首条消息 `parent_id = NULL`，后续消息默认 `parent_id = 上一条激活消息的 id`
  - "重新生成"某条 assistant 消息：新建一条同 `parent_id` 的 assistant 节点，旧的 `is_active` 置 false，新的置 true，`branch_order` 递增
  - "编辑"某条 user 消息：同样新建一条同 `parent_id` 的 user 节点（旧的置 false），然后基于这条新 user 节点开新的 assistant run
  - 用户切换版本时，前端沿 `parent_id → children` 树导航，激活不同的 `branch_order`
- `attachment_id`：架构上预留文件上传字段，第一版不实现
- 不再有 `logs_json` 字段：日志移到子表 `chat_message_log`，便于按消息查询、按 kind 统计、按工具筛选

#### 4.1.3 `chat_message_log`（消息日志子表，结构化）

```sql
CREATE TABLE chat_message_log (
    id                  BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id          BIGINT NOT NULL,                    -- 关联 chat_message.id（仅 ASSISTANT 消息有日志）
    seq                 INT NOT NULL,                       -- 同一消息内的顺序（从 0 递增）
    kind                VARCHAR(24) NOT NULL,               -- thinking / status / tool_call / tool_result / todo_update / clarify_request / ...
    content             TEXT,                               -- 内容文本（与 ExecutionLog.content 同义）
    tool_name           VARCHAR(128),                       -- 仅 kind=tool_call/tool_result 有
    meta_json           TEXT,                               -- 扩展元数据：如 clarifyId/choices、todo 列表 JSON 等
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (message_id) REFERENCES chat_message(id) ON DELETE CASCADE
);
CREATE INDEX idx_chat_log_message ON chat_message_log(message_id, seq);
CREATE INDEX idx_chat_log_kind ON chat_message_log(message_id, kind);
```

**为什么不直接用 `logs_json` JSON 字段**：

| 维度 | `logs_json` 字段 | `chat_message_log` 子表 |
|---|---|---|
| 写入流式 | 整条 JSON 重写或读-改-写 | 一行 INSERT，O(1) |
| 按 kind 查询 | 取出后内存过滤 | `WHERE kind='tool_call'` 索引扫描 |
| 统计 | 全表扫所有 message | `GROUP BY tool_name` 走索引 |
| 字段长度限制 | 单字段可能超大（思考流数千字） | 每行独立，无单字段超大问题 |
| 历史回看 | 取一条 message 后整体反序列化 | 取所有行按 seq 排序即可 |
| 与 `WorkflowRunNode.logs` 元素对应 | 数组直接对应 | 行直接对应（每行 = 数组一个元素） |

**回看时的转换**：`SELECT * FROM chat_message_log WHERE message_id=? ORDER BY seq` → 直接映射为 `ExecutionLog[]`，与 `WorkflowRunNode.logs` 完全同构，可无缝传给 `runResultReplay.buildReplayNode`。

#### 4.1.4 消息树示意图

```
Session A 的消息树（举例）：

[msg-1 USER] parent=NULL, is_active=true                  ← 首条消息
  │
  ├─[msg-2 ASSISTANT success] parent=1, is_active=true    ← 当前激活的回复
  │    │
  │    └─[msg-3 USER] parent=2, is_active=true            ← 第二轮用户输入
  │         │
  │         ├─[msg-4 ASSISTANT success] parent=3, is_active=true   ← 当前激活（branch_order=0）
  │         │
  │         └─[msg-5 ASSISTANT success] parent=3, is_active=false  ← 用户点"重新生成"产生的旧版本（branch_order=1）
  │
  └─[msg-6 ASSISTANT failed] parent=1, is_active=false    ← 用户曾对 msg-1 重新生成（旧版失败）

用户编辑 msg-3 的文本：
  → INSERT msg-7 USER, parent=msg-2（与 msg-3 同 parent）, is_active=true
  → UPDATE msg-3 SET is_active=false
  → 触发新一轮 assistant run，结果 INSERT msg-8 ASSISTANT, parent=msg-7
```

**查询当前激活分支**：递归从 root 出发，每层取 `is_active=true` 的子节点。SQL 用 CTE 递归或应用层循环均可（深度通常 < 100）。

**展示规则**：
- 默认仅渲染"当前激活分支"（root → ... → leaf），等价于 ChatGPT 单链对话视图
- 用户在某条 assistant 消息上看到"< 2/3 >"切换器 → 沿 `parent_id` 取所有兄弟节点，`branch_order` 排序
- 用户在某条 user 消息上看到"编辑"按钮 → 触发"编辑分支"流程

#### 4.1.5 持久化策略

| 事件 | 操作 |
|---|---|
| 用户发起新会话 | INSERT chat_session（hermes_session 用 `"chat-" + UUID`） |
| 用户发送消息 | INSERT chat_message(role=USER, parent_id=上一条激活消息, is_active=true, status=SENT) → 立即返回；异步触发 Bridge run |
| 助手 run 启动 | INSERT chat_message(role=ASSISTANT, parent_id=对应 USER 消息, is_active=true, status=RUNNING, run_id=xxx) |
| 助手 run 流式产生日志 | **逐行** INSERT chat_message_log(message_id=助手消息 id, seq=自增, kind/content/tool_name/...) |
| 助手 run 完成（success） | UPDATE chat_message SET status=SUCCESS, content=finalText, completed_at=now |
| 助手 run 完成（failed） | UPDATE chat_message SET status=FAILED, error=..., completed_at=now |
| 助手 run 中途 clarify | INSERT workflow_pause（**复用现有表**，按 run_id 关联，不绑 workflowId） |
| 助手 run 完成 | UPDATE chat_session SET last_message_at=now；若是首条消息，自动 SET title=截取前 30 字 |
| 用户重新生成 assistant 消息 | UPDATE chat_message SET is_active=false WHERE id=旧 assistant；INSERT 新 chat_message(parent_id=旧 parent, is_active=true)；触发新 run |
| 用户编辑 user 消息 | UPDATE chat_message SET is_active=false WHERE id=旧 user；INSERT 新 chat_message(parent_id=同 parent, is_active=true, content=新文本)；触发新 run |
| 上游触发压缩 | INSERT chat_message(role=SYSTEM, status=SUMMARY, content=摘要文本)；UPDATE chat_session.last_summarized_at=now |

> **逐行 INSERT 日志性能**：单条 assistant 消息的日志行数通常 10-100，最多约 500（极端长 run）。逐行 INSERT 在批量场景下也可改为"每 N 条/每 200ms 批量提交"。MySQL 默认 autocommit 下，100 行日志的写入耗时约 50ms，可接受。
>
> **复用 WorkflowPause 表**：现有 WorkflowPause 字段（runId/nodeId/clarifyId/question/choices/status/answer）与 workflowId 无强耦合，nodeId 字段对 chat 场景填 "agent" 常量即可，无需新建 chat_pause 表。

### 4.2 REST API 设计

#### 4.2.1 会话管理

| Method | Path | 用途 |
|---|---|---|
| `POST` | `/api/chat/sessions` | 新建会话。Body: `{title?, modelId?, skillFolder?}`。返回 `{sessionId, sessionUuid}` |
| `GET` | `/api/chat/sessions` | 列出当前用户的所有会话（分页，按 last_message_at 倒序）。返回 `[{id, title, modelId, skillFolder, status, lastMessageAt, lastMessagePreview?, hasActiveRun?}]`（`hasActiveRun` 用于侧栏显示 loading） |
| `GET` | `/api/chat/sessions/{id}` | 获取会话详情 + 当前激活分支的所有消息。返回 `{session, messages: [{role, content, status, logs, ...}]}` |
| `PATCH` | `/api/chat/sessions/{id}` | 更新会话（改标题、切换 modelId/skillFolder、归档） |
| `DELETE` | `/api/chat/sessions/{id}` | 删除会话（级联删除消息 + 日志） |
| `POST` | `/api/chat/sessions/{id}/reset` | 重置会话（清空 Hermes Agent 的会话状态），**Bridge 需新增端点**。Body: `{keepHistory: true}`（默认保留消息历史，只重置 Agent 实例） |
| `GET` | `/api/chat/sessions/{id}/export` | 导出对话。Query: `?format=md\|json`、`?branch=active\|all`（默认 active）。返回文件流 |
| `GET` | `/api/chat/sessions/{id}/messages` | 分页加载消息（懒加载历史）。Query: `?before=msgId&limit=50`。返回 `{messages, hasMore}` |

#### 4.2.2 单轮问答（核心）

| Method | Path | 用途 |
|---|---|---|
| `POST` | `/api/chat/sessions/{sessionId}/runs` | 发起一轮问答。Body: `{message, modelId?, skillFolder?, maxIterations?, parentMessageId?}`（`parentMessageId` 指定父消息，用于编辑/重新生成场景；正常发送时传当前激活分支的叶子消息 id）。返回 `{runId, messageId}` |
| `GET` | `/api/chat/runs/{runId}/stream` | SSE 订阅（支持 `?lastEventId=N` 断线重连）。**完全复用 SseEventBus** |
| `GET` | `/api/chat/runs/{runId}/state` | 查询当前 run 状态（用于断线后恢复 clarify 等待态） |
| `POST` | `/api/chat/runs/{runId}/resume` | 提交 clarify 答案。Body: `{clarifyId, answer}`。**复用 WorkflowPause** |
| `POST` | `/api/chat/runs/{runId}/stop` | 中止运行（可选实现，调 Bridge 的 abort） |

#### 4.2.3 消息版本管理（重新生成 / 编辑）

| Method | Path | 用途 |
|---|---|---|
| `POST` | `/api/chat/messages/{messageId}/regenerate` | 重新生成 assistant 回复。后端：把该 messageId 置 `is_active=false`，同 parent 创建新 assistant 消息 `is_active=true`，触发新 run。返回 `{runId, newMessageId}` |
| `PATCH` | `/api/chat/messages/{messageId}` | 编辑 user 消息。Body: `{content}`。后端：把该 messageId 置 `is_active=false`，同 parent 创建新 user 消息 `is_active=true, content=新内容`；**不自动触发 run**（前端用返回的 newMessageId 调 `POST /runs` 触发）。返回 `{newMessageId}` |
| `GET` | `/api/chat/messages/{messageId}/versions` | 列出该消息的所有兄弟版本（同 parent_id）。返回 `[{messageId, branchOrder, isActive, createdAt, preview}]` |
| `POST` | `/api/chat/messages/{messageId}/activate` | 切换激活版本。后端：UPDATE 同 parent 的所有消息 `is_active=false`，UPDATE 指定 messageId `is_active=true` |

> **路径前缀决策**：新端点统一用 `/api/chat/...`，与现有 `/api/workflows/...` 平级。虽然 `GET stream` / `GET state` / `POST resume` 在逻辑上与 WorkflowController 同名端点等价（都基于 SseEventBus + WorkflowPause），但**不共享路径**——独立 Controller 更清晰，未来如果 chat 与工作流的 clarify 处理逻辑出现分歧也好扩展。

#### 4.2.4 资源选择器辅助

| Method | Path | 用途 |
|---|---|---|
| `GET` | `/api/models` | 已有，列出模型 |
| `GET` | `/api/skills` | 已有，列出技能（folderName + 名称 + 描述） |

### 4.3 Service 层

#### 4.3.1 `ChatSessionService` / `ChatSessionRepository`

标准 CRUD。增加：
- `touch(sessionId)`：更新 `last_message_at`
- `autoTitleIfNeeded(sessionId)`：若 `title IS NULL`，取最近一条 user 消息内容前 30 字（去掉换行）作为标题
- `export(sessionId, format, branch)`：导出 Markdown / JSON，`branch=active` 仅导出当前激活分支；`branch=all` 导出整棵树（含历史版本）

#### 4.3.2 `ChatMessageService` / `ChatMessageRepository`

标准 CRUD。增加：
- `appendLog(messageId, log)`：assistant run 过程中**逐行**写入 `chat_message_log`（性能优化：批量提交，每 N 条或每 200ms flush 一次）
- `markSuccess(messageId, finalText)`
- `markFailed(messageId, error)`
- `regenerate(messageId)`：核心方法
  ```java
  @Transactional
  public RegenerateResult regenerate(Long oldMessageId) {
      ChatMessage old = repo.findById(oldMessageId).orElseThrow();
      // 1. 旧消息置 inactive
      old.setIsActive(false);
      repo.save(old);
      // 2. 创建新的 assistant 消息（同 parent_id）
      int newOrder = repo.maxBranchOrderByParent(old.getParentId()) + 1;
      ChatMessage fresh = new ChatMessage(
          old.getSessionId(), old.getParentId(), true, newOrder,
          null, Role.ASSISTANT, "", Status.PENDING, old.getModelId(), old.getSkillFolder());
      repo.save(fresh);
      return new RegenerateResult(fresh.getId(), old.getParentId());
  }
  ```
- `editUserMessage(messageId, newContent)`：把旧 user 消息置 inactive，创建同 parent 的新 user 消息（**不触发 run**，由前端单独调 `POST /runs`）
- `activateVersion(messageId)`：把同 parent_id 的所有消息置 inactive，指定 messageId 置 active
- `getActiveBranch(sessionId)`：递归查询当前激活分支的所有消息（CTE 或应用层循环）
- `getVersions(parentId)`：列出同 parent_id 的所有兄弟消息

#### 4.3.3 `ChatMessageLogService` / `ChatMessageLogRepository`

- `appendBatch(messageId, List<ExecutionLog> logs, startSeq)`：批量写入
- `findByMessage(messageId)`：按 seq 排序返回，可映射为 `ExecutionLog[]` 给前端回放
- `findByMessageAndKind(messageId, kind)`：按类型筛选

#### 4.3.4 `ChatRunService`（核心编排）

```java
public RunStartResult startRun(Long sessionId, String userMessage, Long modelId,
                                String skillFolder, Integer maxIterations, Long parentMessageId) {
    ChatSession session = sessionRepo.findById(sessionId).orElseThrow(...);

    // 1. 写用户消息（若 parentMessageId 已指定编辑场景，前端已先调 PATCH /messages，这里跳过）
    Long userMsgId;
    if (parentMessageId != null && isUserMessage(parentMessageId)) {
        // 编辑场景：parentMessageId 是新创建的 user 消息
        userMsgId = parentMessageId;
    } else {
        ChatMessage userMsg = messageRepo.save(new ChatMessage(
            sessionId, parentMessageId, true, nextOrder(parentMessageId),
            null, Role.USER, userMessage, Status.SENT, modelId, skillFolder));
        userMsgId = userMsg.getId();
    }

    // 2. 解析模型配置
    AiModelConfig modelConfig = (modelId != null) ? aiModelService.getModelConfig(modelId) : null;

    // 3. 解析技能系统提示
    String systemPrompt = (skillFolder != null) ? skillService.getSkillFullContent(skillFolder) : null;

    // 4. 生成 runId，注册 SSE 总线
    String runId = UUID.randomUUID().toString().substring(0, 8);
    eventBus.register(runId);

    // 5. 写助手占位消息（parent = userMsgId）
    ChatMessage assistantMsg = messageRepo.save(new ChatMessage(
        sessionId, userMsgId, true, 0, runId,
        Role.ASSISTANT, "", Status.RUNNING, modelId, skillFolder));

    // 6. 异步调 Bridge
    executor.submit(() -> {
        try {
            List<ExecutionLog> buffer = new ArrayList<>();
            String finalText = hermesBridgeClient.run(
                systemPrompt, userMessage,
                maxIterations != null ? maxIterations : 100,
                null, null, session.getHermesSession(),
                null, runId, modelConfig,
                (kind, content, toolName) -> {
                    // 1) SSE 推送
                    WorkflowRunEvent evt = WorkflowRunEvent.nodeStream(runId, "agent", kind, content, toolName);
                    eventBus.publish(runId, evt.getType(), evt);
                    // 2) 累积到缓冲区，批量写库（每 200ms 或 50 条 flush 一次）
                    buffer.add(ExecutionLog.fromStream(kind, content, toolName));
                    logBufferService.flushIfFull(assistantMsg.getId(), buffer);
                });
            // 最终 flush 残留日志
            logBufferService.flushAll(assistantMsg.getId(), buffer);
            messageRepo.markSuccess(assistantMsg.getId(), finalText);
            sessionRepo.touch(sessionId);
            sessionRepo.autoTitleIfNeeded(sessionId);  // 自动标题
        } catch (Exception e) {
            messageRepo.markFailed(assistantMsg.getId(), e.getMessage());
            eventBus.publish(runId, "workflow_error", WorkflowRunEvent.workflowError(runId, e.getMessage()));
        }
    });

    return new RunStartResult(runId, assistantMsg.getId());
}
```

> **关键复用点**：
> - `HermesBridgeClient.run(...)` 的 sink 回调签名与 `NodeStreamSink.emit(nodeId, kind, content, toolName)` 一致，直接传 lambda 即可。
> - `ExecutionLog` 类型枚举与工厂方法直接复用（`ExecutionLog.thinking(...)` / `toolCall(...)` 等）。
> - `WorkflowRunEvent.nodeStream` / `workflowError` 静态工厂直接复用。
> - 日志缓冲批量写库：避免每条日志一次 INSERT 的开销，缓冲到 50 条或 200ms 时一次性写入。

#### 4.3.5 会话重置（Bridge 端配合）

`POST /api/chat/sessions/{id}/reset` 流程：
1. 后端调 `POST /hermes/sessions/{hermesSessionId}/reset`（**Bridge 新增端点**）
2. Bridge 端从 `_agents` 字典中删除该 session 的 Agent 实例（key 为 `session_id::hermes_home::model_id`）
3. 后端 UPDATE `chat_session.hermes_session = "chat-" + newUUID`（下次对话会创建新 Agent）
4. 后端 UPDATE 当前会话所有 assistant 消息 `is_active=false`，保留作为历史可见
5. 返回新的 sessionId，前端切换到"全新开始"状态

> **Bridge 端需新增的端点**：参考 `backend/hermes-bridge/hermes_bridge.py:120-123` 的 `_agents` LRU 池，添加 `POST /sessions/{sessionId}/reset` 路由，逻辑：`_agents.pop(sessionKey, None)`。

### 4.4 HermesBridgeClient 改造评估

**结论：Java 端无需改造，Bridge 端需新增 2 个端点 + 1 个 SSE 事件类型**。

#### 4.4.1 Java 端（无需改造）

现有 `HermesBridgeClient.run(...)` 签名已经包含所有需要的参数：

```java
// HermesBridgeClient.java:143-146
public String run(String systemPrompt, String userMessage, int maxIterations,
                  String workingDir, String hermesHome, String sessionId,
                  String runId, AiModelConfig modelConfig,
                  NodeStreamSink sink) { ... }
```

Agent 问答场景的调用差异：
- `workingDir` / `hermesHome`：传 null（无文件操作上下文；若未来要让 Agent 操作文件，可传临时目录）
- `sessionId`：从 `chat_session.hermes_session` 取，多轮复用 Agent 实例
- `runId`：本轮 UUID
- `modelConfig`：从 `chat_session.model_id` 解析
- `sink`：直接用 lambda 推到 `SseEventBus`

**clarify 落库拦截**：当前 `WorkflowLevelExecutor.wrapSinkWithClarifyHook` @ `WorkflowLevelExecutor.java:790-828` 在 sink 外包了一层拦截器，遇到 `clarify_request` 时写 `WorkflowPause` 表。Agent 问答需要复用这个拦截器，建议把 `wrapSinkWithClarifyHook` 抽到一个独立的工具方法（如 `ClarifyHooks.wrap(sink, runId, pauseRepo)`）供两条链路共用。

**多会话并行**：Java 端 `SseEventBus` 按 `runId` 维护订阅，天然支持多个 run 并行；不同 runId 的事件互不干扰。

#### 4.4.2 Bridge 端（Python，需改造）

| 端点 / 事件 | 状态 | 用途 |
|---|---|---|
| `POST /hermes/sessions/{sessionId}/reset` | **新增** | 重置会话（从 `_agents` 字典删除该 Agent 实例）。详见 §4.3.5 |
| `GET /hermes/sessions/{sessionId}/memory` | **新增（后续迭代）** | 返回 `MEMORY.md` / `USER.md` 内容 |
| `POST /hermes/sessions/{sessionId}/memory` | **新增（后续迭代）** | 保存编辑后的长期记忆 |
| SSE 事件 `context_compacted` | **新增** | 当上游 `compress_context` 触发时推送，携带 `{summary, compacted_until_message_id}`。详见 §6.6 |

> **第一版最小改动**：仅 `POST /sessions/{sessionId}/reset` 必需（用于"重置会话"按钮）。`context_compacted` 事件可缺失——压缩仍会发生（上游自动），但前端无法实时显示摘要分隔条；可后续迭代补全。`/memory` 端点对应"长期记忆编辑"功能，可放到更后期。

---

## 5. 前端设计

### 5.1 路由与导航

- 路由：`/chat` 与 `/chat/:sessionId`（新增到 `frontend/src/router/index.js`）
- 侧栏菜单：在 `AppSidebar.vue` 的"其他管理"分组上方新增"Agent 对话"入口，图标用 `chat` 或 `message`

### 5.2 页面布局

```
┌────────────────────────────────────────────────────────────────────┐
│ [新建会话] [搜索框]                          [Agent 对话]            │
├────────────────┬───────────────────────────────────────────────────┤
│ 会话列表        │  对话区                                            │
│ ┌────────────┐│  ┌─────────────────────────────────────────────┐  │
│ │• 会话 A     ││  │ USER: 用户消息 1                              │  │
│ │ 会话 B      ││  │ ASSISTANT: 助手回复 1（含折叠的思考过程）      │  │
│ │ 会话 C      ││  │ USER: 用户消息 2                              │  │
│ └────────────┘│  │ ASSISTANT: 助手回复 2（运行中）                │  │
│                │  │   ├ ThinkingLive   (区域②：实时状态)          │  │
│                │  │   ├ 思考流          (区域①：可折叠)           │  │
│                │  │   ├ AgentThinkingPanel                       │  │
│                │  │   │  ├ ToolCalls     (区域③：工具调用)       │  │
│                │  │   │  ├ TodoListPanel (区域④：待办清单)       │  │
│                │  │   │  └ AskforPanel   (区域⑤：用户确认)       │  │
│                │  │   └ RunReconnectBadge (断线重连状态)         │  │
│                │  └─────────────────────────────────────────────┘  │
│                │                                                    │
│                │  ┌─────────────────────────────────────────────┐  │
│                │  │ [模型选择器] [技能选择器]                       │  │
│                │  │ ┌─────────────────────────────────────────┐  │  │
│                │  │ │ 输入问题...                              │  │  │
│                │  │ │                                          │  │  │
│                │  │ │                              [发送]      │  │  │
│                │  │ └─────────────────────────────────────────┘  │  │
│                │  └─────────────────────────────────────────────┘  │
└────────────────┴───────────────────────────────────────────────────┘
```

### 5.3 组件划分

#### 5.3.1 新建组件

| 组件路径 | 作用 |
|---|---|
| `frontend/src/views/chat/ChatPage.vue` | 路由页面，左右布局，组合会话列表 + 对话区 |
| `frontend/src/views/chat/ChatSessionList.vue` | 左侧会话列表（新建/切换/删除/重命名 + **loading 指示**） |
| `frontend/src/views/chat/ChatMessageList.vue` | 中间消息流（user/assistant 交替 + 压缩摘要分隔条 + **版本切换器**） |
| `frontend/src/views/chat/ChatMessageItem.vue` | 单条消息渲染：user 简单气泡（含编辑按钮），assistant 复用 AgentThinkingPanel（含重新生成按钮） |
| `frontend/src/views/chat/ChatMessageVersionSwitcher.vue` | "⟨ 2/3 ⟩" 版本切换组件（用于重新生成/编辑历史） |
| `frontend/src/views/chat/ChatComposer.vue` | 底部输入区：模型选择器 + 技能选择器 + 文本框 + 发送按钮 + **导出/重置会话按钮** |
| `frontend/src/views/chat/ChatSummaryDivider.vue` | 压缩摘要分隔条（显示"前 N 条消息已被压缩为摘要"） |
| `frontend/src/composables/useGlobalRuns.js` | **全局 run 池**：管理所有进行中的 run，会话切换不中断（详见 §5.7） |
| `frontend/src/composables/useChat.js` | 当前会话状态管理（绑定到 useGlobalRuns）（详见 5.4） |
| `frontend/src/api/chat.js` | API 封装 |
| `frontend/src/components/common/ModelSelect.vue` | **新抽取的可复用模型选择器**（详见 5.5） |
| `frontend/src/components/common/SkillSelect.vue` | **新抽取的可复用技能选择器** |

#### 5.3.2 直接复用的现有组件

| 组件 | 文件 | 复用方式 |
|---|---|---|
| `AgentThinkingPanel.vue` | `frontend/src/components/agent-thinking/AgentThinkingPanel.vue` | `<AgentThinkingPanel :node="currentMessage" :pending-ask="pendingAsk" :reconnecting="reconnecting" @submit-clarify="submitClarify" />` |
| `ThinkingLive.vue` | 同目录 | `<ThinkingLive :status="msg.currentStatus" />` |
| `ToolCalls.vue` | 同目录 | 通过 AgentThinkingPanel 间接复用 |
| `TodoListPanel.vue` | 同目录 | 通过 AgentThinkingPanel 间接复用 |
| `AskforPanel.vue` | 同目录 | 通过 AgentThinkingPanel 间接复用 |
| `RunReconnectBadge.vue` | 同目录 | `<RunReconnectBadge :reconnecting="reconnecting" />` |

#### 5.3.3 直接复用的工具函数

| 函数 | 文件 | 用途 |
|---|---|---|
| `buildReplayNode` | `frontend/src/utils/runResultReplay.js:136` | 历史会话回看时，从 `logs[]` 还原思考过程 |
| `extractStreamingThinking` | 同文件 :27 | 单独提取思考流（用于历史消息展开） |
| `extractToolHistory` | 同文件 :48 | 单独提取工具调用历史 |
| `extractLatestTodos` | 同文件 :96 | 单独提取最新 todos |
| `stripHermesThinkingDecoration` | `frontend/src/utils/hermesThinking.js:69` | 剥离颜文字装饰 |

### 5.4 `useChat.js` composable 设计

参考现有 `useWorkflowRunner.js`，但去除 VueFlow 画布相关逻辑，并**绑定到全局 run 池**（详见 §5.7）：

```javascript
// frontend/src/composables/useChat.js
import { useGlobalRuns } from './useGlobalRuns'

export function useChat() {
  // 当前会话状态
  const sessions = ref([])                    // 会话列表
  const currentSession = ref(null)            // 当前会话对象
  const messages = ref([])                    // 当前激活分支的消息列表

  // 输入区状态
  const inputText = ref('')
  const selectedModelId = ref(null)
  const selectedSkillFolder = ref(null)

  // 全局 run 池：管理所有进行中的 run（关键依赖）
  const globalRuns = useGlobalRuns()

  // 当前会话的运行时态（从全局池派生）
  const currentRunId = computed(() =>
    messages.value.length && messages.value[messages.value.length - 1].status === 'RUNNING'
      ? messages.value[messages.value.length - 1].runId
      : null
  )
  const currentMessage = computed(() => currentRunId.value ? globalRuns.getState(currentRunId.value) : null)
  const pendingAsk = computed(() => currentRunId.value ? globalRuns.getPendingAsk(currentRunId.value) : null)
  const reconnecting = computed(() => currentRunId.value ? globalRuns.isReconnecting(currentRunId.value) : false)
  const isRunning = computed(() => currentRunId.value !== null)

  // 监听全局池中当前 runId 的事件，把变化应用到 messages 的对应消息上
  watch(currentRunId, (runId) => {
    if (!runId) return
    // 让全局池知道"当前视图关注这个 run"，从而把 SSE 事件回写到 messages
    globalRuns.attachView(runId, (event) => handleSSEEvent(runId, event))
  })

  // SSE 事件分发（基于全局池回调）
  function handleSSEEvent(runId, event) {
    const msg = messages.value.find(m => m.runId === runId)
    if (!msg) return
    if (msg.runtime == null) msg.runtime = { streamingThinking: '', currentStatus: '', currentTool: null, toolHistory: [], todos: [] }

    if (event.type === 'node_stream' && event.kind === 'thinking') {
      msg.runtime.streamingThinking += event.content
    } else if (event.type === 'node_stream' && event.kind === 'status') {
      msg.runtime.currentStatus = stripHermesThinkingDecoration(event.content)
    } else if (event.type === 'node_stream' && event.kind === 'tool_call') {
      msg.runtime.currentTool = { name: event.toolName, argsSummary: event.content, result: null }
      msg.runtime.toolHistory.push({ ...msg.runtime.currentTool })
    } else if (event.type === 'node_stream' && event.kind === 'tool_result') {
      if (msg.runtime.currentTool) msg.runtime.currentTool.result = event.content
    } else if (event.type === 'node_stream' && event.kind === 'todo_update') {
      msg.runtime.todos = JSON.parse(event.content)
    } else if (event.type === 'node_stream' && event.kind === 'clarify_request') {
      // pendingAsk 已由 globalRuns 维护，组件直接读取
    } else if (event.type === 'workflow_complete') {
      msg.content = event.output?.text || event.finalText || ''
      msg.status = 'SUCCESS'
    } else if (event.type === 'workflow_error') {
      msg.status = 'FAILED'
      msg.error = event.message
    } else if (event.type === 'context_compacted') {
      // 上游触发压缩：插入一条 SYSTEM 摘要消息作为视觉分隔
      messages.value.splice(messages.value.indexOf(msg), 0, {
        role: 'SYSTEM', status: 'SUMMARY', content: '前 N 条消息已压缩为摘要：' + event.summary,
      })
    }
  }

  async function startRun(parentMessageId = null) {
    const userText = inputText.value.trim()
    if (!userText || !currentSession.value || isRunning.value) return

    inputText.value = ''

    // 1. 调 POST /api/chat/sessions/{id}/runs（携带 parentMessageId）
    const { runId, messageId } = await startChatRun(currentSession.value.id, {
      message: userText,
      modelId: selectedModelId.value,
      skillFolder: selectedSkillFolder.value,
      parentMessageId,    // 编辑场景下传入新创建的 user 消息 id；正常发送时传当前激活分支叶子 id
    })

    // 2. 立即把用户消息 + 助手占位消息加到 messages（乐观更新）
    const userMsg = { id: parentMessageId, role: 'USER', content: userText, status: 'SENT', createdAt: Date.now() }
    const assistantMsg = { id: messageId, runId, role: 'ASSISTANT', content: '', status: 'RUNNING', runtime: null }
    if (!parentMessageId) messages.value.push(userMsg)
    messages.value.push(assistantMsg)

    // 3. 让全局池接管 SSE 订阅（不会因会话切换而中断）
    globalRuns.startStreaming(runId)
  }

  // 重新生成 assistant 回复
  async function regenerate(messageId) {
    const { runId, newMessageId } = await regenerateMessage(messageId)
    // 局部重载当前分支
    await reloadActiveBranch()
    globalRuns.startStreaming(runId)
  }

  // 编辑 user 消息
  async function editMessage(messageId, newContent) {
    const { newMessageId } = await editUserMessage(messageId, newContent)
    // 创建了新的 user 消息，基于它发起新 run
    await startRun(newMessageId)
  }

  // 切换版本
  async function activateVersion(messageId) {
    await activateMessageVersion(messageId)
    await reloadActiveBranch()
  }

  // 历史会话加载
  async function loadSession(sessionId) {
    const { session, messages: history } = await getSessionDetail(sessionId)
    currentSession.value = session
    selectedModelId.value = session.modelId
    selectedSkillFolder.value = session.skillFolder
    // 把持久化的 logs[] 通过 buildReplayNode 还原为运行时态
    messages.value = history.map(m => {
      if (m.role === 'ASSISTANT' && m.logs?.length) {
        return { ...m, runtime: buildReplayNode({ logs: m.logs }) }
      }
      return m
    })

    // 检查是否有未完成的 run（页面刷新后恢复）
    const lastMsg = messages.value[messages.value.length - 1]
    if (lastMsg?.status === 'RUNNING') {
      globalRuns.startStreaming(lastMsg.runId)   // 重连续传
    }
  }

  // 切换会话：不主动 abort SSE，让全局池接管
  async function switchSession(sessionId) {
    // 不需要 abort；当前 SSE 流由全局池继续维护
    await loadSession(sessionId)
  }

  return {
    sessions, currentSession, messages,
    currentMessage, pendingAsk, reconnecting, isRunning,
    inputText, selectedModelId, selectedSkillFolder,
    loadSessions: ..., startRun, regenerate, editMessage, activateVersion,
    submitClarify: ..., switchSession, exportSession: ..., resetSession: ...,
  }
}
```

> **关键变化**：与初稿相比，useChat 不再自己管理 SSE 连接——所有 SSE 连接由 `useGlobalRuns` 统一管理，会话切换时不中断。`useChat` 通过 `globalRuns.attachView(runId, callback)` 订阅当前视图关注的 run 事件。

### 5.5 抽取 ModelSelect / SkillSelect 组件

当前 `WorkflowEditor.vue` 内联了 4 处 `<n-select :options="modelOptions">`（`WorkflowEditor.vue:1533/1665/1756/1854`）。建议：

1. 抽取为 `frontend/src/components/common/ModelSelect.vue`：
   ```vue
   <template>
     <n-select :value="modelId == null ? null : Number(modelId)"
               :options="modelOptions" placeholder="选择模型（默认使用默认模型）"
               size="small" clearable @update:value="v => $emit('update:modelId', v)" />
   </template>
   <script setup>
   import { ref, onMounted } from 'vue'
   import { getModelList } from '@/api/model'
   const props = defineProps({ modelId: [Number, String, null] })
   const emit = defineEmits(['update:modelId'])
   const modelOptions = ref([])
   onMounted(async () => {
     const list = await getModelList()
     modelOptions.value = list.map(m => ({ label: `${m.name} (${m.modelName})`, value: m.id }))
   })
   </script>
   ```
2. WorkflowEditor.vue 的 4 处替换为 `<ModelSelect v-model="selectedData.modelId" />`（**后续重构任务，不在本次范围内**）。
3. ChatComposer.vue 直接使用 `<ModelSelect v-model="selectedModelId" />`。

SkillSelect 同理（数据源 `getSkillList`）。

### 5.6 历史会话回看

**复用 RunHistory.vue 的回放模式**：

1. 用户点击会话列表中某条历史会话 → `loadSession(sessionId)`。
2. 后端 `GET /api/chat/sessions/{id}` 返回 `{session, messages}`，每条 assistant 消息含 `logs: [{kind, content, toolName, ...}]` 数组（由后端从 `chat_message_log` 表组装）。
3. 前端用 `buildReplayNode({ logs: m.logs })` 把日志数组还原为运行时态对象。
4. `<ChatMessageItem :message="m">` 渲染：
   - user 消息：简单气泡显示 `m.content`，悬停显示"编辑"按钮
   - assistant 消息：默认折叠态，显示首行 + "展开思考过程" 按钮 + "重新生成"按钮 + 版本切换器（如有多版本）
   - 展开后：完整复用 `<AgentThinkingPanel :node="m.runtime" />`
   - system 摘要消息：渲染为浅色分隔条 `ChatSummaryDivider`

### 5.7 多会话并行运行（全局 run 池）

> **核心目标**：会话 A 的 run 进行中，用户切到会话 B 发新消息——A 的 SSE 流不中断；切回 A 仍能看到实时流式输出。

#### 5.7.1 设计思路

把"哪个 run 当前在跑"和"当前视图关注哪个 run"两个状态彻底解耦：

| 状态 | 维护者 | 用途 |
|---|---|---|
| 所有进行中的 run + 其 SSE 连接 | `useGlobalRuns` 单例（应用级） | 后台运行，不中断 |
| 当前查看的会话 + 当前关注的 runId | `useChat` 实例（页面级） | 渲染当前对话区 |

#### 5.7.2 `useGlobalRuns.js` 接口

```javascript
// frontend/src/composables/useGlobalRuns.js（应用级单例）
const runs = reactive(new Map())    // runId → { state, pendingAsk, reconnecting, sseController, viewCallback }

export function useGlobalRuns() {
  return {
    // 启动 SSE 流（创建 AbortController，注册事件回调）
    startStreaming(runId, lastEventId = null) { ... },

    // 视图绑定：useChat 调用，把回调挂上去，便于把事件回写到当前 messages
    attachView(runId, callback) { ... },
    detachView(runId) { ... },

    // 查询
    getState(runId) { ... },
    getPendingAsk(runId) { ... },
    isReconnecting(runId) { ... },
    isRunning(sessionId) { ... },   // 给侧栏会话列表用，决定是否显示 loading 圈圈
    listActiveRuns() { ... },        // 调试用

    // 提交 clarify 答案
    submitClarify(runId, clarifyId, answer) { ... },

    // 自动重连：网络断开时所有 run 自动尝试重连续传
    // 自动清理：runId 完成（SUCCESS/FAILED）后保留 5 分钟，然后清理 state
  }
}
```

#### 5.7.3 工作流程示意

```
[用户在会话 A 发消息]
  useChat.startRun()
    → POST /runs  返回 runId-A
    → messages.push(assistant 占位消息 with runId-A)
    → globalRuns.startStreaming(runId-A)
         └→ 创建 EventSource，订阅 /api/chat/runs/runId-A/stream
         └→ 接收事件 → 调用 viewCallback → useChat.handleSSEEvent
         └→ 持续流式更新 messages 中 runId-A 对应的消息

[用户切到会话 B]
  useChat.switchSession(B)
    → loadSession(B)
    → ❌ 不调用 globalRuns.stopStreaming(runId-A)
    → ✅ 仅 detachView(runId-A)，但 SSE 连接仍在跑
  用户在 B 发消息 → 同样流程创建 runId-B，B 的 SSE 流并存

[用户切回 A]
  useChat.switchSession(A)
    → loadSession(A) 加载 A 的 messages（包含 runId-A 那条仍在 RUNNING）
    → attachView(runId-A, ...) 重新挂上回调
    → 此后 SSE 事件再次驱动 messages 更新（已经积累的事件通过 lastEventId 补传）

[runId-A 完成]
  收到 workflow_complete 事件 → useChat 把消息标记 SUCCESS
  → globalRuns 5 分钟后清理 runId-A 的 state（保留 chat_message_log 已落库的日志）
```

#### 5.7.4 侧栏会话列表的 loading 指示

```javascript
// ChatSessionList.vue
import { useGlobalRuns } from '@/composables/useGlobalRuns'
const globalRuns = useGlobalRuns()

const sessions = ref([])
// 每个会话右侧的 loading 圈圈
function isActive(session) {
  return globalRuns.isRunning(session.id)
}
```

模板：`<span v-if="isActive(s)" class="loading-dot"></span>` 或用 NaiveUI 的 `<n-spin :size="12" />`。

#### 5.7.5 注意事项

- **同会话禁并发 run**：同一 `chat_session` 同时只允许一个 run（前端 `useChat.isRunning` 锁 + 后端校验）；不同会话可并行。
- **跨标签页**：`localStorage` 锁继续生效；如果某会话在另一标签页已在跑，本标签页的 `startRun` 直接报错"已在其他标签页运行"。
- **Bridge 端 Agent 池**：`hermes_bridge.py:120` 的 `_agents` LRU 池默认容量 8，多个会话同时跑时不会冲突——每个 sessionId 独立 Agent 实例。如超出容量，最久未访问的会被淘汰，下次该会话发消息时 Bridge 会重建 Agent（保留 SQLite 持久化的消息历史，但运行时上下文丢失）。
- **页面卸载**：`onBeforeUnmount` 中 `globalRuns.detachAllViews()`（仅断开视图回调，不停止 SSE？还是停止？**选择停止**——用户关闭页面意味着不再需要后台运行；下次进入页面时若 run 未完成，通过 `lastEventId` 重连续传）。

### 5.8 多层记忆 UI（读取视图）

> **设计原则**：不在外层重复实现记忆压缩，直接依赖 Hermes Agent 上游的 `compress_context` 能力。前端只提供"读取视图"。

#### 5.8.1 四层记忆的语义对应

| UI 视图 | 数据来源 | 显示形式 |
|---|---|---|
| **工作记忆**（Working Memory） | 当前 Agent 实例的实时上下文（活跃消息） | 对话区中的消息流（默认视图） |
| **短期记忆**（Short-term） | `chat_message_log` 中当前激活分支的最近 N 条 assistant 消息日志 | 对话区上方"最近工具调用"侧边面板（可折叠） |
| **中期记忆**（Medium-term） | 已被上游 `compress_context` 压缩的 `chat_message(role=SYSTEM, status=SUMMARY)` 记录 | 对话区中插入的"压缩摘要分隔条"；点击展开查看摘要全文 |
| **长期记忆**（Long-term） | Hermes Agent 的 `MEMORY.md` / `USER.md` 持久记忆（上游文件） | 设置抽屉中的"长期记忆"标签页，通过 `GET /api/chat/sessions/{id}/memory` 读取（**Bridge 需新增端点**） |

#### 5.8.2 关键 UI 组件

- **`ChatSummaryDivider.vue`**：对话流中遇到 `role=SYSTEM, status=SUMMARY` 消息时渲染为浅色横条，文本如"前 23 条消息已压缩为摘要（点击展开）"
- **`ChatMemoryDrawer.vue`**：右上角"记忆"按钮打开的抽屉
  - 标签页 1：长期记忆（`MEMORY.md` / `USER.md`，只读，可编辑后调 Bridge 端点保存）
  - 标签页 2：压缩历史（列出所有 SUMMARY 消息，可逐条展开）
  - 标签页 3：会话统计（消息数、Token 估算、压缩次数、最近压缩时间）

#### 5.8.3 触发压缩的提示

上游压缩由 Agent 内部根据 Token 阈值自动触发，前端不主动控制。当 Bridge SSE 推送 `context_compacted` 事件时，前端：
1. 在当前对话流中插入一条 `SYSTEM` 摘要消息（视觉分隔）
2. 在状态栏显示"上下文已压缩"通知
3. 重新加载长期记忆视图（如果有变化）

#### 5.8.4 关键依赖（Bridge 端改造）

- Bridge 需在 SSE 事件中暴露压缩动作（`context_compacted` 事件，携带 `summary` 字段）——**需要 Bridge 改造**
- Bridge 需新增 `GET /sessions/{sessionId}/memory` 端点返回 `MEMORY.md` / `USER.md` 内容
- Bridge 需新增 `POST /sessions/{sessionId}/memory` 端点保存编辑后的长期记忆

> 第一版可不实现"长期记忆编辑"和"压缩历史视图"——先做基础对话流 + 摘要分隔条，桥端 `context_compacted` 事件配合。后续迭代补全。

---

## 6. 关键技术点

### 6.1 多轮对话 session_id 管理

- **首次对话**：前端 `POST /api/chat/sessions` 创建会话，后端生成 `hermesSession = "chat-" + UUID` 存入 `chat_session.hermes_session`。
- **后续每轮**：`ChatRunService.startRun` 从 `chat_session.hermes_session` 读出 sessionId 传给 `HermesBridgeClient.run(..., sessionId, ...)`。
- **Bridge 行为**：`hermes_bridge.py:453-458` 的 `agent.run_conversation(session_id=...)` 会复用 Agent 实例（包括记忆、上下文），实现真正的多轮。
- **会话切换**：每个 chat_session 有独立的 hermes_session，不会串话。

### 6.2 SSE 断线重连

**完全复用现有机制**：
- `GET /api/chat/runs/{runId}/stream?lastEventId=N`：基于 `SseEventBus.subscribe(runId, lastEventId)`，按 lastEventId 补发历史事件。
- `GET /api/chat/runs/{runId}/state`：回退方案，前端 30 秒轮询查询当前状态 + 当前 clarify 暂停点。
- 前端 `useChat.js` 复用 `consumeSSEStream / tryReconnect / pollRunState` 三层降级（从 `useWorkflowRunner.js` 抽出）。

### 6.3 clarify 流程

- Bridge SSE 事件 `clarify_request` → 后端 `wrapSinkWithClarifyHook` 拦截 → 写入 `workflow_pause` 表（run_id 关联，node_id 填 "agent" 常量）→ 推到前端。
- 前端 `pendingAsk` 状态触发 `AskforPanel`。
- 用户提交答案 → `POST /api/chat/runs/{runId}/resume` → 后端调 `HermesBridgeClient.submitClarifyAnswer(clarifyId, answer)` → 唤醒 Bridge 阻塞线程。

### 6.4 消息版本管理（重新生成 / 编辑）

#### 6.4.1 数据结构

`chat_message` 表的 `parent_id` + `is_active` + `branch_order` 构成消息树（详见 §4.1.2/§4.1.4）。

#### 6.4.2 操作语义

| 操作 | 数据变更 | 备注 |
|---|---|---|
| 重新生成 assistant | UPDATE 旧 assistant `is_active=false`；INSERT 新 assistant 同 `parent_id`, `is_active=true`, `branch_order+1` | 同 parent_id 下形成兄弟分支 |
| 编辑 user | UPDATE 旧 user `is_active=false`；INSERT 新 user 同 `parent_id`, `is_active=true`, `content=新文本` | 不立即触发 run；前端单独调 POST /runs |
| 切换版本 | UPDATE 同 parent_id 所有消息 `is_active=false`；UPDATE 指定消息 `is_active=true` | 前端切换"上一版/下一版" |
| 删除消息 | 不真删，仅 `is_active=false` | 保留历史可追溯；CASCADE 删除仅由 session 删除触发 |

#### 6.4.3 Hermes Agent 上下文一致性

- Hermes Agent 通过 `session_id` 复用实例 + SQLite 持久化消息历史。
- 重新生成场景下：Agent 的"记忆"仍包含旧回复，新生成的回复可能与旧版相似。
- **解决**：调用 Bridge 时携带 `parent_message_id`，Bridge 端基于该 id 截断 SQLite 历史到该位置（`messages WHERE id <= parent_id`），从而模拟"从此点重新生成"的语义——**需要 Bridge 改造**。
- 简化方案（第一版）：不截断，让 Agent 完整记忆——重新生成可能产生相似的回复，但实现简单。后续迭代支持精准回退。

### 6.5 多会话并行运行架构

详见 §5.7。关键点：

- **全局 run 池**：`useGlobalRuns` 单例管理所有进行中的 SSE 连接，会话切换仅切换"视图绑定"，不关闭连接。
- **后端无状态**：SSE 连接由 `SseEventBus` 维护，与 HTTP session 无关，可被任意客户端订阅（按 `runId`）。
- **Bridge 端 Agent 池容量**：`hermes_bridge.py:120` 的 `_agents` OrderedDict LRU 默认 8，超出时淘汰最久未访问。多会话并行需保证不同 sessionId 不冲突——可正常并行；若同时跑超 8 个会话，最老的 Agent 被淘汰（SQLite 历史不丢，但运行时上下文丢失，下次该会话发消息时 Agent 重建）。
- **资源限制**：建议前端 + 后端校验"同时活跃 run 数上限"（如 5 个），避免单用户开太多并行会话压垮 Bridge。

### 6.6 多层记忆模式（基于 Hermes Agent 上游）

#### 6.6.1 上游能力概览

Hermes Agent `compress_context`（详见 `backend/hermes-agent/agent/conversation_compression.py:2128` + `agent/context_compressor.py:1317`）已实现：

- **触发**：当前对话 token 用量超过模型上下文的 `compression.threshold`（默认 0.50，即 50%）时自动触发
- **流程**：4 阶段 LLM 摘要（gather → summarize → format → finalize），把早期消息归档为结构化摘要
- **软归档**：被压缩的消息 `active=0, compacted=1`（详见 `hermes_state.py:6413` 的 `archive_and_compact`），仍保留在 SQLite 中可查询，但不进入下次 LLM 上下文
- **跨次压缩连续性**：通过 `_previous_summary` 字段把上次摘要带入下次压缩，避免摘要漂移
- **配置**：`compression.threshold=0.50`、`compression.target_ratio=0.20`、`compression.protect_last_n=20`（详见 `hermes_cli/config_defaults.py:559-747`）

#### 6.6.2 设计原则

**外层（agent-management）不重复实现压缩，只做读取视图**。

- ❌ 不在 Java 端扫描消息历史 → 调 LLM 生成摘要
- ❌ 不在外层维护"摘要表"
- ✅ 完全依赖 Hermes Agent 自身的 `compress_context` 自动触发
- ✅ 前端通过 Bridge SSE 事件 + 数据库查询展示"工作 / 短期 / 中期 / 长期"四层视图（详见 §5.8）

#### 6.6.3 辅助压缩模型注意点

- 上游配置 `compression.auxiliary.compression.model: null` 默认与主模型一致
- **关键约束**：辅助模型的 context window 必须 ≥ 主模型，否则压缩过程中可能 silently drop 中间片段
- agent-management 端在 `AiModelConfig` 上提供"是否可作为压缩模型"标签，让用户在模型管理页面标注（**后续迭代，不在第一版**）

#### 6.6.4 与消息树的交互

- 压缩发生时，Bridge SSE 推送 `context_compacted` 事件（携带 `summary` + `compacted_until_message_id`）
- 后端监听该事件，在 `chat_message` 表 INSERT 一条 `role=SYSTEM, status=SUMMARY, content=summary` 消息，`parent_id` 指向 `compacted_until_message_id`
- 该 SYSTEM 消息参与消息树展示，但**不参与重新生成 / 编辑的分支判断**（前端跳过 role=SYSTEM 节点）

### 6.7 SSE 断线重连（适配全局池）

详见 §5.7。关键变化：

- 单个 SSE 连接的断线重连由 `useGlobalRuns` 内部处理，不依赖当前视图
- 网络恢复后，连接自动以 `lastEventId` 续传，期间产生的事件不丢失
- 若 `runId` 已完成（后端已发 `workflow_complete`），不再重连，直接通过 `GET /api/chat/runs/{runId}/state` 拉取最终状态

### 6.8 clarify 流程（适配全局池）

- Bridge SSE 事件 `clarify_request` → 后端 `wrapSinkWithClarifyHook` 拦截 → 写入 `workflow_pause` 表 → 推到前端
- 前端 `useGlobalRuns` 把 `pendingAsk` 存到 runId 对应的 state 中
- 若该 runId 是当前视图关注的 run → `useChat` 派生出 `pendingAsk`，触发 AskforPanel
- 若该 runId 不是当前视图关注的 run（用户切到别的会话了）→ AskforPanel 不显示；但侧栏会话列表显示"等待输入"角标，点击切回即可看到 AskforPanel
- 用户提交答案 → `globalRuns.submitClarify(runId, clarifyId, answer)` → POST /resume → 唤醒 Bridge 阻塞线程

### 6.9 并发与生命周期

- **单会话单流**：同一 chat_session 不允许并发 run（前端 `useChat.isRunning` 锁 + 后端校验）。
- **跨会话并行**：不同 chat_session 可并行（受全局上限约束，见 §6.5）。
- **会话切换**：前端切换会话时**不** abort SSE——交给全局池继续维护。
- **页面卸载**：`onBeforeUnmount` 调用 `globalRuns.shutdown()` 关闭所有 SSE 连接。
- **跨标签页**：用 `localStorage.setItem('chat-active-run-' + sessionId, runId)` 简单锁。

### 6.10 性能

- `chat_message_log` 表：单条 assistant 消息产生 10-500 条日志，逐行 INSERT 累计耗时可控（详见 §4.1.5 性能注释）。批量提交（每 50 条或 200ms flush）可进一步降低开销。
- `getSessionDetail` 返回时考虑分页（最近 50 条），更老的通过 `GET /api/chat/sessions/{id}/messages?before=msgId` 懒加载。
- `chat_session` 表索引 `(status, last_message_at DESC)` 加速左侧列表查询。
- `chat_message` 表索引 `(session_id, created_at)` + `(parent_id, is_active)` 加速消息流与版本查询。
- `chat_message_log` 表索引 `(message_id, seq)` + `(message_id, kind)` 加速日志回放与按类型筛选。

---

## 7. 复用矩阵

| 资产 | 状态 | 备注 |
|---|---|---|
| `SseEventBus` | ✅ 直接复用 | 注入即用 |
| `WorkflowPause` 表 + Repository | ✅ 直接复用 | node_id 填 "agent" 常量 |
| `HermesBridgeClient.run` | ✅ 直接复用 | 9 参数签名已涵盖所有场景 |
| `HermesBridgeClient.submitClarifyAnswer` | ✅ 直接复用 | |
| `ExecutionLog` + `WorkflowRunEvent` | ✅ 直接复用 | SSE 事件 DTO |
| `AiModelService` / `SkillService` | ✅ 直接复用 | |
| `agent-thinking/*` 6 个组件 | ✅ 直接复用 | 核心复用点 |
| `runResultReplay.js` / `hermesThinking.js` | ✅ 直接复用 | 历史回放 |
| `useWorkflowRunner.js` 的 SSE/重连/clarify 逻辑 | 🟡 抽取核心逻辑 | 复制到 `useChat.js`，去掉 nodeResults 数组与 VueFlow 耦合 |
| `WorkflowLevelExecutor.wrapSinkWithClarifyHook` | 🟡 抽取为工具方法 | 供两条链路共用 |
| `WorkflowEditor.vue` 内联的 `<n-select>` 模型选择器 | 🟡 抽取为 ModelSelect | WorkflowEditor 后续重构可同步替换 |
| `WorkflowEngine` / `WorkflowLevelExecutor` 主流程 | ❌ 不复用 | 绕过，避免无关开销 |
| `WorkflowRun` / `WorkflowRunNode` 表 | ❌ 不复用 | 新建 ChatSession / ChatMessage 语义更清晰 |
| `WorkflowController` 现有端点 | ❌ 不复用 | 新建 ChatController 路径 `/api/chat/...` |

---

## 8. 实施计划

按依赖顺序分阶段（每阶段可独立验证）：

### 阶段 1：后端骨架（约 2 天）
1. 新建 `ChatSession` / `ChatMessage` / `ChatMessageLog` 实体 + Repository + 基础 CRUD Service
2. 新建 `ChatController`：会话 CRUD 端点
3. 抽取 `ClarifyHooks.wrap(sink, runId, pauseRepo)` 工具方法（从 `WorkflowLevelExecutor.wrapSinkWithClarifyHook` 复制）
4. 实现 `ChatRunService.startRun(...)`：调 `HermesBridgeClient.run` + `SseEventBus.publish` + 批量日志写入（缓冲 flush）
5. 添加 `GET /api/chat/runs/{runId}/stream` / `state` / `POST /resume` 端点
6. 实现自动标题（`autoTitleIfNeeded`）
7. 单元/集成测试：构造一次问答，验证 SSE 流式输出 + 持久化（含 `chat_message_log` 行）

**验证**：用 curl/Postman 触发 `POST /api/chat/sessions/{id}/runs`，订阅 SSE 流，确认事件正常推送；查询 `chat_message_log` 表确认日志逐行落库。

### 阶段 2：前端骨架 + 多会话并行（约 2-3 天）
1. 抽取 `ModelSelect.vue` / `SkillSelect.vue` 组件
2. 实现 `useGlobalRuns.js` 单例（核心：管理 SSE 连接、视图绑定、自动重连）
3. 实现 `useChat.js` composable（绑定 `useGlobalRuns`）
4. 实现 `api/chat.js` 封装
5. 实现 `ChatPage.vue` + 子组件（SessionList with loading 指示 / MessageList / MessageItem / Composer）
6. 路由 + 侧栏菜单注册

**验证**：
- 完整跑通"新建会话 → 发消息 → 看到思考过程 → 收到回复"
- **多会话并行场景**：会话 A run 进行中 → 切到 B 发新消息 → 切回 A 仍能看流式 → A 完成后看到完整回复

### 阶段 3：clarify + 断线重连（约 1 天）
1. 验证 clarify_request → AskforPanel → 提交答案 → 继续执行 全链路
2. 验证刷新页面后通过 `lastEventId` 重连续传
3. 验证关闭浏览器后通过 `GET /state` 轮询恢复 clarify 等待态
4. 验证多会话并行时 clarify 在非当前会话的处理（侧栏角标提示）

### 阶段 4：版本管理（重新生成 / 编辑）（约 1-2 天）
1. 实现后端 `regenerate` / `editUserMessage` / `activateVersion` Service + Controller
2. 实现前端 `ChatMessageVersionSwitcher.vue` 切换组件
3. 实现消息编辑交互（hover 显示编辑按钮 → 弹出编辑框 → 提交后触发新 run）
4. 验证 Hermes Agent 上下文一致性（第一版不截断历史，仅测试效果）

### 阶段 5：导出 + 会话重置（约 1 天）
1. 实现后端 `export(sessionId, format, branch)` Service（Markdown + JSON 两种格式）
2. 实现前端导出按钮 + 下载逻辑
3. 实现 Bridge 端 `POST /sessions/{id}/reset` 端点（删除 Agent 实例）
4. 实现前端"重置会话"按钮（重置后保留消息历史，仅清空 Agent 上下文）

### 阶段 6：多层记忆 UI（约 1-2 天，部分依赖 Bridge 改造）
1. Bridge 端 SSE 推送 `context_compacted` 事件
2. 后端监听该事件写入 `chat_message(role=SYSTEM, status=SUMMARY)`
3. 前端实现 `ChatSummaryDivider.vue` 分隔条
4. 后续迭代：长期记忆视图（依赖 Bridge `GET/POST /sessions/{id}/memory` 端点）

### 阶段 7：打磨（按需）
1. 会话搜索、归档
2. 助手回复复制
3. Token 用量统计
4. 多标签页锁提示

---

## 9. 风险与开放问题

### 9.1 已识别风险

| 风险 | 缓解措施 |
|---|---|
| Hermes Bridge 长时间运行（数小时 clarify 等待）下 session_id 复用可能因 Python 进程重启失效 | Bridge 进程重启后 session_id 失效，下次对话会自动创建新 Agent（Bridge 端的 `_agents` 字典 LRU 清理）；前端检测到 "session not found" 错误时，自动新建会话 |
| `chat_message_log` 单条消息日志行数过多（极端长 run 可能 500+ 行） | 批量 INSERT（每 50 条或 200ms flush）；按 `message_id+seq` 索引高效查询；超过阈值（如 1000 行）报警并截断思考流 |
| Agent 调用工具时访问文件系统可能产生工作目录污染 | Agent 问答场景下传 `workingDir=null`，禁用文件工具；如未来需要，传临时目录 + 复用 `sandbox_patch.py` |
| 同一用户在多个标签页打开同一会话 | localStorage 简单锁；后期可引入 WebSocket 双向通知 |
| 多轮对话累积上下文导致 Token 超限 | 复用 Hermes Agent 自身的 `compress_context` 能力（上游已实现），不额外处理；前端通过 `context_compacted` 事件提示 |
| 多会话并行时 Bridge Agent 池溢出（默认 8 个上限） | 前端 + 后端校验"同时活跃 run 数上限"（建议 5 个）；超出时拒绝新建 run 并提示用户 |
| 重新生成/编辑后 Hermes Agent 上下文不一致 | 第一版不截断 Agent 历史，仅新建分支；接受"重新生成可能相似"的体验；后续 Bridge 改造支持历史截断 |
| `chat_message_log` 行级写入开销（高频小事务） | 缓冲 flush 机制（50 条或 200ms 批量提交）；极端场景可关闭逐行持久化，仅 final 时一次性写入 |
| 辅助压缩模型 context window 小于主模型导致摘要 silently drop | 在 `AiModelConfig` 标注"是否可作为压缩模型"；后续在模型选择器过滤 |
| Bridge 端 Agent LRU 淘汰后下次对话丢失运行时上下文 | session SQLite 历史不丢，但 Agent 运行时状态丢失——下次消息会重建 Agent，对话连续性受影响；可考虑增大 LRU 容量或后台保活 |

### 9.2 已确认的决策（原开放问题）

| 决策点 | 结论 |
|---|---|
| 是否需要"重新生成回复" | **需要**（阶段 4 实现） |
| 是否需要"消息编辑" | **需要**（阶段 4 实现，与重新生成共用消息树） |
| 会话标题策略 | **自动从首条消息前 30 字截取**，用户可在侧栏双击编辑 |
| 是否支持文件上传 | **暂缓**（架构上预留 `chat_message.attachment_id` 字段） |
| 是否需要导出对话 | **需要**（阶段 5 实现 Markdown + JSON 两种格式） |
| 是否支持多会话并行 | **需要**（阶段 2 即实现，核心：全局 run 池） |
| 多层记忆模式 | **复用上游 + UI 视图**（阶段 6 部分实现，长期记忆编辑后续迭代） |

### 9.3 仍待确认的问题

1. **Bridge 端 Agent LRU 容量**：当前默认 8，是否需要增大以支持更多并行会话？（建议保持 8，超出时优雅降级）
2. **会话搜索范围**：仅搜标题，还是扩展到消息内容（需要全文索引）？
3. **导出格式细节**：Markdown 用什么模板？JSON 是否包含 `chat_message_log` 完整日志？
4. **多用户场景**：当前设计未涉及用户体系（所有会话对所有人可见），未来接入用户系统时是否需要加 `owner_id` 字段？

---

## 10. 参考链接

### 现有可复用资产
- [`backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java`](../../backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java) —— Bridge 调用入口
- [`backend/src/main/java/com/agent/management/external/SseEventBus.java`](../../backend/src/main/java/com/agent/management/external/SseEventBus.java) —— SSE 事件总线
- [`backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java:790`](../../backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java) —— `wrapSinkWithClarifyHook` 抽取源
- [`frontend/src/components/agent-thinking/`](../../frontend/src/components/agent-thinking/) —— 思考过程展示组件目录
- [`frontend/src/composables/useWorkflowRunner.js`](../../frontend/src/composables/useWorkflowRunner.js) —— `useChat.js` 的逻辑抽取源
- [`frontend/src/utils/runResultReplay.js`](../../frontend/src/utils/runResultReplay.js) —— 历史回放工具

### 上游压缩相关源码
- [`backend/hermes-agent/agent/conversation_compression.py:2128`](../../backend/hermes-agent/agent/conversation_compression.py) —— `compress_context` 主流程
- [`backend/hermes-agent/agent/context_compressor.py:1317`](../../backend/hermes-agent/agent/context_compressor.py) —— `ContextCompressor` 类
- [`backend/hermes-agent/hermes_state.py:6413`](../../backend/hermes-agent/hermes_state.py) —— `archive_and_compact` 软归档逻辑
- [`backend/hermes-agent/hermes_state_schema.py:655`](../../backend/hermes-agent/hermes_state_schema.py) —— `messages` 表 schema（`active` / `compacted` 字段）
- [`backend/hermes-agent/hermes_cli/config_defaults.py:559-747`](../../backend/hermes-agent/hermes_cli/config_defaults.py) —— 压缩配置默认值
- [`backend/hermes-bridge/hermes_bridge.py:120-123, 170`](../../backend/hermes-bridge/hermes_bridge.py) —— Bridge `_agents` LRU 池（多会话并行基础）

### 相关文档
- [`docs/design/hermes-sandbox-design.md`](./hermes-sandbox-design.md) —— Hermes Bridge 沙箱设计
- [`docs/reference/hermes-agent-upgrade-reference.md`](../reference/hermes-agent-upgrade-reference.md) —— Hermes-Agent 升级参考
- [`docs/design/workflow-output-envelope-design.md`](./workflow-output-envelope-design.md) —— 节点输出信封化设计（消息状态结构参考）

### 上游依赖
- Hermes Bridge `/run` 端点支持 `systemPrompt + userMessage + sessionId + modelConfig`（详见 `backend/hermes-bridge/hermes_bridge.py:138-149`）
- Hermes Agent 支持纯对话模式（`agent.run_conversation`，详见 `backend/hermes-bridge/hermes_bridge.py:453-458`）
- 上游 PR [#69774](https://github.com/NousResearch/hermes-agent/pull/69774) 已让 clarify 永久等待成为原生能力（详见 [`docs/patches/hermes-agent/README.md`](../patches/hermes-agent/README.md)）
- 上游 `compress_context` 自动触发，Bridge 通过 SSE 暴露 `context_compacted` 事件（**需 Bridge 改造，第一版可缺失**）
