# Hermes 思考过程展示重构实施计划

> **版本**：v1.1
> **日期**：2026-08-02
> **状态**：待用户确认
> **范围**：智能操作节点、Agent/Skill 节点、技能生成（待用户确认是否纳入）
>
> **v1.1 变更**：用户确定 Q1=同步阻塞方案；新增"无超时 Askfor"要求（用户可能几小时后才回来）；调研发现 S1 拉长超时方案在网络中断时无法恢复，确定采用 **S2 持久化 + 重连** 子方案。

---

## 1. 需求重述

用户希望把 Hermes agent 执行时的思考过程展示从单一的"追加型思考"扩展为 5 个区域：

| 序号 | 区域 | 数据特征 | 更新模式 |
|---|---|---|---|
| ① | 追加型思考过程 | 模型流式输出的完整文本（已存在） | 增量累加 |
| ② | 实时型思考过程 | "正在进行 XXX"、"下一步：XXX" 等当前状态文案 | **覆盖式**（同一条目被新文案替换） |
| ③ | 工具调用 | 上方：当前调用中的工具；下方：历史调用列表 | 上方覆盖、下方追加 |
| ④ | TODO List | agent 规划的待办，含已完成/未完成 | 增删改（item 状态切换） |
| ⑤ | Askfor | 模型抛出的"需用户确认"问题，含选项按钮组；**最后一项固定为"自定义输入"** | 阻塞式（等待用户作答后消失） |

---

## 2. 现状梳理（基于代码调研）

### 2.1 Hermes 原生能力盘点

| 能力 | Python 侧位置 | Bridge 是否透传 | Java 是否接收 | 前端是否展示 |
|---|---|---|---|---|
| 流式文本（text） | `conversation_loop.py:461` | ✅ `hermes_bridge.py:287` | ✅ `HermesBridgeClient.java:293` | ✅ 追加思考 |
| 工具开始/结束 | `tool_executor.py:431` | ✅ `hermes_bridge.py:294-297` | ✅ `HermesBridgeClient.java:269, 283` | ❌ `useWorkflowRunner.js:184` 主动忽略 |
| TODO 工具 | `tools/todo_tool.py:240` | ⚠️ 仅作普通 tool_end | ⚠️ 仅作 TOOL_RESULT | ❌ |
| Askfor（clarify） | `run_agent.py:378` | ❌ | ❌ | ❌ |
| 实时进度（step） | `run_agent.py:380` | ❌ | ❌ | ❌ |
| 状态通知（status） | `run_agent.py:384` | ❌ | ❌ | ❌ |
| 思考 spinner（thinking） | `run_agent.py:376` | ❌ | ❌ | ❌ |
| 工具细分进度（tool_progress） | `agent_init.py:184` | ❌ | ❌ | ❌ |

**核心结论**：Hermes 的能力池是充足的，瓶颈在 **Bridge 透传层只接了 3 个回调**（`stream_callback` / `tool_start_callback` / `tool_complete_callback`），其余 5+ 类回调全部沉默执行。

### 2.2 数据通道现状

```
[Python AIAgent]
   ↓ (callback)
[hermes_bridge.py / FastAPI:18731]  ← 仅透传 text/tool_start/tool_end
   ↓ (SSE)
[HermesBridgeClient.java]           ← switch 仅识别 5 类 ExecutionLog
   ↓ (NodeStreamSink)
[WorkflowLevelExecutor.java:196]    ← 包装为 node_stream 事件
   ↓ (SseEmitter)
[useWorkflowRunner.js:130]          ← handleSSEEvent 仅处理 thinking 累加
   ↓
[WorkflowEditor.vue:2293]           ← <pre> 纯文本追加
```

### 2.3 双向通道缺口（针对 Askfor）与"无超时"挑战

**两个关键事实**：

1. **Bridge 当前完全没接 clarify_callback**：`hermes_bridge.py:211-223` 创建 AIAgent 时未传 `clarify_callback`，导致 `agent.clarify_callback is None`。agent 调用 `clarify` 工具时直接返回 `"Clarify tool is not available in this execution context."`（`tools/clarify_tool.py:57-61`），不阻塞、自主决策。
2. **当前 4 层超时全部不允许"几小时"的等待**：
   - clarify_callback 阻塞：CLI 默认 120s（`hermes_cli/callbacks.py:42`）/ Gateway 默认 600s（`tools/clarify_gateway.py:245`）
   - Java HttpURLConnection readTimeout：300s（`HermesBridgeClient.java:198`）
   - SseEmitter：30 分钟（`WorkflowController.java:203`）
   - 工作流主超时：30 分钟（`WorkflowLevelExecutor.java:56`）

