# 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] ←
纯文本追加
```
### 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: }`
- 调用 `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` 的 `` 区为 ` `
- `frontend/src/views/skill/SkillGeneration.vue`:替换 `437-494` 的 log-section 为 ` `
- `frontend/src/views/workflow/RunHistory.vue`:替换 `300-308` 为 ` `
**AgentThinkingPanel.vue 设计**:
```vue
```
**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 |