hermes-thinking-display-overhaul-plan.md 31 KB

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_callbackhermes_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.pywait_for_responseclarify_gateway.py:103-143)用 1 秒切片 Event.wait 轮询 + touch_activity_if_due 防 watchdog,配合 resolve_gateway_clarifyclarify_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.nodeStreamkind 枚举扩展

    • 现有: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 增加新分支

    • stepsink.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 顶部新增字段):

    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:184tool_call/tool_result 的忽略

  2. 扩展 nodeResults[i] 字段

    {
     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-122reader.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 设计

<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-463tool-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 后立即启动心跳任务。

实现

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