**核心难点**：用户希望 Askfor **无超时**（可能几小时后才回来）。但单纯拉长超时（S1 方案）在网络中断场景下不可行——用户笔记本合盖 8 小时后 TCP 连接已死，前端 `reader.read()` 抛 network error（`useWorkflowRunner.js:123-127` 当前只显示"工作流执行失败"），用户回来后**无法重连**，工作流会在 24h 兜底 abort 后变成 FAILED。

**Hermes 已有的同步阻塞参考实现**：`tools/clarify_gateway.py` 的 `wait_for_response`（`clarify_gateway.py:103-143`）用 1 秒切片 `Event.wait` 轮询 + `touch_activity_if_due` 防 watchdog，配合 `resolve_gateway_clarify`（`clarify_gateway.py:150-162`）唤醒。**Gateway 模式在用，可直接照搬**。

**项目已具备的关键基础设施**（决定方案走向）：
- `external/SseEventBus.java:32-141` 已实现按 runId 缓存最近 1000 条事件 + `Last-Event-ID` 头断线重连 + 完成后缓存保留 1 小时
- `ExternalApiController.java:227` 的外部 API 入口已支持断线重连
- `model/entity/WorkflowRun.java` / `WorkflowRunNode.java` 已有完整的运行/节点状态字段
- `NoopSseEmitter.java:16` 已用 `Long.MAX_VALUE` 实现"永不超时"（外部 API 现有实践）

**结论**：采用 **S2 持久化 + 重连** 方案——clarify 触发时把问题写入 DB + 透传到前端 SSE，前端连接断开后通过 `Last-Event-ID` 重新订阅 SseEventBus 续接；用户答题后调 `POST /runs/{runId}/resume` 把答案送回 Bridge 唤醒阻塞线程。

---

## 3. 待用户确认的关键问题

以下问题不同选择会导致改造范围差异巨大，**实施前必须确认**：

### Q1. Askfor 的交互语义 ✅ 已确认

> **用户决定**：采用同步阻塞方案。Askfor 必须无超时（用户可能几小时后才回来）。

**最终方案**：同步阻塞 + **S2 持久化 + 重连** 组合（详见 §4.7 Askfor 子方案专章）。

**关键决策**：
- ✅ clarify_callback 在 Bridge 后台线程同步阻塞，复用 hermes-agent 自带的 `tools/clarify_gateway.py`
- ✅ 工作流线程仍阻塞，但通过独立 `clarifyWaitExecutor` 池与 `nodeExecutor` 解耦
- ✅ clarify 触发时把问题持久化到 `WorkflowPause` 表，前端连接断开后可重连续接
- ✅ 用户答题后调 `POST /runs/{runId}/resume` 把答案送回 Bridge 唤醒阻塞线程
- ✅ 所有层超时改为 24h（兜底），但**靠 SSE comment 心跳 + 重连机制保证"无超时体验"**

### Q2. 实时型思考过程的数据源

| 方案 | 数据源 | 描述 |
|---|---|---|
| A. 复用 step_callback | 每轮 LLM 调用前的进度 | 颗粒度粗（一轮一次） |
| B. 复用 tool_progress_callback | tool.started / reasoning.available | 颗粒度细，但语义偏工具 |
| C. 复用 thinking_callback | TUI spinner 文案 | 接近用户期望，但当前触发位置较少 |
| D. 全部都接 | 前端按优先级合并展示 | 最完整，但事件流复杂 |

**推荐**：方案 D，前端按 `tool.started > status > step > thinking_spinner` 优先级合并到同一"当前状态"槽位。

### Q3. TODO 数据提取方式

| 方案 | 描述 |
|---|---|
| A. 解析 todo 工具调用结果 | Bridge 识别 tool_name='todo' 时，把 tool_end 的 result_summary（截断后的 JSON）解析为 TODO 列表 |
| B. 改 todo 工具不被截断 | 给 Bridge 加白名单，`todo` 工具的 result 不做 500 字截断，原样转发 |

**推荐**：方案 B。方案 A 会因为 500 字截断丢失数据。

### Q4. 三个执行入口是否统一支持

- 智能操作节点（`HermesSmartActionExecutor`）
- Agent / Skill 节点（`HermesAgentExecutor`）
- 技能生成（`SkillGenerationServiceImpl`）

**推荐**：三个全部支持。前两个走同一 `HermesBridgeClient` 通道，零额外成本；技能生成也调用同一 Bridge，只需在 `SkillGenerationController` 的 SSE 转发处补几个事件类型。

### Q5. 前端是改造现有面板还是新建组件

| 方案 | 描述 |
|---|---|
| A. 改造 WorkflowEditor.vue 的 `node-result-card` | 在现有卡片内增加 4 个折叠区 |
| B. 抽出 `AgentThinkingPanel.vue` 公共组件 | 三处入口（WorkflowEditor/SkillGeneration/RunHistory）共用 |

**推荐**：方案 B。三处现有实现高度重复，借这次重构统一收口。

### Q6. 历史回放是否支持新增 4 类信息

`RunHistory.vue` 从持久化 `ExecutionLog` 回放。新增 4 类信息若也要在历史中回看，需要扩展 `ExecutionLog.Type` 枚举并持久化。

**推荐**：扩展枚举，支持回放。代价小（加几个枚举值），价值大（用户体验一致）。

---

## 4. 分阶段实施计划

> **方案前提**：Q1=同步阻塞（S2 持久化+重连）、Q2=D 全部都接、Q3=B 不截断、Q4=全部入口、Q5=B 抽公共组件、Q6=扩展枚举支持回放。

### 阶段 1：Bridge 透传层扩展（Python 侧）

**目标**：把 Hermes 已有的 6 类回调全部接出来，作为新 SSE 事件转发；同时挂载同步阻塞的 clarify_callback。

**改动文件**：
- `backend/hermes-bridge/hermes_bridge.py`

**改动点**：

1. **回调注册**（`hermes_bridge.py:332-340` 附近）：
   - `step_callback` → 发 `step` 事件，含 `{iteration, prev_tools}`
   - `status_callback` → 发 `status` 事件，含 `{category, message}`
   - `thinking_callback` → 发 `thinking_status` 事件（注意与现有 `text` 区分），含 `{content}`
   - `tool_progress_callback` → 发 `tool_progress` 事件，含 `{event_name, ...detail}`
   - `clarify_callback` → 调用 `wait_for_response`，发 `clarify_request` 事件，含 `{clarify_id, question, choices}`；阻塞等待 `/clarify/{clarify_id}/answer` POST
   - `tool_complete_callback` 增强：识别 `tool=='todo'` 时发独立 `todo_update` 事件，content 为完整 TODO JSON

2. **todo 工具白名单**（`hermes_bridge.py:297` 附近）：截断逻辑前加判断 `if tool == 'todo': result_summary = json.dumps(result)`，跳过 500 字截断

3. **新增端点**：`POST /clarify/{clarify_id}/answer`
   - 接收 `{answer: <string>}`
   - 调用 `tools.clarify_gateway.resolve_gateway_clarify(clarify_id, answer)` 唤醒阻塞线程
   - 不做超时检查——Bridge 端 clarify_callback 阻塞时长由 Java 配置控制

4. **`wait_for_response` 无超时改造**（`tools/clarify_gateway.py:103-143`）：增加 `timeout is None` 分支，跳过 deadline 检查，仅靠 `Event.wait` 切片轮询维持 watchdog 心跳

**验证**：
- 启动 hermes-bridge，用 `curl` POST `/run` 发起请求，断言 SSE 流中能看到所有新事件类型
- 单元测试：mock AIAgent 触发各 callback，断言 Bridge 转发正确

### 阶段 2：Java 接收层扩展

**目标**：`HermesBridgeClient` 能解析新 SSE 事件，并通过 sink 推给工作流引擎。

**改动文件**：
- `backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java`
- `backend/src/main/java/com/agent/management/engine/WorkflowRunEvent.java`
- `backend/src/main/java/com/agent/management/engine/ExecutionLog.java`
- `backend/src/main/java/com/agent/management/engine/NodeStreamSink.java`

**改动点**：

1. **`WorkflowRunEvent.nodeStream` 的 `kind` 枚举扩展**：
   - 现有：`thinking | tool_call | tool_result`
   - 新增：`status | step | tool_progress | todo_update | clarify_request`

2. **`ExecutionLog.Type` 枚举扩展**：
   - 新增：`STATUS | STEP | TOOL_PROGRESS | TODO_UPDATE | CLARIFY_REQUEST`

3. **`HermesBridgeClient.parseSseResponse` 的 switch 增加新分支**：
   - `step` → `sink.emit(nodeId, "step", json, null)` + 写入 `logs(STEP)`
   - `status` → 同上 kind=`status`
   - `tool_progress` → 同上 kind=`tool_progress`
   - `todo_update` → 同上 kind=`todo_update`，content 为完整 TODO JSON
   - `clarify_request` → **特殊处理**（见阶段 4）：解析 `{clarify_id, question, choices}` → `sink.emit(nodeId, "clarify_request", json, null)` + **不结束 SSE 读取循环**，继续 readLine 等待后续事件

4. **`HermesBridgeClient.run` 增加超时配置**：
   - `conn.setReadTimeout(86_400_000)` — 24h（兜底，正常情况靠重连机制）

5. **新增方法**：`HermesBridgeClient.submitClarifyAnswer(runId, clarifyId, answer)`
   - HTTP POST 到 `http://127.0.0.1:18731/clarify/{clarifyId}/answer`

**验证**：
- 单元测试：mock SSE 流含新事件，断言 sink.emit 被以正确 kind 调用
- 集成测试：跑含 todo 工具的工作流，断言 logs 中出现 TODO_UPDATE 条目

### 阶段 3：引擎推送层适配 + clarify 持久化

**目标**：`WorkflowLevelExecutor` 把新 kind 通过 SSE 推给前端；引入独立的 clarify 等待池与暂停持久化。

**改动文件**：
- `backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java`
- `backend/src/main/java/com/agent/management/engine/WorkflowEngine.java`
- `backend/src/main/java/com/agent/management/controller/WorkflowController.java`
- `backend/src/main/java/com/agent/management/external/SseEventBus.java`
- `backend/src/main/java/com/agent/management/model/entity/WorkflowRun.java`
- `backend/src/main/java/com/agent/management/model/entity/WorkflowPause.java` **（新增）**
- `backend/src/main/java/com/agent/management/engine/repository/WorkflowPauseRepository.java` **（新增）**

**改动点**：

1. **`node_stream` 事件透传**（`WorkflowLevelExecutor.java:196` 附近）：现有 sink lambda 已能包装任意 kind，无需特殊处理

2. **独立 clarify 等待池**（`WorkflowLevelExecutor.java` 顶部新增字段）：
   ```java
   private static final ExecutorService clarifyWaitExecutor =
       Executors.newCachedThreadPool(r -> {
         Thread t = new Thread(r, "clarify-wait");
         t.setDaemon(true);
         return t;
       });
   ```
   - 在 `HermesAgentExecutor` 接收到 `clarify_request` 事件后，把当前线程的等待动作 `CompletableFuture.supplyAsync(..., clarifyWaitExecutor).join()` 转移线程，让 `nodeExecutor` 池立即释放槽位

3. **持久化暂停点**：`WorkflowPause` 实体字段：
   ```
   id / run_id / node_id / clarify_id / question / choices(JSON) /
   created_at / answered_at / answer / status(UNANSWERED/ANSWERED)
   ```
   - `HermesAgentExecutor` 收到 `clarify_request` 时 INSERT 一条
   - 用户答题后 UPDATE 状态

4. **主超时延长**：`EXECUTION_TIMEOUT_MINUTES = 1440L`（24h 兜底）

5. **SseEmitter 超时延长**：`WorkflowController.java:203` 改为 `new SseEmitter(86_400_000L)`，与 `SseEventBus.SSE_TIMEOUT_MS` 同步

6. **新增 Controller 端点**：
   - `POST /api/workflows/runs/{runId}/resume`：接收 `{clarifyId, answer}` → 调 `HermesBridgeClient.submitClarifyAnswer` → 更新 `WorkflowPause` 状态
   - `GET /api/workflows/runs/{runId}/stream`：**关键新增**——按 runId 重订阅 SseEventBus（复用外部 API 已有的 Last-Event-ID 机制），断线重连入口
   - `GET /api/workflows/runs/{runId}/state`：返回 `{status, currentPause: {clarifyId, question, choices} | null}`，供前端轮询恢复

7. **SseEventBus 调优**：
   - `MAX_EVENTS_PER_RUN` 调大到 5000（clarify 期间累积事件可能超 1000）
   - `TTL_AFTER_COMPLETION_MS` 改为 25 小时（确保用户回来后还能看到完整重放）

8. **SSE 心跳**：在 `WorkflowController.run()` 启动后，并发启动一个 `ScheduledExecutorService`，每 30 秒向 SseEmitter 发送 `event().comment(":keep-alive")`，防止 NAT/防火墙清连接

**验证**：
- 端到端：前端打开 SSE，跑含 clarify 的工作流，断开网络再重连续接
- 单元测试：clarifyWaitExecutor 解耦后，16 个并发 clarify 不阻塞普通节点

### 阶段 4：前端 composable 扩展

**目标**：`useWorkflowRunner.js` 能接收新事件、维护对应状态、支持断线重连。

**改动文件**：
- `frontend/src/composables/useWorkflowRunner.js`
- `frontend/src/api/workflow.js`

**改动点**：

1. **解除 `useWorkflowRunner.js:184` 对 `tool_call`/`tool_result` 的忽略**

2. **扩展 `nodeResults[i]` 字段**：
   ```js
   {
     streamingThinking: '',           // ① 已有
     currentStatus: '',               // ② 实时型，覆盖式
     currentTool: null,               // ③ 当前调用中的工具
     toolHistory: [],                 // ③ 历史工具调用
     todos: [],                       // ④ TODO 列表
     pendingAsk: null,                // ⑤ Askfor 问题
   }
   ```

3. **`handleSSEEvent` 增加分支**：
   - `kind=status|step|tool_progress` → 更新 `currentStatus`（优先级：tool_progress > status > step）
   - `kind=tool_call` → 推入 `currentTool`，unshift 到 `toolHistory`
   - `kind=tool_result` → 清空 `currentTool`，更新 `toolHistory[0].result`
   - `kind=todo_update` → 解析 JSON 覆盖 `todos`
   - `kind=clarify_request` → 解析 `{clarifyId, question, choices}` → 设置 `pendingAsk`

4. **新增 `submitClarify(clarifyId, answer)` 方法**：
   - POST 到 `/api/workflows/runs/{runId}/resume`
   - 成功后清空 `pendingAsk`

5. **关键：断线重连逻辑**：
   - `useWorkflowRunner.js:100-122` 的 `reader.read()` 抛错时不再立即报失败
   - 改为：记录当前 `lastEventId`（从 SSE 响应头或事件 id 字段提取）→ 5 秒后调用 `GET /api/workflows/runs/{runId}/stream?lastEventId=N` 重连
   - 重连失败 5 次后回退到 `GET /api/workflows/runs/{runId}/state` 轮询（每 30 秒），发现 PAUSED 则渲染 Askfor UI

**验证**：
- vitest 单元测试：构造事件序列，断言 reactive 状态正确
- 浏览器手测：跑工作流，用 DevTools 的 Network 模拟离线，断言自动重连

### 阶段 5：前端公共组件抽取

**目标**：新建 `AgentThinkingPanel.vue`，三处入口共用。

**新建文件**：
- `frontend/src/components/agent/AgentThinkingPanel.vue` 主容器
- `frontend/src/components/agent/AskforBlock.vue`
- `frontend/src/components/agent/StatusBlock.vue`
- `frontend/src/components/agent/CurrentToolBlock.vue`
- `frontend/src/components/agent/ThinkingFoldBlock.vue`
- `frontend/src/components/agent/TodoFoldBlock.vue`
- `frontend/src/components/agent/ToolHistoryFoldBlock.vue`

**改动文件**：
- `frontend/src/views/workflow/WorkflowEditor.vue`：替换 `2293-2304` 的 `<pre>` 区为 `<AgentThinkingPanel :node-result="r" ... />`
- `frontend/src/views/skill/SkillGeneration.vue`：替换 `437-494` 的 log-section 为 `<AgentThinkingPanel :agent-state="..." />`
- `frontend/src/views/workflow/RunHistory.vue`：替换 `300-308` 为 `<AgentThinkingPanel mode="replay" :logs="..." />`

**AgentThinkingPanel.vue 设计**：
```vue
<template>
  <div class="agent-thinking-panel">
    <!-- ⑤ Askfor（最上方，最显眼，仅在 pendingAsk 时出现） -->
    <AskforBlock v-if="state.pendingAsk" :ask="state.pendingAsk" @answer="onAnswer" />

    <!-- ② 实时型思考（覆盖式，带脉冲动画） -->
    <StatusBlock v-if="state.currentStatus" :text="state.currentStatus" live />

    <!-- ③ 工具调用：当前进行中（覆盖式） -->
    <CurrentToolBlock v-if="state.currentTool" :tool="state.currentTool" />

    <!-- ① 追加型思考（折叠，自动滚到底） -->
    <ThinkingFoldBlock :text="state.streamingThinking" :expanded="expandedThinking" />

    <!-- ④ TODO List（折叠） -->
    <TodoFoldBlock v-if="state.todos.length" :todos="state.todos" :expanded="expandedTodo" />

    <!-- ③ 工具调用历史（折叠） -->
    <ToolHistoryFoldBlock :tools="state.toolHistory" :expanded="expandedHistory" />
  </div>
</template>
```

**AskforBlock.vue 关键设计**：
- 使用 NRadioGroup 列出模型提供的选项
- **末尾固定追加一项**："自定义输入"（标签可改）
- 用户选择"自定义输入"时展开 NInput + NButton，提交自定义文本
- 提交按钮调用 `emit('answer', value)`，由父组件调 `submitClarify(clarifyId, value)`
- 提交后按钮禁用 + 显示 spinner，等待节点继续执行后 Askfor 区域自然消失
- 包含一个 `state.currentStatus = '等待用户回答'` 的视觉提示，让用户感受到"系统正在等"

**子组件复用清单**：
- `StatusBlock`：复用 `nrc-thinking-live` 脉冲动画（`WorkflowEditor.vue:2991-2995, 3027-3037`）
- `CurrentToolBlock`：单卡片 + spinner
- `ThinkingFoldBlock`：复用 `.nrc-thinking` 样式 + 自动滚动（借鉴 `SkillGeneration.vue:259-264`）
- `TodoFoldBlock`：复用 `RagChatPanel.vue:22-29` 的 `.step` 样式
- `ToolHistoryFoldBlock`：复用 `SkillGeneration.vue:446-463` 的 `tool-item` 样式

**验证**：
- 三处入口手测：跑同一工作流，肉眼确认 5 区域都正常
- 历史回放：跑完后进 RunHistory，确认 5 区域都能从 logs 回放
- Askfor 流程：模型触发 clarify → Askfor UI 出现 → 用户选择 → Askfor 消失 → 工作流继续

### 阶段 6：端到端测试与回归

**验证清单**：
- [ ] 智能操作节点跑通：5 区域全部正常显示
- [ ] Agent/Skill 节点跑通：同上
- [ ] 技能生成跑通：同上
- [ ] RunHistory 回放：5 区域从持久化 logs 还原
- [ ] Askfor 模拟离线场景：触发 clarify → 关闭浏览器 → 5 分钟后重开 → 通过 `/state` 接口恢复 Askfor UI → 提交答案 → 工作流继续
- [ ] Askfor 8 小时长等待：触发 clarify 后维持连接 8 小时（含心跳），用户答题后正常恢复
- [ ] 大流量压测：长文本思考过程、高频 tool_progress 事件不卡 UI
- [ ] 错误场景：Bridge 异常断开、SSE 超时、24h 兜底 abort

---

## 4.7 Askfor 子方案专章：S2 持久化 + 重连

### 时序图（正常路径）

```
Hermes agent   Bridge(daemon)   Bridge(主)   Java Executor   SseEmitter   前端
    │              │                │             │              │           │
    │ clarify_tool │                │             │              │           │
    │─────────────>│                │             │              │           │
    │              │ Event.wait     │             │              │           │
    │              │ +queue.put     │             │              │           │
    │              │ (clarify_req)  │             │              │           │
    │              │───────────────>│ yield SSE   │              │           │
    │              │                │────────────>│ emit sink    │           │
    │              │                │             │────────────>│ event     │
    │              │                │             │              │──────────>│
    │              │                │             │ INSERT pause │           │
    │              │                │             │              │           │ 渲染 Askfor UI
    │              │                │             │              │           │
    │              │   ...用户离开 8 小时...      │              │           │
    │              │                │             │              │  TCP 断   │ reader.read()抛错
    │              │                │             │              │           │
    │              │                │             │              │           │ 5秒后重连
    │              │                │             │              │<──────────│ GET /stream
    │              │                │             │              │           │ ?lastEventId=N
    │              │                │             │              │──────────>│ 续接 SSE
    │              │                │             │              │           │
    │              │                │             │              │           │ 用户答题
    │              │                │             │ POST /resume │           │
    │              │                │             │<─────────────│<──────────│
    │              │                │             │ POST /answer │           │
    │              │                │             │─────────────>│           │
    │              │ resolve        │             │              │           │
    │              │ Event.set()    │             │              │           │
    │              │<───────────────│             │              │           │
    │              │                │             │              │           │
    │              │ 继续执行       │             │              │           │
    │<─────────────│                │             │              │           │
    │ 后续节点... │                │             │              │           │
```

### 各层超时设置

| 层 | 配置 | 数值 | 说明 |
|---|---|---|---|
| clarify_callback | `wait_for_response` 的 timeout 参数 | `None`（无限） | 改造 `clarify_gateway.py` 支持 None 分支 |
| HttpURLConnection readTimeout | `HermesBridgeClient.java:198` | `86_400_000` ms（24h） | 仅兜底，正常情况由 SSE 心跳维持 |
| SseEmitter | `WorkflowController.java:203` | `86_400_000` ms（24h） | 仅兜底 |
| SseEventBus SSE_TIMEOUT_MS | `SseEventBus.java:41` | `86_400_000` ms（24h） | 仅兜底 |
| WorkflowLevelExecutor 主超时 | `WorkflowLevelExecutor.java:56` | `1440L` 分钟（24h） | 仅兜底，防止永久死锁 |
| SseEventBus 完成后 TTL | `SseEventBus.java:38` | `90_000_000` ms（25h） | 完成后保留 1h+ 以便用户回看 |
| WorkflowPause DB 记录保留 | 定时清理任务 | 30 天 | 超过 30 天未回答视为放弃 |

**重要说明**：所有"24h"超时都是**兜底**——正常情况下用户答题几秒内完成，靠 SSE 心跳 + 重连机制保证体验上的"无超时"。24h 是为了防止用户彻底失联时占用资源。

### SSE 心跳设计

**位置**：`WorkflowController.java` 新增 SseEmitter 后立即启动心跳任务。

**实现**：
```java
ScheduledFuture<?> heartbeat = heartbeatScheduler.scheduleAtFixedRate(() -> {
  try {
    emitter.send(SseEmitter.event().comment(":keep-alive"));
  } catch (IOException e) {
    // 连接已断，停止心跳
  }
}, 30, 30, TimeUnit.SECONDS);
```

**心跳周期 30 秒**：
- < 60 秒：超过大多数 NAT 设备的 TCP 连接空闲超时
- > 10 秒：避免过度网络流量

### 工作流线程占用分析

**关键改进**：独立 `clarifyWaitExecutor` 池

**对比 S1 vs S2（含独立池）**：

| 维度 | S1（拉长超时） | S2（持久化+重连+独立池） |
|---|---|---|
| 用户离开 8h 时节点 worker 池占用 | 占满 8h | 0（已转移 clarifyWaitExecutor） |
| 用户离开 8h 时 clarifyWaitExecutor 池占用 | N/A | 每个阻塞 clarify 占 1 个（CachedThreadPool 无上限，但有 24h 兜底） |
| TCP 断开后服务端行为 | 24h 后才 abort | 用户答题前一直等（合理），24h 兜底 |
| 用户回来后体验 | 工作流已 FAILED，需重跑 | 自动重连续接，答题后继续 |

### 失败模式与降级

1. **用户彻底不回来（>24h）**：触发兜底 abort → WorkflowRun.status=FAILED → WorkflowPause.status=UNANSWERED → 前端展示"已超时放弃"
2. **Bridge 进程崩溃**：clarify_callback 阻塞丢失 → Java SSE 读到 EOF → 工作流走正常错误策略（failStrategy）
3. **SseEventBus 容量超限**：MAX_EVENTS_PER_RUN=5000 不够时，丢弃最老的非关键事件（保留 clarify_request）——需在 `SseEventBus.java:32` 加优先级逻辑
4. **数据库不可用**：WorkflowPause 写入失败 → 直接走 S1 行为（不持久化，靠 24h 兜底）

---

## 5. 风险评估

| 风险 | 等级 | 缓解 |
|---|---|---|
| Bridge 透传层改动可能影响既有 thinking 流（高流量场景） | **高** | 加 feature flag（环境变量 `HERMES_BRIDGE_EXPOSE_ALL_CALLBACKS=true`），新回调可灰度开关；保留旧逻辑作 fallback |
| Askfor 同步阻塞方案复杂（持久化 + 重连 + 心跳 + 独立池） | **高** | 分阶段交付：先实现 ①~④（无 Askfor）+ Askfor UI 骨架；Askfor 全链路作为最后阶段，独立可关闭 |
| Hermes 各 callback 的实际触发频率/数据量未充分压测 | **中** | 阶段 1 完成后做一轮压测，前端可加节流（如 tool_progress 每 200ms 合并） |
| 三处入口（WorkflowEditor/SkillGeneration/RunHistory）样式差异大，公共组件抽取后可能风格不一致 | **中** | 阶段 5 完成后逐一人工对比，必要时加 prop 暴露主题差异 |
| clarifyWaitExecutor 无界可能被恶意工作流耗尽 | **中** | 单工作流同时 pending clarify 上限 8 个；超过则拒绝新 clarify，触发 failStrategy |
| SseEventBus 容量超限丢事件导致重连后丢数据 | **中** | 加优先级标记，clarify_request 永不丢弃 |
| todo 工具结果不截断可能导致 SSE 单条过大 | **低** | 单条 TODO 列表通常 <2KB，可接受；如需可加 50KB 上限保护 |
| RunHistory 持久化字段扩展需要数据库迁移 | **低** | ExecutionLog 是 JSON 字段，加枚举值不需要 DDL；WorkflowPause 是新表，需要 Flyway 迁移 |
| 24h 兜底超时仍可能让用户失联 | **低** | 监控指标：当前 pending clarify 数；超过阈值告警 |

---

## 6. 工作量估算

| 阶段 | 后端工时 | 前端工时 | 测试工时 | 合计 |
|---|---|---|---|---|
| 1. Bridge 透传（含 clarify_callback） | 8h | - | 2h | 10h |
| 2. Java 接收（含 clarify_request 处理） | 5h | - | 1h | 6h |
| 3. 引擎推送 + 持久化 + 重连端点 + 心跳 + 独立池 | 10h | - | 3h | 13h |
| 4. 前端 composable（含断线重连） | - | 6h | 2h | 8h |
| 5. 前端公共组件（7 个子组件） | - | 14h | 3h | 17h |
| 6. 端到端测试（含 8h 长等待模拟） | - | - | 6h | 6h |
| **合计** | **23h** | **20h** | **17h** | **60h**（约 7.5 人日） |

> 比 v1.0 多 23h，主要来自 Askfor S2 方案的持久化、重连、心跳、独立池。

---

## 7. 不在本期范围

- ❌ 思考过程的 Markdown 渲染（保持纯文本 + 自动滚动，Markdown 化作为后续小迭代）
- ❌ 多 agent 协同的思考过程聚合（当前仅单 agent 场景）
- ❌ 思考过程导出为文本/JSON 文件
- ❌ 思考过程全文搜索
- ❌ TODO List 的手动编辑（仅展示 agent 自己规划的，不允许用户干预）
- ❌ Askfor 多轮追问（当前一轮一答；多轮需扩展 clarify 协议）
- ❌ Askfor 用户回答历史持久化（仅 WorkflowPause 记录最后一次）
- ❌ WebSocket 改造（本期保留 SSE + POST 答案架构，未来若有强实时性需求再评估）

---

## 8. 变更记录

| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0 | 2026-08-02 | 初稿，待用户确认 Q1~Q6 |
| v1.1 | 2026-08-02 | Q1 已确认=同步阻塞方案；新增"无超时"要求；调研 S1/S2/S3 三方案后确定采用 **S2 持久化+重连**；新增 §4.7 Askfor 子方案专章；工时从 37h 上调至 60h |
