Kaynağa Gözat

1. 重构了智能体运行展示区,将原本单一的流式思考过程拆分为实时状态、思考过程、工具调用、待办清单、需要您的确认等独立面板,节点运行完毕后默认折叠,便于聚焦关键信息;
2. 实现了 AskFor 暂停-恢复机制,智能体运行中遇到需要用户确认的问题时整个工作流会暂停等待,用户在展示区直接选择选项或自定义输入后才会继续运行,且不设超时,避免长时间离开后任务失败;
3. 实现了运行过程断线重连能力,刷新页面或网络抖动后可自动恢复到中断位置继续显示,SSE 连接超时延长到 24 小时,匹配用户长时间挂机的真实场景;
4. 优化了运行记录页面,使其与工作流编辑器的运行结果展示完全一致,包含思考过程、工具调用、待办清单、AskFor 选项等完整内容的回放,便于事后追溯;
5. 将工作空间文件列表和运行 ID 从输出节点结果中独立出来,作为运行的属性展示,与“下载工作空间”按钮归到一起,最终输出结果更清爽;
6. 统一了运行展示区的视觉风格,移除所有 emoji 表情、统一了括号内字体、过滤了 Hermes 输出中的颜文字,整体改为深色风格,与工作流深蓝主题保持一致;
7. 调整了展示面板的顺序为「实时状态 → 思考过程 → 待办清单 → 工具调用 → 执行日志 → 最终结果」,节点运行结束后实时状态自动隐藏;
8. 优化了 AskFor 自定义输入框的样式,与选项按钮保持一致的尺寸和字体,提交按钮改为与“保存”一致的纯色按钮;
9. 新增了多份参考与设计文档,包括 Dify 工作流节点参考、Hermes 思考过程展示改造方案等,便于后续维护与第三方依赖升级。

weisijie 2 ay önce
ebeveyn
işleme
af6f907d92
34 değiştirilmiş dosya ile 6251 ekleme ve 288 silme
  1. 161 6
      backend/hermes-bridge/hermes_bridge.py
  2. 3 0
      backend/src/main/java/com/agent/management/controller/RunHistoryController.java
  3. 161 4
      backend/src/main/java/com/agent/management/controller/WorkflowController.java
  4. 48 1
      backend/src/main/java/com/agent/management/engine/ExecutionLog.java
  5. 12 7
      backend/src/main/java/com/agent/management/engine/NodeStreamSink.java
  6. 56 7
      backend/src/main/java/com/agent/management/engine/WorkflowEngine.java
  7. 144 10
      backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java
  8. 15 7
      backend/src/main/java/com/agent/management/engine/WorkflowRunEvent.java
  9. 3 49
      backend/src/main/java/com/agent/management/engine/executor/OutputExecutor.java
  10. 158 1
      backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java
  11. 15 1
      backend/src/main/java/com/agent/management/external/ExternalApiController.java
  12. 20 7
      backend/src/main/java/com/agent/management/external/SseEventBus.java
  13. 75 0
      backend/src/main/java/com/agent/management/model/entity/WorkflowPause.java
  14. 9 1
      backend/src/main/java/com/agent/management/model/entity/WorkflowRun.java
  15. 24 0
      backend/src/main/java/com/agent/management/repository/WorkflowPauseRepository.java
  16. 8 0
      backend/src/main/java/com/agent/management/repository/WorkflowRunRepository.java
  17. 675 0
      docs/design/agent-chat-page-design.md
  18. 538 0
      docs/plans/hermes-thinking-display-overhaul-plan.md
  19. 647 0
      docs/reference/dify-workflow-nodes-reference.md
  20. 247 0
      docs/reference/hermes-agent-upgrade-reference.md
  21. 1257 0
      docs/reference/jizhi-platform-reference.md
  22. 32 0
      frontend/src/api/workflow.js
  23. 58 0
      frontend/src/components/agent-thinking/AgentThinkingPanel.vue
  24. 217 0
      frontend/src/components/agent-thinking/AskforPanel.vue
  25. 44 0
      frontend/src/components/agent-thinking/RunReconnectBadge.vue
  26. 52 0
      frontend/src/components/agent-thinking/ThinkingLive.vue
  27. 149 0
      frontend/src/components/agent-thinking/TodoListPanel.vue
  28. 253 0
      frontend/src/components/agent-thinking/ToolCalls.vue
  29. 328 48
      frontend/src/composables/useWorkflowRunner.js
  30. 103 0
      frontend/src/utils/hermesThinking.js
  31. 155 0
      frontend/src/utils/runResultReplay.js
  32. 257 94
      frontend/src/views/workflow/RunHistory.vue
  33. 183 45
      frontend/src/views/workflow/WorkflowEditor.vue
  34. 144 0
      prompt.md

+ 161 - 6
backend/hermes-bridge/hermes_bridge.py

@@ -123,6 +123,14 @@ _agent_lock = threading.Lock()
 _agents: "OrderedDict[str, object]" = OrderedDict()
 _AGENT_CACHE_MAX = 8  # 最大缓存实例数;超出按 FIFO 淘汰,防止无界增长
 
+# ---------------------------------------------------------------------------
+# 全局 clarify 会话映射:clarify_id → session_key
+# 用于 POST /clarify/{clarify_id}/answer 端点定位 _ClarifyEntry
+# (session_key 在 register 时与 clarify_id 绑定,可直接 resolve)
+# ---------------------------------------------------------------------------
+_clarify_lock = threading.Lock()
+_clarify_ids: "set[str]" = set()  # 仅用于跟踪 Bridge 模式发起的 clarify_id
+
 # ---------------------------------------------------------------------------
 # 请求模型
 # ---------------------------------------------------------------------------
@@ -280,7 +288,21 @@ def _load_hermes_config():
 
 def _run_agent_stream(agent, system_prompt: str, user_message: str, run_id: str,
                      working_dir: Optional[str] = None, session_id: Optional[str] = None):
-    """运行 Agent 并生成 SSE 事件流"""
+    """运行 Agent 并生成 SSE 事件流
+
+    事件类型(v1.1 扩展):
+      - text:               流式文本片段(LLM 输出)
+      - tool_start:         工具调用开始(含工具名与参数)
+      - tool_end:           工具调用结束(含截断后的结果摘要)
+      - todo_update:        todo 工具的完整 JSON 输出(不截断,独立事件)
+      - step:               每轮 LLM 调用前进度回调
+      - status:             生命周期/警告类状态消息
+      - thinking_status:    TUI 风格思考 spinner 文案(与 text 区分)
+      - tool_progress:      工具细粒度进度(tool.started / reasoning.available 等)
+      - clarify_request:    模型请求用户确认的问题(含 clarify_id 与选项)
+      - done:               执行完成,包含最终回复
+      - error:              执行出错
+    """
     queue = []
     done_event = threading.Event()
 
@@ -288,15 +310,106 @@ def _run_agent_stream(agent, system_prompt: str, user_message: str, run_id: str,
         """接收流式文本片段"""
         queue.append(("text", {"content": delta}))
 
-    def on_tool_start(tool_name: str, args: dict):
-        """工具开始"""
+    def on_tool_start(tool_call_id, tool_name: str, args: dict):
+        """工具开始。
+        注意:Hermes tool_start_callback 实际签名为 (tool_call_id, name, args),
+        必须接收 3 个位置参数,否则会触发 TypeError 被 hermes-agent 静默吞掉。
+        """
         queue.append(("tool_start", {"tool": tool_name, "args": args}))
 
-    def on_tool_end(tool_name: str, result: str):
-        """工具结束"""
-        summary = result[:500] if result else ""
+    def on_tool_end(tool_call_id, tool_name: str, args, result):
+        """工具结束。
+        注意:Hermes tool_complete_callback 实际签名为 (tool_call_id, name, args, result),
+        必须接收 4 个位置参数。result 可能是 str / list / dict,统一转字符串处理。
+        todo 工具特殊处理:不截断、发独立 todo_update 事件。
+        """
+        # result 统一转字符串(OpenAI tool_result 可能是 list/dict)
+        if isinstance(result, str):
+            result_text = result
+        elif result is None:
+            result_text = ""
+        else:
+            try:
+                result_text = json.dumps(result, ensure_ascii=False)
+            except Exception:
+                result_text = str(result)
+
+        if tool_name == "todo":
+            # 完整保留 todo 工具的 JSON 输出(用于前端 TODO List 渲染)
+            queue.append(("todo_update", {"tool": tool_name, "result": result_text}))
+            # 同时发标准 tool_end(保持向后兼容,前端旧逻辑仍能看到调用记录)
+            summary = result_text[:500]
+            queue.append(("tool_end", {"tool": tool_name, "result_summary": summary}))
+            return
+        summary = result_text[:500]
         queue.append(("tool_end", {"tool": tool_name, "result_summary": summary}))
 
+    def on_step(api_call_count: int, prev_tools: list):
+        """每轮 LLM 调用前的进度回调"""
+        queue.append(("step", {
+            "iteration": api_call_count,
+            "prev_tools": list(prev_tools) if prev_tools else [],
+        }))
+
+    def on_status(category: str, message: str):
+        """生命周期/警告类状态消息"""
+        queue.append(("status", {"category": category, "message": message}))
+
+    def on_thinking(content: str):
+        """TUI 风格思考 spinner 文案(如 '🤔 思考中...')
+        与 text 事件区分:text 是 LLM 真实输出片段,thinking_status 是 UI 状态提示"""
+        if content:  # 忽略清空信号(content == ""),避免噪声
+            queue.append(("thinking_status", {"content": content}))
+
+    def on_tool_progress(event_type: str, name: Optional[str] = None,
+                         preview: Optional[str] = None, args: Optional[dict] = None,
+                         **kwargs):
+        """工具细粒度进度事件(tool.started / reasoning.available / _thinking 等)"""
+        queue.append(("tool_progress", {
+            "event_type": event_type,
+            "name": name,
+            "preview": preview,
+            "args": args,
+        }))
+
+    def on_clarify(question: str, choices=None) -> str:
+        """同步阻塞 clarify 回调
+        Bridge 模式下:
+          1. 生成 clarify_id,注册到 clarify_gateway
+          2. 发 clarify_request SSE 事件(Java 收到后会持久化 + 推给前端)
+          3. 阻塞等待 POST /clarify/{clarify_id}/answer 唤醒(无超时)
+          4. 返回用户答案给 agent
+        """
+        import secrets
+        from tools import clarify_gateway
+        clarify_id = secrets.token_hex(8)
+        session_key = f"bridge-{run_id}"
+        clarify_gateway.register(
+            clarify_id=clarify_id,
+            session_key=session_key,
+            question=question,
+            choices=list(choices) if choices else None,
+        )
+        with _clarify_lock:
+            _clarify_ids.add(clarify_id)
+        queue.append(("clarify_request", {
+            "clarify_id": clarify_id,
+            "run_id": run_id,
+            "question": question,
+            "choices": list(choices) if choices else None,
+        }))
+        logger.info("[%s] Clarify 等待用户回答: clarify_id=%s, question=%s", run_id, clarify_id, question[:80])
+        # 无超时等待;实际超时由 Java/HermesBridgeClient 的 24h 兜底控制
+        response = clarify_gateway.wait_for_response(clarify_id, timeout=None)
+        with _clarify_lock:
+            _clarify_ids.discard(clarify_id)
+        if response is None:
+            # 不应发生(None 表示超时,但 timeout=None 永不超时),降级返回空让 agent 自决策
+            logger.warning("[%s] Clarify 意外返回 None: clarify_id=%s", run_id, clarify_id)
+            return ""
+        logger.info("[%s] Clarify 收到回答: clarify_id=%s, response=%s", run_id, clarify_id, response[:80])
+        return response
+
     def _run():
         # 通过 contextvar 设置本次运行的 agent 工作目录;
         # hermes-agent 内 resolve_agent_cwd() 会优先读取此值,
@@ -331,6 +444,11 @@ def _run_agent_stream(agent, system_prompt: str, user_message: str, run_id: str,
         try:
             agent.tool_start_callback = on_tool_start
             agent.tool_complete_callback = on_tool_end
+            agent.step_callback = on_step
+            agent.status_callback = on_status
+            agent.thinking_callback = on_thinking
+            agent.tool_progress_callback = on_tool_progress
+            agent.clarify_callback = on_clarify
 
             result = agent.run_conversation(
                 user_message=user_message,
@@ -345,6 +463,12 @@ def _run_agent_stream(agent, system_prompt: str, user_message: str, run_id: str,
             logger.error("Agent 执行异常: %s", e, exc_info=True)
             queue.append(("error", {"message": str(e)}))
         finally:
+            # 清理本 run 未完成的 clarify(agent 异常退出时不留孤儿阻塞线程)
+            try:
+                from tools import clarify_gateway
+                clarify_gateway.clear_session(f"bridge-{run_id}")
+            except Exception:
+                pass
             if cwd_token is not None:
                 try:
                     _SESSION_CWD.reset(cwd_token)
@@ -416,6 +540,37 @@ async def run_agent(req: RunRequest):
     )
 
 
+@app.post("/clarify/{clarify_id}/answer")
+async def clarify_answer(clarify_id: str, payload: dict):
+    """接收前端送回的 clarify 答案,唤醒阻塞中的 agent 线程。
+
+    Bridge 模式下,agent 调用 clarify 工具时会在 on_clarify 回调里阻塞等待。
+    Java/HermesBridgeClient 收到 clarify_request SSE 事件后持久化到 DB 并推给前端,
+    前端展示 Askfor UI;用户作答后通过 Java 转发到本端点。
+
+    请求体:{"answer": "<string>"}
+    返回:{"ok": true, "resolved": true} 或 {"ok": true, "resolved": false}(已超时/不存在)
+    """
+    from tools import clarify_gateway
+
+    answer = (payload or {}).get("answer", "")
+    if not isinstance(answer, str):
+        answer = str(answer) if answer is not None else ""
+
+    resolved = clarify_gateway.resolve_gateway_clarify(clarify_id, answer)
+    if not resolved:
+        with _clarify_lock:
+            known = clarify_id in _clarify_ids
+        if not known:
+            logger.warning("收到未知 clarify_id 的回答: %s", clarify_id)
+        else:
+            logger.info("clarify_id %s 已被清理(agent 已退出)", clarify_id)
+    else:
+        logger.info("Clarify 回答已送达: clarify_id=%s", clarify_id)
+
+    return {"ok": True, "resolved": resolved}
+
+
 @app.post("/skills/invalidate")
 async def skills_invalidate():
     """

+ 3 - 0
backend/src/main/java/com/agent/management/controller/RunHistoryController.java

@@ -92,6 +92,7 @@ public class RunHistoryController {
         vo.setStatus(run.getStatus());
         vo.setInputs(run.getInputs());
         vo.setOutputs(run.getOutputs());
+        vo.setWorkingDirFiles(run.getWorkingDirFiles());
         vo.setError(run.getError());
         vo.setStartedAt(run.getStartedAt());
         vo.setCompletedAt(run.getCompletedAt());
@@ -117,6 +118,8 @@ public class RunHistoryController {
         private String status;
         private String inputs;
         private String outputs;
+        /** 工作空间文件列表 JSON 数组字符串(应用 .agentignore 过滤后的相对路径) */
+        private String workingDirFiles;
         private String error;
         private Object startedAt;
         private Object completedAt;

+ 161 - 4
backend/src/main/java/com/agent/management/controller/WorkflowController.java

@@ -5,11 +5,17 @@ import com.agent.management.engine.AgentIgnoreFilter;
 import com.agent.management.engine.WorkflowEngine;
 import com.agent.management.engine.WorkflowRunDirManager;
 import com.agent.management.engine.WorkflowRunRequest;
+import com.agent.management.engine.hermes.HermesBridgeClient;
+import com.agent.management.external.SseEventBus;
 import com.agent.management.model.dto.SkillDTO;
 import com.agent.management.model.entity.Workflow;
+import com.agent.management.model.entity.WorkflowPause;
+import com.agent.management.model.entity.WorkflowRun;
 import com.agent.management.model.vo.PageResult;
 import com.agent.management.model.vo.WorkflowVersionVO;
 import com.agent.management.model.vo.WorkflowVO;
+import com.agent.management.repository.WorkflowPauseRepository;
+import com.agent.management.repository.WorkflowRunRepository;
 import com.agent.management.repository.WfCategoryRepository;
 import com.agent.management.service.AiModelService;
 import com.agent.management.service.SkillService;
@@ -23,6 +29,7 @@ import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.UrlResource;
 import org.springframework.http.HttpHeaders;
@@ -59,6 +66,15 @@ public class WorkflowController {
     private final AiModelService aiModelService;
     private final WorkflowVersionServiceImpl workflowVersionService;
     private final AgentIgnoreFilter agentIgnoreFilter;
+    /** v1.1:断线重连入口(GET /runs/{runId}/stream 复用现有 EventBus) */
+    private final SseEventBus sseEventBus;
+    /** v1.1:clarify 持久化查询(GET /state)与回答更新(POST /resume) */
+    private final WorkflowPauseRepository workflowPauseRepository;
+    /** v1.1:通过 runId 反查 workflowId / 状态(GET /state 用) */
+    private final WorkflowRunRepository workflowRunRepository;
+    /** v1.1:调用 Bridge 的 /clarify/{id}/answer 端点(hermes.enabled=false 时为 null) */
+    @Autowired(required = false)
+    private HermesBridgeClient hermesBridgeClient;
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
@@ -197,14 +213,155 @@ public class WorkflowController {
             inputs.put("_preRunId", preRunId);
         }
 
-        // SseEmitter 超时需容纳多节点串行执行(单节点 HTTP 读超时 5 分钟,N 个节点最坏 = N × 5 分钟)。
-        // 注意:future.cancel(true) 无法中断 HttpURLConnection 的阻塞读取,引擎的 300s 上限不构成硬约束。
-        // 故取 30 分钟作为安全余量;如节点更多请相应调大。
-        SseEmitter emitter = new SseEmitter(1_800_000L); // 30 分钟超时
+        // v1.1:SseEmitter 超时从 30 分钟延长到 24 小时(兜底)。
+        // Askfor 场景用户可能几小时后才回来回答 clarify,工作流线程一直阻塞。
+        // 真正的"无超时"由前端断线重连机制(GET /runs/{runId}/stream?lastEventId=N)保证。
+        SseEmitter emitter = new SseEmitter(86_400_000L); // 24 小时
+        // v1.1:每 30 秒发送 SSE 心跳,防止 NAT/防火墙清连接
+        WorkflowController.startHeartbeat(emitter);
         workflowEngine.executeAsync(workflowId, inputs, emitter);
         return emitter;
     }
 
+    /**
+     * v1.1:向 SseEmitter 周期性发送心跳(comment 帧),防止 NAT/防火墙清连接。
+     * 调用方在 emitter 完成后会自动停止调度(onCompletion/onTimeout/onError 回调)。
+     */
+    private static final java.util.concurrent.ScheduledExecutorService HEARTBEAT_SCHEDULER =
+            java.util.concurrent.Executors.newSingleThreadScheduledExecutor(r -> {
+                Thread t = new Thread(r, "sse-heartbeat");
+                t.setDaemon(true);
+                return t;
+            });
+
+    private static void startHeartbeat(SseEmitter emitter) {
+        java.util.concurrent.ScheduledFuture<?>[] handle = new java.util.concurrent.ScheduledFuture<?>[1];
+        Runnable stop = () -> {
+            if (handle[0] != null) handle[0].cancel(false);
+        };
+        handle[0] = HEARTBEAT_SCHEDULER.scheduleAtFixedRate(() -> {
+            try {
+                emitter.send(SseEmitter.event().comment(":keep-alive"));
+            } catch (IOException e) {
+                // 连接已断开,停止心跳
+                stop.run();
+            }
+        }, 30, 30, java.util.concurrent.TimeUnit.SECONDS);
+        emitter.onCompletion(stop);
+        emitter.onTimeout(stop);
+        emitter.onError(ex -> stop.run());
+    }
+
+    /**
+     * v1.1:提交用户对 clarify_request 的回答。
+     * 唤醒 Bridge 中阻塞等待的 Hermes Agent 线程,工作流继续执行。
+     *
+     * @param runId 工作流运行 ID
+     * @param body  {clarifyId, answer}
+     */
+    @PostMapping("/runs/{runId}/resume")
+    public Result<Map<String, Object>> resumeRun(@PathVariable String runId, @RequestBody Map<String, String> body) {
+        String clarifyId = body.get("clarifyId");
+        String answer = body.get("answer");
+        if (clarifyId == null || clarifyId.isBlank()) {
+            return Result.error("缺少 clarifyId");
+        }
+        // 更新 WorkflowPause 状态
+        try {
+            workflowPauseRepository.findByClarifyId(clarifyId).ifPresent(p -> {
+                p.setAnswer(answer);
+                p.setAnsweredAt(java.time.LocalDateTime.now());
+                p.setStatus("ANSWERED");
+                workflowPauseRepository.save(p);
+            });
+        } catch (Exception e) {
+            log.warn("[Resume] 更新 WorkflowPause 失败 clarifyId={}: {}", clarifyId, e.getMessage());
+        }
+        // 转发给 Bridge,唤醒阻塞线程
+        boolean resolved = false;
+        if (hermesBridgeClient != null) {
+            try {
+                resolved = hermesBridgeClient.submitClarifyAnswer(clarifyId, answer);
+            } catch (Exception e) {
+                log.warn("[Resume] 调用 Bridge submitClarifyAnswer 失败 clarifyId={}: {}", clarifyId, e.getMessage());
+            }
+        } else {
+            log.warn("[Resume] HermesBridgeClient 未启用,无法唤醒 clarifyId={}", clarifyId);
+        }
+        return Result.success(Map.of("resolved", resolved));
+    }
+
+    /**
+     * v1.1:断线重连入口 —— 按 runId 重订阅 SseEventBus。
+     *
+     * <p>客户端 reader.read() 抛错时不应直接报失败,而是记录 lastEventId 后通过此端点续接。
+     * 复用 SseEventBus.subscribe 内置的 Last-Event-ID 重放 + 活跃订阅注册。</p>
+     *
+     * @param runId        工作流运行 ID
+     * @param lastEventId  HTTP 头部 Last-Event-ID(或 query 参数)
+     */
+    @GetMapping(value = "/runs/{runId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter reconnectStream(@PathVariable String runId,
+                                       @RequestHeader(value = "Last-Event-ID", required = false) Long lastEventIdHeader,
+                                       @RequestParam(value = "lastEventId", required = false) Long lastEventIdQuery) {
+        Long lastEventId = lastEventIdHeader != null ? lastEventIdHeader : lastEventIdQuery;
+        SseEmitter emitter = sseEventBus.subscribe(runId, lastEventId);
+        if (emitter == null) {
+            // runId 不存在或已超 TTL:返回 404 而非空流
+            emitter = new SseEmitter(60_000L);
+            try {
+                emitter.send(SseEmitter.event().name("workflow_error")
+                        .data(Map.of("error", "运行不存在或已过期: " + runId)));
+                emitter.complete();
+            } catch (IOException ignore) { /* ignore */ }
+            return emitter;
+        }
+        WorkflowController.startHeartbeat(emitter);
+        return emitter;
+    }
+
+    /**
+     * v1.1:查询运行状态 + 当前未答暂停点。
+     *
+     * <p>用户离开几小时后回来时,前端先调此端点:
+     * 若 status=RUNNING 且 currentPause 非空,立即恢复 Askfor UI;
+     * 然后再调 /stream 断线重连续接 SSE。</p>
+     */
+    @GetMapping("/runs/{runId}/state")
+    public Result<Map<String, Object>> getRunState(@PathVariable String runId) {
+        Map<String, Object> state = new LinkedHashMap<>();
+        // 工作流状态:通过 runId 精确查询(避免 findAll 全表扫描)
+        try {
+            java.util.Optional<WorkflowRun> run = workflowRunRepository.findByRunId(runId);
+            if (run.isPresent()) {
+                state.put("status", run.get().getStatus());
+            } else {
+                state.put("status", "UNKNOWN");
+            }
+        } catch (Exception e) {
+            state.put("status", "UNKNOWN");
+        }
+        // 当前未答暂停点
+        Map<String, Object> pause = null;
+        try {
+            java.util.Optional<WorkflowPause> p =
+                    workflowPauseRepository.findFirstByRunIdAndStatusOrderByCreatedAtDesc(runId, "UNANSWERED");
+            if (p.isPresent()) {
+                WorkflowPause wp = p.get();
+                pause = new LinkedHashMap<>();
+                pause.put("clarifyId", wp.getClarifyId());
+                pause.put("nodeId", wp.getNodeId()); // 前端按 nodeId 过滤 AskforPanel
+                pause.put("question", wp.getQuestion());
+                pause.put("choices", wp.getChoices()); // JSON 字符串,前端自行解析
+                pause.put("createdAt", wp.getCreatedAt().toString());
+            }
+        } catch (Exception e) {
+            log.warn("[State] 查询 WorkflowPause 失败 runId={}: {}", runId, e.getMessage());
+        }
+        state.put("currentPause", pause);
+        return Result.success(state);
+    }
+
     /**
      * 下载运行工作目录的 zip 打包
      */

+ 48 - 1
backend/src/main/java/com/agent/management/engine/ExecutionLog.java

@@ -7,7 +7,29 @@ import java.time.Instant;
  */
 public class ExecutionLog {
 
-    public enum Type { INFO, TOOL_CALL, TOOL_RESULT, THINKING, ERROR }
+    /**
+     * 节点执行日志类型。
+     *
+     * <p>v1.1 扩展:新增 STATUS / STEP / TOOL_PROGRESS / TODO_UPDATE / CLARIFY_REQUEST,
+     * 用于 Hermes 思考过程展示重构的 5 区域。前端按 type 分类渲染。</p>
+     */
+    public enum Type {
+        INFO,
+        TOOL_CALL,
+        TOOL_RESULT,
+        THINKING,
+        ERROR,
+        /** 实时状态文案(覆盖式,如"压缩上下文中"、"等待用户回答") */
+        STATUS,
+        /** 每轮 LLM 调用前的进度(iteration / prev_tools) */
+        STEP,
+        /** 工具细粒度进度(tool.started / reasoning.available) */
+        TOOL_PROGRESS,
+        /** todo 工具的完整 JSON 输出,前端解析后渲染 TODO List */
+        TODO_UPDATE,
+        /** 模型请求用户确认(含 clarify_id / question / choices) */
+        CLARIFY_REQUEST,
+    }
 
     private final Type type;
     private final Instant timestamp;
@@ -41,6 +63,31 @@ public class ExecutionLog {
         return new ExecutionLog(Type.ERROR, message, null);
     }
 
+    /** v1.1:实时状态文案(覆盖式) */
+    public static ExecutionLog status(String message) {
+        return new ExecutionLog(Type.STATUS, message, null);
+    }
+
+    /** v1.1:每轮 LLM 调用前的进度(message=iteration序号,detail=prev_tools JSON) */
+    public static ExecutionLog step(int iteration, String prevToolsJson) {
+        return new ExecutionLog(Type.STEP, "iteration " + iteration, prevToolsJson);
+    }
+
+    /** v1.1:工具细粒度进度(message=event_type,detail=preview/args JSON) */
+    public static ExecutionLog toolProgress(String eventType, String detailJson) {
+        return new ExecutionLog(Type.TOOL_PROGRESS, eventType, detailJson);
+    }
+
+    /** v1.1:todo 工具完整输出(detail=完整 JSON) */
+    public static ExecutionLog todoUpdate(String fullJson) {
+        return new ExecutionLog(Type.TODO_UPDATE, "todo", fullJson);
+    }
+
+    /** v1.1:模型请求用户确认(message=question,detail=choices JSON) */
+    public static ExecutionLog clarifyRequest(String clarifyId, String question, String choicesJson) {
+        return new ExecutionLog(Type.CLARIFY_REQUEST, question, "{\"clarify_id\":\"" + clarifyId + "\",\"choices\":" + choicesJson + "}");
+    }
+
     public Type getType() { return type; }
     public Instant getTimestamp() { return timestamp; }
     public String getMessage() { return message; }

+ 12 - 7
backend/src/main/java/com/agent/management/engine/NodeStreamSink.java

@@ -2,14 +2,19 @@ package com.agent.management.engine;
 
 /**
  * 节点执行过程中的流式事件回调。
- * executor 通过它实时推送 thinking / tool_call / tool_result 增量片段,
+ * executor 通过它实时推送各类增量片段,
  * 由引擎包装成 {@code node_stream} SSE 事件转发给前端。
  *
- * <p>语义:</p>
+ * <p>v1.1 kind 取值与语义:</p>
  * <ul>
- *   <li>{@code thinking}    —— LLM 思考过程的增量 token,前端应按 nodeId 聚合累加</li>
- *   <li>{@code tool_call}   —— 工具调用开始(content 为参数摘要)</li>
- *   <li>{@code tool_result} —— 工具调用结束(content 为结果摘要)</li>
+ *   <li>{@code thinking}        —— LLM 思考过程的增量 token,前端按 nodeId 聚合累加</li>
+ *   <li>{@code tool_call}       —— 工具调用开始(content 为参数摘要)</li>
+ *   <li>{@code tool_result}     —— 工具调用结束(content 为结果摘要)</li>
+ *   <li>{@code status}          —— 实时状态文案(覆盖式,如"压缩上下文中")</li>
+ *   <li>{@code step}            —— 每轮 LLM 调用前进度(content 为 iteration+prev_tools JSON)</li>
+ *   <li>{@code tool_progress}   —— 工具细粒度进度(content 为 event_type+preview JSON)</li>
+ *   <li>{@code todo_update}     —— todo 工具完整输出(content 为完整 TODO JSON)</li>
+ *   <li>{@code clarify_request} —— 模型请求用户确认(content 为 clarify_id+question+choices JSON)</li>
  * </ul>
  */
 @FunctionalInterface
@@ -17,8 +22,8 @@ public interface NodeStreamSink {
 
     /**
      * @param nodeId   节点 ID
-     * @param kind     事件类型:"thinking" | "tool_call" | "tool_result"
-     * @param content  增量内容(thinking 片段 / 工具参数摘要 / 工具结果摘要)
+     * @param kind     事件类型:thinking | tool_call | tool_result | status | step | tool_progress | todo_update | clarify_request
+     * @param content  增量内容(按 kind 语义填充)
      * @param toolName 工具名;kind != tool_* 时为 {@code null}
      */
     void emit(String nodeId, String kind, String content, String toolName);

+ 56 - 7
backend/src/main/java/com/agent/management/engine/WorkflowEngine.java

@@ -13,9 +13,12 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.*;
+import java.util.stream.Collectors;
 
 /**
  * 工作流调度引擎
@@ -33,6 +36,7 @@ public class WorkflowEngine {
     private final WorkflowRunDirManager runDirManager;
     private final WorkflowLevelExecutor levelExecutor;
     private final SseEventBus eventBus;
+    private final AgentIgnoreFilter agentIgnoreFilter;
     private final ExecutorService executor = Executors.newFixedThreadPool(
             Runtime.getRuntime().availableProcessors(),
             r -> {
@@ -54,13 +58,15 @@ public class WorkflowEngine {
     public WorkflowEngine(WorkflowRepository workflowRepo,
                           WorkflowRunRepository runRepo, WorkflowRunNodeRepository runNodeRepo,
                           WorkflowRunDirManager runDirManager,
-                          WorkflowLevelExecutor levelExecutor, SseEventBus eventBus) {
+                          WorkflowLevelExecutor levelExecutor, SseEventBus eventBus,
+                          AgentIgnoreFilter agentIgnoreFilter) {
         this.workflowRepo = workflowRepo;
         this.runRepo = runRepo;
         this.runNodeRepo = runNodeRepo;
         this.runDirManager = runDirManager;
         this.levelExecutor = levelExecutor;
         this.eventBus = eventBus;
+        this.agentIgnoreFilter = agentIgnoreFilter;
         log.info("[WorkflowEngine] 已初始化,使用 WorkflowLevelExecutor 处理节点遍历");
     }
 
@@ -169,26 +175,31 @@ public class WorkflowEngine {
             log.error("[WorkflowEngine] levelExecutor 抛出异常: {}", e.getMessage(), e);
             safeSend(emitter, WorkflowRunEvent.workflowError(runId, e.getMessage()));
             persistRunResult(runRecord, new WorkflowLevelExecutor.ExecutionOutcome(
-                    new LinkedHashMap<>(), new ArrayList<>(), true, e.getMessage()));
+                    new LinkedHashMap<>(), new ArrayList<>(), true, e.getMessage()), null);
             emitter.complete();
             return;
         }
 
-        // 7. 持久化 + 推送完成/错误事件
-        persistRunResult(runRecord, outcome);
+        // 7. 扫描工作目录文件列表(成功时才有意义;失败时跳过)
+        List<String> workingDirFiles = outcome.failed ? null : scanWorkingDirFiles(workingDir);
+
+        // 8. 持久化 + 推送完成/错误事件
+        persistRunResult(runRecord, outcome, workingDirFiles);
         if (outcome.failed) {
             emitter.complete();
             return;
         }
-        safeSend(emitter, WorkflowRunEvent.workflowComplete(runId, outcome.finalOutputs));
+        safeSend(emitter, WorkflowRunEvent.workflowComplete(runId, outcome.finalOutputs, workingDirFiles));
         emitter.complete();
         log.info("[WorkflowEngine] 执行完成, runId={}, 输出变量数={}", runId, outcome.finalOutputs.size());
     }
 
     /**
-     * 把执行结果回写到 WorkflowRun + 节点记录表
+     * 把执行结果回写到 WorkflowRun + 节点记录表。
+     * workingDirFiles 仅在成功时由调用方扫描后传入,作为 run 自身的属性持久化(不再注入 outputs JSON)。
      */
-    private void persistRunResult(WorkflowRun runRecord, WorkflowLevelExecutor.ExecutionOutcome outcome) {
+    private void persistRunResult(WorkflowRun runRecord, WorkflowLevelExecutor.ExecutionOutcome outcome,
+                                  List<String> workingDirFiles) {
         runRecord.setStatus(outcome.failed ? "FAILED" : "SUCCESS");
         runRecord.setCompletedAt(LocalDateTime.now());
         if (outcome.failed) {
@@ -199,11 +210,49 @@ public class WorkflowEngine {
             } catch (JsonProcessingException e) {
                 log.warn("[WorkflowEngine] 序列化输出失败", e);
             }
+            if (workingDirFiles != null && !workingDirFiles.isEmpty()) {
+                try {
+                    runRecord.setWorkingDirFiles(MAPPER.writeValueAsString(workingDirFiles));
+                } catch (JsonProcessingException e) {
+                    log.warn("[WorkflowEngine] 序列化工作空间文件列表失败", e);
+                }
+            }
         }
         runRepo.save(runRecord);
         runNodeRepo.saveAll(outcome.nodeRecords);
     }
 
+    /**
+     * 扫描工作目录下应保留的文件列表(应用 .agentignore 过滤),返回相对路径(正斜杠)。
+     * 与原 OutputExecutor 中实现的过滤语义一致:跳过目录自身、命中 .agentignore 的文件及祖先目录命中规则。
+     */
+    private List<String> scanWorkingDirFiles(Path workingDir) {
+        if (workingDir == null || !Files.isDirectory(workingDir)) return Collections.emptyList();
+        try {
+            return Files.walk(workingDir)
+                    .filter(p -> shouldInclude(workingDir, p))
+                    .map(p -> workingDir.relativize(p).toString().replace('\\', '/'))
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            log.warn("[WorkflowEngine] 扫描工作目录失败: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    private boolean shouldInclude(Path workingDir, Path p) {
+        if (Files.isDirectory(p)) return false;
+        Path rel = workingDir.relativize(p);
+        String relStr = rel.toString().replace('\\', '/');
+        if (agentIgnoreFilter.shouldIgnore(relStr, false)) return false;
+        Path cur = rel;
+        while (cur != null && cur.getNameCount() > 1) {
+            cur = cur.getParent();
+            if (cur == null) break;
+            if (agentIgnoreFilter.shouldIgnore(cur.toString().replace('\\', '/'), true)) return false;
+        }
+        return true;
+    }
+
     /**
      * 安全发送 SSE 事件
      */

+ 144 - 10
backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java

@@ -1,22 +1,27 @@
 package com.agent.management.engine;
 
 import com.agent.management.external.SseEventBus;
+import com.agent.management.model.entity.WorkflowPause;
 import com.agent.management.model.entity.WorkflowRunNode;
+import com.agent.management.repository.WorkflowPauseRepository;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import jakarta.annotation.PreDestroy;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
@@ -38,8 +43,10 @@ import java.util.concurrent.atomic.AtomicReference;
  *
  * <p>线程模型:</p>
  * <ul>
- *   <li>{@link #nodeExecutor}:节点 worker 线程池,{@link #executeOneNode} 在此执行</li>
- *   <li>回调链(onNodeCompleted / tryScheduleSuccessors)也在 nodeExecutor 中执行</li>
+ *   <li>{@link #nodeExecutor}:快节点 worker 线程池,{@link #executeOneNode} 在此执行</li>
+ *   <li>{@link #clarifyWaitExecutor}:长阻塞节点专用池,承担 agent / smartAction 等 Hermes 调用,
+ *       避免长时间等待用户 clarify 答题时把 nodeExecutor 占满,导致其他快节点无 worker 可用</li>
+ *   <li>回调链(onNodeCompleted / tryScheduleSuccessors)在节点所在池中执行</li>
  *   <li>主线程仅在 {@code ctx.done.get(...)} 上等待</li>
  * </ul>
  */
@@ -52,15 +59,27 @@ public class WorkflowLevelExecutor {
     private static final ObjectMapper MAPPER = new ObjectMapper()
             .registerModule(new JavaTimeModule());
 
-    /** dataflow 调度主线程等待超时(兜底,避免死锁时永久阻塞) */
-    private static final long EXECUTION_TIMEOUT_MINUTES = 30L;
+    /**
+     * dataflow 调度主线程等待超时(兜底,避免死锁时永久阻塞)。
+     * <p>v1.1:从 30 分钟调到 24 小时。Askfor 场景用户可能几小时后才回来回答 clarify,
+     * 工作流线程一直阻塞。真正的"无超时"由前端断线重连机制保证,这里只是最终兜底。</p>
+     */
+    private static final long EXECUTION_TIMEOUT_MINUTES = 1440L;
 
     private final Map<String, NodeExecutor> executorMap;
     private final SseEventBus eventBus;
     private final NodeWorkspaceBuilder workspaceBuilder;
     /**
-     * 节点执行的线程池。独立于 WorkflowEngine.executor(工作流主线程池),
+     * v1.1:clarify_request 暂停点持久化(断线重连后恢复 Askfor UI)。
+     * 非必需依赖:测试场景或引擎独立测试时允许为 null。
+     */
+    private final WorkflowPauseRepository pauseRepository;
+    /**
+     * 快节点执行的线程池。独立于 WorkflowEngine.executor(工作流主线程池),
      * 避免节点任务与外层 wrapper 争用同一池导致死锁。
+     *
+     * <p>仅承担 userInput / llm / condition / output / skill / knowledgeRetrieval 等
+     * 同步短任务节点;agent / smartAction 等可能长时间阻塞的节点走 {@link #clarifyWaitExecutor}。</p>
      */
     private final ExecutorService nodeExecutor = Executors.newFixedThreadPool(
             Math.max(8, Runtime.getRuntime().availableProcessors() * 2),
@@ -71,8 +90,52 @@ public class WorkflowLevelExecutor {
             }
     );
 
+    /**
+     * 长阻塞节点专用线程池("clarify 等待池")。
+     *
+     * <p>背景:Hermes Agent / SmartAction 通过 SSE 长连接串行读取 Bridge 事件,
+     * 当 Python 侧触发 {@code clarify_request} 时,整个 SSE 读取会一直阻塞到用户答完题
+     * (可能长达数小时)。如果这类节点跑在 {@link #nodeExecutor} 里,多个工作流同时卡 clarify
+     * 会快速耗尽池容量,导致后续快节点(userInput / output 等)拿不到 worker。</p>
+     *
+     * <p>所以把 agent / smartAction 单独剥离到本池:
+     * <ul>
+     *   <li>容量更大({@code max(32, CPU*4)}),因为线程主要在 IO 等待,CPU 占用低</li>
+     *   <li>独立线程名 {@code "clarify-wait-executor"},便于线程 dump 定位</li>
+     *   <li>与 nodeExecutor 互不影响:clarify 长等待不会拖垮快节点调度</li>
+     * </ul></p>
+     */
+    private final ExecutorService clarifyWaitExecutor = Executors.newFixedThreadPool(
+            Math.max(32, Runtime.getRuntime().availableProcessors() * 4),
+            r -> {
+                Thread t = new Thread(r, "clarify-wait-executor");
+                t.setDaemon(true);
+                return t;
+            }
+    );
+
+    /**
+     * 判断节点是否属于"长阻塞"类型,需提交到 {@link #clarifyWaitExecutor}。
+     *
+     * <p>当前覆盖 agent / smartAction:在 hermes.enabled=true 时它们走 SSE 阻塞读,
+     * 可能数小时等 clarify;在 hermes.enabled=false 时它们仍是较重的 LLM 同步调用,
+     * 分到独立池也不会拖慢调度。</p>
+     */
+    private boolean isLongBlockingNode(String nodeType) {
+        return "agent".equals(nodeType) || "smartAction".equals(nodeType);
+    }
+
+    /**
+     * 按节点类型选择执行池:长阻塞节点 → {@link #clarifyWaitExecutor},其他 → {@link #nodeExecutor}。
+     */
+    private ExecutorService selectPoolFor(String nodeType) {
+        return isLongBlockingNode(nodeType) ? clarifyWaitExecutor : nodeExecutor;
+    }
+
+    @Autowired
     public WorkflowLevelExecutor(Map<String, NodeExecutor> injected, SseEventBus eventBus,
-                                  NodeWorkspaceBuilder workspaceBuilder) {
+                                  NodeWorkspaceBuilder workspaceBuilder,
+                                  @Autowired(required = false) WorkflowPauseRepository pauseRepository) {
         // Spring 注入 Map<String, NodeExecutor> 时 key 是 bean name(如 "userInputExecutor"),
         // 而引擎查找用的是 getType() 返回值(如 "userInput")。两者命名不一致,
         // 此处按 getType() 重建 map,避免对 bean 命名的依赖。
@@ -83,12 +146,20 @@ public class WorkflowLevelExecutor {
         this.executorMap = typed;
         this.eventBus = eventBus;
         this.workspaceBuilder = workspaceBuilder;
+        this.pauseRepository = pauseRepository;
         log.info("[WorkflowLevelExecutor] 已注册节点执行器: {}", this.executorMap.keySet());
     }
 
+    /** 兼容旧测试用例:无 pauseRepository 的构造器 */
+    public WorkflowLevelExecutor(Map<String, NodeExecutor> injected, SseEventBus eventBus,
+                                  NodeWorkspaceBuilder workspaceBuilder) {
+        this(injected, eventBus, workspaceBuilder, null);
+    }
+
     @PreDestroy
     public void shutdown() {
         nodeExecutor.shutdown();
+        clarifyWaitExecutor.shutdown();
         try {
             if (!nodeExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                 nodeExecutor.shutdownNow();
@@ -97,6 +168,16 @@ public class WorkflowLevelExecutor {
             Thread.currentThread().interrupt();
             nodeExecutor.shutdownNow();
         }
+        try {
+            // clarify 等待池的线程可能卡在用户长时间未答的 clarify 上,
+            // 给稍长的优雅关闭时间,超时后强制中断(线程是 daemon,不影响 JVM 退出)
+            if (!clarifyWaitExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
+                clarifyWaitExecutor.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            clarifyWaitExecutor.shutdownNow();
+        }
     }
 
     /**
@@ -192,9 +273,13 @@ public class WorkflowLevelExecutor {
                                            String runId, Long runRecordId, SseEmitter emitter) {
         DataflowCtx ctx = new DataflowCtx(dag, context, runId, runRecordId, emitter);
 
-        // 注入流式回调:executor 调用 sink.emit(...) 时实时包装为 node_stream 事件推给前端
-        context.setStreamSink((nodeId, kind, content, toolName) ->
-                safeSend(emitter, WorkflowRunEvent.nodeStream(runId, nodeId, kind, content, toolName)));
+        // 注入流式回调:executor 调用 sink.emit(...) 时实时包装为 node_stream 事件推给前端。
+        // v1.1:包装一层 clarify_request 拦截器,遇到 Askfor 时同步写入 WorkflowPause 表,
+        //      用于前端断线重连后通过 GET /state 恢复 Askfor UI。
+        context.setStreamSink(wrapSinkWithClarifyHook(
+                (nodeId, kind, content, toolName) ->
+                        safeSend(emitter, WorkflowRunEvent.nodeStream(runId, nodeId, kind, content, toolName)),
+                runId));
 
         // === 初始化入度(按 source 去重,与 DagResolver 入度计算规则保持一致) ===
         for (Map.Entry<String, List<DagResolver.EdgeInfo>> e : dag.getIncomingEdges().entrySet()) {
@@ -261,7 +346,7 @@ public class WorkflowLevelExecutor {
 
         CompletableFuture<NodeExecutionResult> future = CompletableFuture.supplyAsync(
                 () -> executeOneNode(nodeId, ctx.dag, ctx.context, ctx.runId, ctx.emitter, ctx.activeEdges),
-                nodeExecutor);
+                selectPoolFor(ctx.dag.getNodeTypeMap().get(nodeId)));
         // putIfAbsent 防止并发场景下重复提交(如多前驱同时归零)
         CompletableFuture<NodeExecutionResult> race = ctx.inFlightFutures.putIfAbsent(nodeId, future);
         if (race != null) {
@@ -692,4 +777,53 @@ public class WorkflowLevelExecutor {
             log.warn("[WorkflowEngine] SSE 推送失败: {}", e.getMessage());
         }
     }
+
+    /**
+     * v1.1:包装 NodeStreamSink,拦截 clarify_request 事件并同步写入 WorkflowPause 表。
+     *
+     * <p>用途:用户离开几小时后回来时,前端断线重连可先调 GET /runs/{runId}/state
+     * 拿到 currentPause,立即恢复 Askfor UI;同时 SSE stream 也会重放 clarify_request 事件,
+     * 两者冗余但可靠。</p>
+     *
+     * <p>容错:DB 写入失败仅记录日志,不阻塞主流程(事件仍会推送给前端)。</p>
+     */
+    private NodeStreamSink wrapSinkWithClarifyHook(NodeStreamSink delegate, String runId) {
+        return (nodeId, kind, content, toolName) -> {
+            // 先转发原始事件,确保前端实时收到
+            delegate.emit(nodeId, kind, content, toolName);
+            // 仅拦截 clarify_request,解析 {clarify_id, question, choices} 并落库
+            if (!"clarify_request".equals(kind) || pauseRepository == null || content == null) {
+                return;
+            }
+            try {
+                JsonNode payload = MAPPER.readTree(content);
+                String clarifyId = payload.path("clarify_id").asText("");
+                String question = payload.path("question").asText("");
+                String choicesJson = payload.has("choices")
+                        ? MAPPER.writeValueAsString(payload.get("choices")) : "[]";
+                if (clarifyId.isEmpty()) {
+                    log.warn("[WorkflowLevelExecutor] clarify_request 缺少 clarify_id: {}", content);
+                    return;
+                }
+                // 同一 clarify_id 重复落库去重(断线重连续传可能再次送达)
+                Optional<WorkflowPause> existing = pauseRepository.findByClarifyId(clarifyId);
+                if (existing.isPresent()) {
+                    return;
+                }
+                WorkflowPause pause = new WorkflowPause();
+                pause.setRunId(runId);
+                pause.setNodeId(nodeId);
+                pause.setClarifyId(clarifyId);
+                pause.setQuestion(question);
+                pause.setChoices(choicesJson);
+                pause.setStatus("UNANSWERED");
+                pause.setCreatedAt(LocalDateTime.now());
+                pauseRepository.save(pause);
+                log.info("[WorkflowLevelExecutor] 已持久化 clarify 暂停点 runId={} clarifyId={} nodeId={}",
+                        runId, clarifyId, nodeId);
+            } catch (Exception e) {
+                log.warn("[WorkflowLevelExecutor] 写入 WorkflowPause 失败(不影响主流程): {}", e.getMessage());
+            }
+        };
+    }
 }

+ 15 - 7
backend/src/main/java/com/agent/management/engine/WorkflowRunEvent.java

@@ -19,19 +19,23 @@ public class WorkflowRunEvent {
     /** 节点执行后的完整 variables 快照(debug 视图,仅 node_status SUCCESS 携带) */
     private final Map<String, Object> contextSnapshot;
     // node_stream 事件专用字段
-    private final String kind;      // thinking | tool_call | tool_result
+    // v1.1 kind 扩展:thinking | tool_call | tool_result | status | step | tool_progress | todo_update | clarify_request
+    private final String kind;
     private final String content;   // 增量内容
     private final String toolName;  // 工具名(kind != tool_* 时为 null)
+    /** workflow_complete 携带的工作空间文件列表(与 WorkflowRun.workingDirFiles 同源,便于前端实时展示) */
+    private final List<String> workingDirFiles;
 
     private WorkflowRunEvent(String runId, String type, String nodeId, String status,
                              Object output, String error, Boolean success, List<ExecutionLog> logs) {
-        this(runId, type, nodeId, status, output, error, success, logs, null, null, null, null);
+        this(runId, type, nodeId, status, output, error, success, logs, null, null, null, null, null);
     }
 
     private WorkflowRunEvent(String runId, String type, String nodeId, String status,
                              Object output, String error, Boolean success, List<ExecutionLog> logs,
                              Map<String, Object> contextSnapshot,
-                             String kind, String content, String toolName) {
+                             String kind, String content, String toolName,
+                             List<String> workingDirFiles) {
         this.runId = runId;
         this.type = type;
         this.nodeId = nodeId;
@@ -44,6 +48,7 @@ public class WorkflowRunEvent {
         this.kind = kind;
         this.content = content;
         this.toolName = toolName;
+        this.workingDirFiles = workingDirFiles;
     }
 
     public static WorkflowRunEvent nodeRunning(String runId, String nodeId) {
@@ -53,7 +58,7 @@ public class WorkflowRunEvent {
     public static WorkflowRunEvent nodeResult(String runId, NodeExecutionResult result) {
         return new WorkflowRunEvent(runId, "node_status", result.getNodeId(),
                 result.getStatus().name(), result.getOutput(), result.getError(), null, result.getLogs(),
-                result.getContextSnapshot(), null, null, null);
+                result.getContextSnapshot(), null, null, null, null);
     }
 
     /**
@@ -63,11 +68,13 @@ public class WorkflowRunEvent {
     public static WorkflowRunEvent nodeStream(String runId, String nodeId, String kind,
                                               String content, String toolName) {
         return new WorkflowRunEvent(runId, "node_stream", nodeId, null, null, null, null, null,
-                null, kind, content, toolName);
+                null, kind, content, toolName, null);
     }
 
-    public static WorkflowRunEvent workflowComplete(String runId, Map<String, Object> outputs) {
-        return new WorkflowRunEvent(runId, "workflow_complete", null, null, outputs, null, true, null);
+    public static WorkflowRunEvent workflowComplete(String runId, Map<String, Object> outputs,
+                                                    List<String> workingDirFiles) {
+        return new WorkflowRunEvent(runId, "workflow_complete", null, null, outputs, null, true, null,
+                null, null, null, null, workingDirFiles);
     }
 
     public static WorkflowRunEvent workflowError(String runId, String error) {
@@ -87,4 +94,5 @@ public class WorkflowRunEvent {
     public String getKind() { return kind; }
     public String getContent() { return content; }
     public String getToolName() { return toolName; }
+    public List<String> getWorkingDirFiles() { return workingDirFiles; }
 }

+ 3 - 49
backend/src/main/java/com/agent/management/engine/executor/OutputExecutor.java

@@ -1,6 +1,5 @@
 package com.agent.management.engine.executor;
 
-import com.agent.management.engine.AgentIgnoreFilter;
 import com.agent.management.engine.NodeExecutionResult;
 import com.agent.management.engine.NodeExecutor;
 import com.agent.management.engine.NodeInputResolver;
@@ -10,17 +9,11 @@ import com.agent.management.engine.WorkflowContext;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * 输出节点执行器
@@ -34,18 +27,17 @@ import java.util.stream.Collectors;
  *   <li>{@code data.fields[]} 每一项作为输入字段解析:field.name 同时作为输出 key(裸名,不带 predId__ 前缀)</li>
  *   <li>未声明 fields:返回空 data(用户决策:严格语义)</li>
  *   <li>required 字段缺失:返回 failure envelope</li>
- *   <li>{@code _workingDirFiles} / {@code _runId} 作为元数据额外注入(用户决策:全部保留)</li>
  * </ul>
+ *
+ * <p>注意:工作空间文件列表({@code _workingDirFiles})和运行 ID({@code _runId})已不再注入 envelope.data,
+ * 改由 WorkflowEngine 在运行完成时作为 WorkflowRun 的属性持久化,前端从 run 对象上直接读取。</p>
  */
 @Slf4j
 @Component
-@RequiredArgsConstructor
 public class OutputExecutor implements NodeExecutor {
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
-    private final AgentIgnoreFilter agentIgnoreFilter;
-
     @Override
     public String getType() {
         return "output";
@@ -81,46 +73,8 @@ public class OutputExecutor implements NodeExecutor {
         }
         // 未声明 fields:projectedData 保持为空(严格语义)
 
-        // 元数据注入(用户决策:全部保留)
-        Path workingDir = context.getWorkingDir();
-        if (workingDir != null && Files.isDirectory(workingDir)) {
-            try {
-                List<String> files = Files.walk(workingDir)
-                        .filter(p -> shouldInclude(workingDir, p))
-                        .map(p -> workingDir.relativize(p).toString().replace('\\', '/'))
-                        .collect(Collectors.toList());
-                if (!files.isEmpty()) {
-                    projectedData.put("_workingDirFiles", files);
-                }
-            } catch (IOException e) {
-                log.warn("[Output] 扫描工作目录失败: {}", e.getMessage());
-            }
-        }
-        projectedData.put("_runId", context.getRunId());
-
         log.info("[Output] 节点 {} 输出类型={}, 变量数={}", nodeId, outputType, projectedData.size());
         return NodeExecutionResult.success(nodeId,
                 NodeOutputEnvelope.success("工作流执行成功", projectedData).toMap());
     }
-
-    /**
-     * 判断路径是否应保留在文件列表中。
-     * 同时跳过目录本身、.agentignore 命中规则(按路径或任意祖先目录命中均算)。
-     */
-    private boolean shouldInclude(Path workingDir, Path p) {
-        if (Files.isDirectory(p)) return false;
-        // 检查路径自身及其每个祖先目录是否被 .agentignore 命中
-        Path rel = workingDir.relativize(p);
-        String relStr = rel.toString().replace('\\', '/');
-        // 自身(作为文件)
-        if (agentIgnoreFilter.shouldIgnore(relStr, false)) return false;
-        // 各级祖先目录(如 .hermes/skills/foo.md 需要被 .hermes/ 命中)
-        Path cur = rel;
-        while (cur != null && cur.getNameCount() > 1) {
-            cur = cur.getParent();
-            if (cur == null) break;
-            if (agentIgnoreFilter.shouldIgnore(cur.toString().replace('\\', '/'), true)) return false;
-        }
-        return true;
-    }
 }

+ 158 - 1
backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java

@@ -195,7 +195,9 @@ public class HermesBridgeClient {
             conn.setRequestProperty("X-Bridge-Token", authToken);
         }
         conn.setConnectTimeout(10000);
-        conn.setReadTimeout(300000);
+        // v1.1:readTimeout 改为 24 小时(兜底),实际 Askfor 长等待由 SSE 心跳 + 前端重连机制保证。
+        // 原 5 分钟超时无法支撑用户离开几小时后再回来回答的场景。
+        conn.setReadTimeout(86_400_000);
 
         // === 请求 DEBUG 日志(仅写入 LLM 日志文件,不输出到控制台) ===
         LLM_HTTP.debug("[Hermes-REQ] ========== 请求开始 ==========");
@@ -304,6 +306,105 @@ public class HermesBridgeClient {
                                 }
                             }
                         }
+                        // v1.1:每轮 LLM 调用前的进度(iteration + prev_tools)
+                        case "step" -> {
+                            int iteration = data.path("iteration").asInt(0);
+                            String prevToolsJson = data.has("prev_tools")
+                                    ? objectMapper.writeValueAsString(data.get("prev_tools")) : "[]";
+                            logs.add(ExecutionLog.step(iteration, prevToolsJson));
+                            if (sink != null) {
+                                try {
+                                    sink.emit(nodeId, "step",
+                                            "{\"iteration\":" + iteration + ",\"prev_tools\":" + prevToolsJson + "}",
+                                            null);
+                                } catch (Exception ex) {
+                                    log.warn("[Hermes] sink 推送 step 失败: {}", ex.getMessage());
+                                }
+                            }
+                        }
+                        // v1.1:实时状态文案(覆盖式,如"压缩上下文中")
+                        case "status" -> {
+                            String message = data.path("message").asText("");
+                            if (!message.isEmpty()) {
+                                logs.add(ExecutionLog.status(message));
+                                if (sink != null) {
+                                    try {
+                                        sink.emit(nodeId, "status", message, null);
+                                    } catch (Exception ex) {
+                                        log.warn("[Hermes] sink 推送 status 失败: {}", ex.getMessage());
+                                    }
+                                }
+                            }
+                        }
+                        // v1.1:thinking_callback 透传(Hermes 思考增量,与 text 区分用于实时思考区展示)
+                        case "thinking_status" -> {
+                            String content = data.path("content").asText("");
+                            if (!content.isEmpty()) {
+                                if (sink != null) {
+                                    try {
+                                        // 前端按 kind=status 聚合到"实时思考"区
+                                        sink.emit(nodeId, "status", content, null);
+                                    } catch (Exception ex) {
+                                        log.warn("[Hermes] sink 推送 thinking_status 失败: {}", ex.getMessage());
+                                    }
+                                }
+                            }
+                        }
+                        // v1.1:工具细粒度进度(event_type + name + preview/args)
+                        case "tool_progress" -> {
+                            String tpEventType = data.path("event_type").asText("");
+                            String toolName = data.path("name").asText(null);
+                            String preview = data.has("preview")
+                                    ? objectMapper.writeValueAsString(data.get("preview")) : null;
+                            String detailJson = (preview != null) ? preview
+                                    : (data.has("args") ? objectMapper.writeValueAsString(data.get("args")) : "{}");
+                            logs.add(ExecutionLog.toolProgress(tpEventType, detailJson));
+                            if (sink != null) {
+                                try {
+                                    sink.emit(nodeId, "tool_progress",
+                                            "{\"event_type\":\"" + tpEventType + "\",\"name\":\"" + toolName
+                                                    + "\",\"preview\":" + (preview != null ? preview : "null") + "}",
+                                            toolName);
+                                } catch (Exception ex) {
+                                    log.warn("[Hermes] sink 推送 tool_progress 失败: {}", ex.getMessage());
+                                }
+                            }
+                        }
+                        // v1.1:todo 工具完整输出(前端解析后渲染 TODO List)
+                        // Python 端 hermes_bridge.py 已用 json.dumps 把 result 序列化为字符串,
+                        // 这里用 asText 取字符串原值,避免 writeAsString 再次转义导致前端二次 parse 失败
+                        case "todo_update" -> {
+                            String fullJson = data.path("result").asText("{}");
+                            logs.add(ExecutionLog.todoUpdate(fullJson));
+                            if (sink != null) {
+                                try {
+                                    sink.emit(nodeId, "todo_update", fullJson, "todo");
+                                } catch (Exception ex) {
+                                    log.warn("[Hermes] sink 推送 todo_update 失败: {}", ex.getMessage());
+                                }
+                            }
+                        }
+                        // v1.1:模型请求用户确认(Askfor 区域)
+                        // 注意:clarify_request 不结束 SSE 读取循环,Bridge 在 Python 侧同步等待用户回答后
+                        // 才会继续推送后续事件。这里只负责把请求转发给前端,不中断 readLine。
+                        case "clarify_request" -> {
+                            String clarifyId = data.path("clarify_id").asText("");
+                            String question = data.path("question").asText("");
+                            String choicesJson = data.has("choices")
+                                    ? objectMapper.writeValueAsString(data.get("choices")) : "[]";
+                            logs.add(ExecutionLog.clarifyRequest(clarifyId, question, choicesJson));
+                            if (sink != null) {
+                                try {
+                                    sink.emit(nodeId, "clarify_request",
+                                            "{\"clarify_id\":\"" + clarifyId + "\",\"question\":\""
+                                                    + question.replace("\"", "\\\"") + "\",\"choices\":" + choicesJson + "}",
+                                            null);
+                                } catch (Exception ex) {
+                                    log.warn("[Hermes] sink 推送 clarify_request 失败: {}", ex.getMessage());
+                                }
+                            }
+                            log.info("[Hermes] 请求用户确认 clarify_id={}, question={}", clarifyId, question);
+                        }
                         case "done" -> finalResponse.append(data.path("content").asText(""));
                         case "error" -> {
                             String msg = data.path("message").asText("未知错误");
@@ -427,6 +528,62 @@ public class HermesBridgeClient {
         return String.join(", ", names);
     }
 
+    /**
+     * v1.1:向 Bridge 提交用户对 clarify_request 的回答。
+     *
+     * <p>由 WorkflowController 在收到前端 POST /runs/{runId}/resume 时调用。
+     * Bridge 收到后会调用 clarify_gateway.resolve_gateway_clarify,
+     * 唤醒在 on_clarify 回调中阻塞等待的 Hermes Agent 线程。</p>
+     *
+     * @param clarifyId clarify_request 事件携带的 clarify_id
+     * @param answer    用户回答(选项值或自定义文本)
+     * @return true 表示 Bridge 报告已成功唤醒等待方;false 表示未找到对应 clarify 或已过期
+     */
+    public boolean submitClarifyAnswer(String clarifyId, String answer) {
+        String url = "http://127.0.0.1:" + properties.getBridge().getPort()
+                + "/clarify/" + clarifyId + "/answer";
+        try {
+            Map<String, Object> body = new ConcurrentHashMap<>();
+            body.put("answer", answer == null ? "" : answer);
+            byte[] bodyBytes = objectMapper.writeValueAsBytes(body);
+
+            HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
+            conn.setRequestMethod("POST");
+            conn.setDoOutput(true);
+            conn.setRequestProperty("Content-Type", "application/json");
+            String authToken = properties.getBridge().getAuthToken();
+            if (authToken != null && !authToken.isBlank()) {
+                conn.setRequestProperty("X-Bridge-Token", authToken);
+            }
+            conn.setConnectTimeout(3000);
+            conn.setReadTimeout(5000);
+            conn.getOutputStream().write(bodyBytes);
+            conn.getOutputStream().flush();
+
+            int code = conn.getResponseCode();
+            if (code != 200) {
+                log.warn("[HermesBridgeClient] /clarify/{}/answer 返回非 200: {}", clarifyId, code);
+                conn.disconnect();
+                return false;
+            }
+            // 解析响应中的 resolved 字段
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
+                StringBuilder sb = new StringBuilder();
+                String line;
+                while ((line = reader.readLine()) != null) sb.append(line);
+                JsonNode resp = objectMapper.readTree(sb.toString());
+                boolean resolved = resp.path("resolved").asBoolean(false);
+                log.info("[HermesBridgeClient] clarify 回答已提交 clarify_id={}, resolved={}", clarifyId, resolved);
+                conn.disconnect();
+                return resolved;
+            }
+        } catch (Exception e) {
+            log.warn("[HermesBridgeClient] 提交 clarify 回答失败 clarify_id={}: {}", clarifyId, e.getMessage());
+            return false;
+        }
+    }
+
     /**
      * 通知 Bridge 清空技能索引缓存与 Agent 池,使新同步的技能立即生效。
      * 由 HermesSkillSyncService 在技能写时同步后调用;失败仅记录日志,不影响主流程。

+ 15 - 1
backend/src/main/java/com/agent/management/external/ExternalApiController.java

@@ -233,7 +233,7 @@ public class ExternalApiController {
             eventBus.register(runId);
             eventBus.publish(runId,
                     record.getStatus().equals("FAILED") ? "workflow_error" : "workflow_complete",
-                    WorkflowRunEvent.workflowComplete(runId, parseOutputs(record)));
+                    WorkflowRunEvent.workflowComplete(runId, parseOutputs(record), parseWorkingDirFiles(record)));
         }
 
         SseEmitter emitter = eventBus.subscribe(runId, lastEventId);
@@ -510,6 +510,20 @@ public class ExternalApiController {
         }
     }
 
+    /**
+     * 把 WorkflowRun.workingDirFiles JSON 字符串解析为 List<String>,
+     * 供断线重连场景重建 workflow_complete 事件使用。
+     */
+    @SuppressWarnings("unchecked")
+    private List<String> parseWorkingDirFiles(WorkflowRun record) {
+        if (record.getWorkingDirFiles() == null || record.getWorkingDirFiles().isBlank()) return null;
+        try {
+            return MAPPER.readValue(record.getWorkingDirFiles(), List.class);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     @SuppressWarnings("unchecked")
     private Map<String, Object> parseJsonToMap(String json) {
         if (json == null || json.isBlank()) return null;

+ 20 - 7
backend/src/main/java/com/agent/management/external/SseEventBus.java

@@ -31,14 +31,25 @@ import java.util.concurrent.atomic.AtomicLong;
 @Component
 public class SseEventBus {
 
-    /** 单个 runId 缓存的最大事件数(超出后丢弃最旧事件,断线重连可能拿不到完整历史) */
-    private static final int MAX_EVENTS_PER_RUN = 1000;
+    /**
+     * 单个 runId 缓存的最大事件数(超出后丢弃最旧事件,断线重连可能拿不到完整历史)。
+     * <p>v1.1:从 1000 调到 5000。clarify 长等待期间可能持续累积 thinking / tool_progress 事件,
+     * 用户几小时后回来重连需看到完整思考过程。clarify_request 永不丢弃(在 publish 中特殊处理)。</p>
+     */
+    private static final int MAX_EVENTS_PER_RUN = 5000;
 
-    /** 运行结束后缓存保留时长 */
-    private static final long TTL_AFTER_COMPLETION_MS = 3600_000L;
+    /**
+     * 运行结束后缓存保留时长。
+     * <p>v1.1:从 1 小时调到 25 小时。配合 24h 兜底超时,确保用户离开一天回来后还能看到完整重放。</p>
+     */
+    private static final long TTL_AFTER_COMPLETION_MS = 90_000_000L; // 25h
 
-    /** SSE 连接超时:30 分钟(与 WorkflowController.run 一致) */
-    private static final long SSE_TIMEOUT_MS = 1_800_000L;
+    /**
+     * SSE 连接超时。
+     * <p>v1.1:从 30 分钟调到 24 小时(兜底)。Askfor 场景用户可能几小时后才回来,
+     * 真正的"无超时"由前端断线重连 + Last-Event-ID 续传机制保证。</p>
+     */
+    private static final long SSE_TIMEOUT_MS = 86_400_000L; // 24h
 
     private final Map<String, RunStream> streams = new ConcurrentHashMap<>();
     private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
@@ -70,7 +81,9 @@ public class SseEventBus {
         RunStream stream = streams.computeIfAbsent(runId, k -> new RunStream());
         RecordedEvent rec = new RecordedEvent(stream.idGen.incrementAndGet(), eventName, payload, Instant.now());
 
-        // 写入缓存(超出窗口则截断头部)
+        // 写入缓存(超出窗口则丢弃最旧事件)
+        // v1.1:MAX_EVENTS_PER_RUN 调到 5000,长等待期间一般不会超限;若超限丢失最旧的 thinking 片段可接受,
+        // clarify_request 持续保存在 WorkflowPause 表中,前端断线重连时优先通过 GET /state 恢复 Askfor UI。
         stream.events.add(rec);
         while (stream.events.size() > MAX_EVENTS_PER_RUN) {
             stream.events.remove(0);

+ 75 - 0
backend/src/main/java/com/agent/management/model/entity/WorkflowPause.java

@@ -0,0 +1,75 @@
+package com.agent.management.model.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * v1.1:工作流暂停点(clarify_request 触发时持久化)。
+ *
+ * <p>用于:</p>
+ * <ul>
+ *   <li>前端断线重连后通过 runId 查询当前是否有未答 clarify,恢复 Askfor UI</li>
+ *   <li>审计:记录每次 clarify 的问题、选项、用户回答</li>
+ *   <li>兜底清理:长期未答(&gt;30 天)的记录由定时任务清理</li>
+ * </ul>
+ *
+ * <p>状态流转:{@code UNANSWERED} → {@code ANSWERED}(用户提交答案)/
+ * {@code UNANSWERED} → {@code TIMEOUT}(工作流超时 abort 后由引擎标记)</p>
+ */
+@Data
+@Entity
+@Table(name = "workflow_pause",
+        indexes = {
+                @Index(name = "idx_workflow_pause_run_id", columnList = "run_id"),
+                @Index(name = "idx_workflow_pause_clarify_id", columnList = "clarify_id", unique = true)
+        })
+public class WorkflowPause {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    /** 关联工作流运行 ID(与 workflow_run.run_id 对应) */
+    @Column(name = "run_id", nullable = false)
+    private String runId;
+
+    /** 触发 clarify 的节点 ID */
+    @Column(name = "node_id", nullable = false)
+    private String nodeId;
+
+    /** clarify_gateway 生成的唯一 ID,Bridge 侧用于匹配 answer */
+    @Column(name = "clarify_id", nullable = false)
+    private String clarifyId;
+
+    /** 模型抛出的问题文本 */
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String question;
+
+    /** 选项 JSON 数组(如 ["是","否","自定义"]) */
+    @Column(columnDefinition = "TEXT")
+    private String choices;
+
+    /** 暂停触发时间 */
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+
+    /** 用户回答时间;UNANSWERED 时为 null */
+    @Column(name = "answered_at")
+    private LocalDateTime answeredAt;
+
+    /** 用户的回答文本(选项值或自定义输入) */
+    @Column(columnDefinition = "TEXT")
+    private String answer;
+
+    /** UNANSWERED / ANSWERED / TIMEOUT */
+    @Column(nullable = false)
+    private String status;
+
+    @PrePersist
+    protected void onCreate() {
+        if (createdAt == null) createdAt = LocalDateTime.now();
+        if (status == null) status = "UNANSWERED";
+    }
+}

+ 9 - 1
backend/src/main/java/com/agent/management/model/entity/WorkflowRun.java

@@ -37,10 +37,18 @@ public class WorkflowRun {
     @Column(columnDefinition = "TEXT")
     private String inputs;
 
-    /** 输出结果 JSON */
+    /** 输出结果 JSON(仅业务字段,不再包含元数据) */
     @Column(columnDefinition = "TEXT")
     private String outputs;
 
+    /**
+     * 工作空间文件列表(JSON 数组字符串,应用 .agentignore 过滤后的相对路径)。
+     * 由 WorkflowEngine 在运行完成时扫描 workingDir 后写入,与 outputs 同级,
+     * 供前端独立展示(不再注入到 output 节点的 envelope.data)。
+     */
+    @Column(name = "working_dir_files", columnDefinition = "TEXT")
+    private String workingDirFiles;
+
     private String error;
 
     @Column(name = "started_at")

+ 24 - 0
backend/src/main/java/com/agent/management/repository/WorkflowPauseRepository.java

@@ -0,0 +1,24 @@
+package com.agent.management.repository;
+
+import com.agent.management.model.entity.WorkflowPause;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * v1.1:工作流暂停点(clarify)持久化。
+ */
+@Repository
+public interface WorkflowPauseRepository extends JpaRepository<WorkflowPause, Long> {
+
+    /** 按 runId 查最近的未答暂停点(一个 run 同时一般只有一个 pending clarify) */
+    Optional<WorkflowPause> findFirstByRunIdAndStatusOrderByCreatedAtDesc(String runId, String status);
+
+    /** 按 clarifyId 精确查找(用户答题时使用) */
+    Optional<WorkflowPause> findByClarifyId(String clarifyId);
+
+    /** 列出某 run 的所有暂停点(含已答,用于审计回放) */
+    List<WorkflowPause> findByRunIdOrderByCreatedAtAsc(String runId);
+}

+ 8 - 0
backend/src/main/java/com/agent/management/repository/WorkflowRunRepository.java

@@ -5,9 +5,17 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
 import java.util.List;
+import java.util.Optional;
 
 @Repository
 public interface WorkflowRunRepository extends JpaRepository<WorkflowRun, Long> {
     List<WorkflowRun> findByWorkflowIdOrderByStartedAtDesc(Long workflowId);
     List<WorkflowRun> findAllByOrderByStartedAtDesc();
+
+    /**
+     * v1.1:通过 runId 精确查询(runId 上有 unique 索引,避免 findAll + filter 全表扫描)。
+     * 用于 GET /runs/{runId}/state、POST /runs/{runId}/resume 等断线重连场景。
+     */
+    Optional<WorkflowRun> findByRunId(String runId);
 }
+

+ 675 - 0
docs/design/agent-chat-page-design.md

@@ -0,0 +1,675 @@
+# 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`) |
+| 是否持久化历史 | **需要** | 关闭页面后可在侧栏切换历史会话回看,类似现有"运行历史" |
+
+---
+
+## 2. 用户故事
+
+### 2.1 典型流程
+
+1. 用户从侧栏进入"Agent 对话"页面。
+2. 左侧"会话列表"展示历史会话(按时间倒序),点击新建会话按钮开始新对话。
+3. 右侧"对话区"是消息流(用户消息 / 助手回复交替)+ 底部输入区。
+4. 输入区上方有"模型选择器"和"技能选择器"两个下拉(均可留空):
+   - 不选模型 → 用系统默认模型
+   - 不选技能 → Hermes 凭自身系统提示与 `skill_view` 工具自主决策
+   - 选了技能 → 把该技能的 `SKILL.md` 作为系统提示词注入
+5. 用户输入问题、点击发送:
+   - 用户消息立即出现在对话区
+   - 助手回复区出现一个"思考中"占位卡片,卡片内实时展示思考过程(5 区域:思考流、实时状态、当前工具、工具历史、待办清单)
+   - 若 Agent 中途发起 clarify(请求用户确认),卡片内出现 AskforPanel,用户点选项或自定义输入后继续
+   - 完成后,思考过程折叠为一行摘要,最终答案展开显示
+6. 用户可基于当前回复继续提问,会话上下文累积。
+7. 关闭页面后再次进入,会话列表仍可见,点击任一会话可回看完整对话。
+
+### 2.2 边界场景
+
+- **会话切换**:用户从会话 A 切到会话 B,A 的 SSE 流应被正确清理(不留监听器、不混状态)。
+- **会话进行中刷新页面**:通过 SSE 断线重连机制(`GET /api/chat/runs/{runId}/stream?lastEventId=N`)恢复。
+- **会话进行中关闭浏览器再打开**:通过持久化的 `WorkflowPause` + `GET /api/chat/runs/{runId}/state` 轮询恢复 clarify 等待态。
+- **跨标签页**:同一会话不允许在多个标签页同时发新消息(前端用 `localStorage` 简单锁,避免 session_id 冲突)。
+
+---
+
+## 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 数据模型(新增两张表)
+
+#### 4.1.1 `chat_session`(会话主表)
+
+```sql
+CREATE TABLE chat_session (
+    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
+    session_uuid    VARCHAR(36) NOT NULL UNIQUE,        -- 对外暴露的 UUID(即 Hermes session_id)
+    title           VARCHAR(255),                       -- 会话标题(默认取首条用户消息前 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
+    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
+    run_id          VARCHAR(64),                        -- 关联本轮 Bridge run(assistant 消息才有,user 消息为 NULL)
+    role            VARCHAR(16) NOT NULL,               -- USER / ASSISTANT / SYSTEM(预留)
+    content         TEXT NOT NULL,                      -- 消息文本(user 输入原文 / assistant 最终回复)
+    status          VARCHAR(16) NOT NULL,               -- USER(Always 'SENT') / ASSISTANT(RUNNING/SUCCESS/FAILED)
+    model_id        BIGINT,                             -- 实际使用的模型(冗余存储,便于回看当时配置)
+    skill_folder    VARCHAR(255),                       -- 实际使用的技能
+    logs_json       TEXT,                               -- ASSISTANT 思考过程日志(同 WorkflowRunNode.logs 结构)
+    error           TEXT,                               -- FAILED 时的错误信息
+    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    completed_at    TIMESTAMP,
+    FOREIGN KEY (session_id) REFERENCES chat_session(id) ON DELETE CASCADE
+);
+CREATE INDEX idx_chat_message_session ON chat_message(session_id, created_at);
+```
+
+> `logs_json` 字段存储与 `WorkflowRunNode.logs` 完全相同的 ExecutionLog 数组(JSON),这样前端可**直接复用** `runResultReplay.js` 的 `buildReplayNode` 函数还原思考过程。
+
+#### 4.1.3 持久化策略
+
+| 事件 | 操作 |
+|---|---|
+| 用户发起新会话 | INSERT chat_session(hermes_session 用 `UUID` 或自增 id 转字符串) |
+| 用户发送消息 | INSERT chat_message(role=USER, status=SENT) → 立即返回;异步触发 Bridge run |
+| 助手 run 启动 | INSERT chat_message(role=ASSISTANT, status=RUNNING, run_id=xxx) |
+| 助手 run 完成(success) | UPDATE chat_message SET status=SUCCESS, content=finalText, logs_json=..., 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 |
+
+> **复用 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?}]` |
+| `GET` | `/api/chat/sessions/{id}` | 获取会话详情 + 所有消息。返回 `{session, messages: [{role, content, status, logs, ...}]}` |
+| `PATCH` | `/api/chat/sessions/{id}` | 更新会话(改标题、切换 modelId/skillFolder、归档) |
+| `DELETE` | `/api/chat/sessions/{id}` | 删除会话(级联删除消息) |
+
+#### 4.2.2 单轮问答(核心)
+
+| Method | Path | 用途 |
+|---|---|---|
+| `POST` | `/api/chat/sessions/{sessionId}/runs` | 发起一轮问答。Body: `{message, modelId?, skillFolder?, maxIterations?}`。返回 `{runId}`,**SSE 流通过单独端点订阅** |
+| `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** |
+
+> **路径前缀决策**:新端点统一用 `/api/chat/...`,与现有 `/api/workflows/...` 平级。虽然 `GET stream` / `GET state` / `POST resume` 在逻辑上与 WorkflowController 同名端点等价(都基于 SseEventBus + WorkflowPause),但**不共享路径**——独立 Controller 更清晰,未来如果 chat 与工作流的 clarify 处理逻辑出现分歧也好扩展。
+
+#### 4.2.3 资源选择器辅助
+
+| Method | Path | 用途 |
+|---|---|---|
+| `GET` | `/api/models` | 已有,列出模型 |
+| `GET` | `/api/skills` | 已有,列出技能(folderName + 名称 + 描述) |
+
+### 4.3 Service 层
+
+#### 4.3.1 `ChatSessionService` / `ChatSessionRepository`
+
+标准 CRUD。增加 `touch(sessionId)` 方法更新 `last_message_at`。
+
+#### 4.3.2 `ChatMessageService` / `ChatMessageRepository`
+
+标准 CRUD。增加:
+- `appendLog(runId, log)`:assistant run 过程中流式累积 logs(可选;也可仅在完成时一次性写入)
+- `markSuccess(runId, finalText, logsJson)`
+- `markFailed(runId, error)`
+
+#### 4.3.3 `ChatRunService`(核心编排)
+
+```java
+public String startRun(Long sessionId, String userMessage, Long modelId, String skillFolder, Integer maxIterations) {
+    ChatSession session = sessionRepo.findById(sessionId).orElseThrow(...);
+
+    // 1. 写用户消息
+    messageRepo.save(new ChatMessage(sessionId, Role.USER, userMessage, Status.SENT));
+
+    // 2. 解析模型配置
+    AiModelConfig modelConfig = (modelId != null)
+        ? aiModelService.getModelConfig(modelId)
+        : null;  // null → Bridge 走环境变量默认
+
+    // 3. 解析技能系统提示
+    String systemPrompt = (skillFolder != null)
+        ? skillService.getSkillFullContent(skillFolder)
+        : null;
+
+    // 4. 生成 runId,注册 SSE 总线
+    String runId = UUID.randomUUID().toString().substring(0, 8);
+    eventBus.register(runId);
+
+    // 5. 写助手占位消息
+    ChatMessage assistantMsg = messageRepo.save(new ChatMessage(
+        sessionId, runId, Role.ASSISTANT, "", Status.RUNNING, modelId, skillFolder));
+
+    // 6. 异步调 Bridge
+    executor.submit(() -> {
+        try {
+            List<ExecutionLog> logs = new ArrayList<>();
+            String finalText = hermesBridgeClient.run(
+                systemPrompt, userMessage,
+                maxIterations != null ? maxIterations : 100,
+                null, null, session.getHermesSession(),  // session_id 复用 Agent 实例
+                null, runId, modelConfig,
+                (kind, content, toolName) -> {           // sink 回调
+                    WorkflowRunEvent evt = WorkflowRunEvent.nodeStream(runId, "agent", kind, content, toolName);
+                    eventBus.publish(runId, evt.getType(), evt);
+                    logs.add(ExecutionLog.fromStream(kind, content, toolName));  // 累积持久化
+                });
+            messageRepo.markSuccess(runId, finalText, toJson(logs));
+            sessionRepo.touch(sessionId);
+        } catch (Exception e) {
+            messageRepo.markFailed(runId, e.getMessage());
+            eventBus.publish(runId, "workflow_error", WorkflowRunEvent.workflowError(runId, e.getMessage()));
+        }
+    });
+
+    return runId;
+}
+```
+
+> **关键复用点**:
+> - `HermesBridgeClient.run(...)` 的 sink 回调签名与 `NodeStreamSink.emit(nodeId, kind, content, toolName)` 一致,直接传 lambda 即可。
+> - `ExecutionLog` 类型枚举与工厂方法直接复用(`ExecutionLog.thinking(...)` / `toolCall(...)` 等)。
+> - `WorkflowRunEvent.nodeStream` / `workflowError` 静态工厂直接复用。
+
+### 4.4 HermesBridgeClient 改造评估
+
+**结论:无需改造**。
+
+现有 `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)`)供两条链路共用。
+
+---
+
+## 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` | 左侧会话列表(新建/切换/删除/重命名) |
+| `frontend/src/views/chat/ChatMessageList.vue` | 中间消息流(user/assistant 交替) |
+| `frontend/src/views/chat/ChatMessageItem.vue` | 单条消息渲染:user 简单气泡,assistant 复用 AgentThinkingPanel |
+| `frontend/src/views/chat/ChatComposer.vue` | 底部输入区:模型选择器 + 技能选择器 + 文本框 + 发送按钮 |
+| `frontend/src/composables/useChat.js` | 核心 composable(详见 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_json` 还原思考过程 |
+| `extractStreamingThinking` | 同文件 :27 | 单独提取思考流(用于历史消息展开) |
+| `extractToolHistory` | 同文件 :48 | 单独提取工具调用历史 |
+| `extractLatestTodos` | 同文件 :96 | 单独提取最新 todos |
+| `stripHermesThinkingDecoration` | `frontend/src/utils/hermesThinking.js:69` | 剥离颜文字装饰 |
+
+### 5.4 `useChat.js` composable 设计
+
+参考现有 `useWorkflowRunner.js`,但去除 VueFlow 画布相关逻辑:
+
+```javascript
+// frontend/src/composables/useChat.js
+export function useChat() {
+  // 当前会话状态
+  const sessions = ref([])                    // 会话列表
+  const currentSession = ref(null)            // 当前会话对象
+  const messages = ref([])                    // 当前会话的消息列表
+
+  // 运行时态(仅最新一条 assistant 消息)
+  const currentMessage = ref(null)            // 形如 { runId, streamingThinking, currentStatus, currentTool, toolHistory, todos }
+  const pendingAsk = ref(null)                // { nodeId, clarifyId, question, choices }
+  const reconnecting = ref(false)
+  const isRunning = ref(false)
+
+  // 输入区状态
+  const inputText = ref('')
+  const selectedModelId = ref(null)
+  const selectedSkillFolder = ref(null)
+
+  // SSE 流处理(核心:从 useWorkflowRunner 抽出,去掉 nodeResults 数组逻辑)
+  async function startRun() {
+    const userText = inputText.value.trim()
+    if (!userText || !currentSession.value || isRunning.value) return
+
+    // 1. 立即把用户消息加到 messages
+    messages.value.push({ role: 'USER', content: userText, createdAt: Date.now() })
+    inputText.value = ''
+
+    // 2. 调 POST /api/chat/sessions/{id}/runs
+    const { runId } = await startChatRun(currentSession.value.id, {
+      message: userText,
+      modelId: selectedModelId.value,
+      skillFolder: selectedSkillFolder.value,
+    })
+
+    // 3. 创建助手占位消息
+    currentMessage.value = {
+      runId,
+      streamingThinking: '',
+      currentStatus: '',
+      currentTool: null,
+      toolHistory: [],
+      todos: [],
+    }
+    messages.value.push({ role: 'ASSISTANT', runId, status: 'RUNNING', runtime: currentMessage.value })
+    isRunning.value = true
+
+    // 4. 订阅 SSE 流
+    await consumeSSEStream(runId)
+  }
+
+  // SSE 事件分发(从 useWorkflowRunner.handleSSEEvent 简化)
+  function handleSSEEvent(event) {
+    if (event.type === 'node_stream' && event.kind === 'thinking') {
+      currentMessage.value.streamingThinking += event.content
+    } else if (event.type === 'node_stream' && event.kind === 'status') {
+      currentMessage.value.currentStatus = stripHermesThinkingDecoration(event.content)
+    } else if (event.type === 'node_stream' && event.kind === 'tool_call') {
+      currentMessage.value.currentTool = { name: event.toolName, argsSummary: event.content, result: null }
+      currentMessage.value.toolHistory.push({ ...currentMessage.value.currentTool })
+    } else if (event.type === 'node_stream' && event.kind === 'tool_result') {
+      if (currentMessage.value.currentTool) currentMessage.value.currentTool.result = event.content
+    } else if (event.type === 'node_stream' && event.kind === 'todo_update') {
+      currentMessage.value.todos = JSON.parse(event.content)
+    } else if (event.type === 'node_stream' && event.kind === 'clarify_request') {
+      pendingAsk.value = {
+        nodeId: event.nodeId || 'agent',
+        clarifyId: event.clarifyId,
+        question: event.question,
+        choices: event.choices || [],
+      }
+    } else if (event.type === 'workflow_complete') {
+      // 把 finalText 写入 messages 中对应 runId 的助手消息
+      const msg = messages.value.find(m => m.runId === event.runId)
+      if (msg) {
+        msg.content = event.output?.text || event.finalText || ''
+        msg.status = 'SUCCESS'
+      }
+      isRunning.value = false
+      currentMessage.value = null
+    } else if (event.type === 'workflow_error') {
+      const msg = messages.value.find(m => m.runId === event.runId)
+      if (msg) { msg.status = 'FAILED'; msg.error = event.message }
+      isRunning.value = false
+      currentMessage.value = null
+    }
+  }
+
+  // 复用 useWorkflowRunner 的 consumeSSEStream / tryReconnect / pollRunState 三层降级
+  // 仅把 GET /api/workflows/runs/{runId}/stream 改为 GET /api/chat/runs/{runId}/stream
+
+  // 历史会话加载
+  async function loadSession(sessionId) {
+    const { session, messages: history } = await getSessionDetail(sessionId)
+    currentSession.value = session
+    selectedModelId.value = session.modelId
+    selectedSkillFolder.value = session.skillFolder
+    // 把持久化的 logs_json 通过 buildReplayNode 还原为运行时态
+    messages.value = history.map(m => {
+      if (m.role === 'ASSISTANT' && m.logsJson) {
+        return { ...m, runtime: buildReplayNode({ logs: JSON.parse(m.logsJson) }) }
+      }
+      return m
+    })
+  }
+
+  // 切换会话时清理 SSE 监听
+  function switchSession(sessionId) {
+    if (currentMessage.value) {
+      // 主动断开当前 SSE 连接
+      abortSSE()
+    }
+    currentMessage.value = null
+    pendingAsk.value = null
+    isRunning.value = false
+    loadSession(sessionId)
+  }
+
+  return {
+    sessions, currentSession, messages, currentMessage, pendingAsk, reconnecting, isRunning,
+    inputText, selectedModelId, selectedSkillFolder,
+    loadSessions: ..., startRun: startRun, submitClarify: ..., switchSession,
+  }
+}
+```
+
+### 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. 后端返回 `messages` 数组,每条 assistant 消息含 `logsJson` 字段。
+3. 前端用 `buildReplayNode({ logs: JSON.parse(logsJson) })` 把日志还原为运行时态对象。
+4. `<ChatMessageItem :message="m">` 渲染:
+   - user 消息:简单气泡显示 `m.content`
+   - assistant 消息:默认折叠态,显示首行 + "展开思考过程" 按钮
+   - 展开后:完整复用 `<AgentThinkingPanel :node="m.runtime" />`
+
+---
+
+## 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 并发与生命周期
+
+- **单会话单流**:同一 chat_session 不允许并发 run(前端 `isRunning` 锁 + 后端校验)。
+- **会话切换**:前端切换会话时主动 `abortSSE()`,避免遗留监听器。
+- **页面卸载**:`onBeforeUnmount` 中 abort SSE 连接,避免内存泄漏。
+- **跨标签页**:用 `localStorage.setItem('chat-active-run-' + sessionId, runId)` 简单锁,新标签页发现已有 run 在跑则提示"已在其他标签页运行"。
+
+### 6.5 性能
+
+- `logs_json` 字段可能较大(思考流数千字 + 工具调用结果)。`getSessionDetail` 返回时考虑分页(如仅返回最近 50 条消息,更老的通过 `GET /api/chat/sessions/{id}/messages?before=msgId` 懒加载)。
+- `chat_session` 表加索引 `(status, last_message_at DESC)` 加速左侧列表查询。
+- `chat_message` 表加索引 `(session_id, created_at)` 加速消息流查询。
+
+---
+
+## 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:后端骨架(约 1-2 天)
+1. 新建 `ChatSession` / `ChatMessage` 实体 + Repository + 基础 CRUD Service
+2. 新建 `ChatController`:会话 CRUD 端点
+3. 抽取 `ClarifyHooks.wrap(sink, runId, pauseRepo)` 工具方法(从 `WorkflowLevelExecutor.wrapSinkWithClarifyHook` 复制)
+4. 实现 `ChatRunService.startRun(...)`:调 `HermesBridgeClient.run` + `SseEventBus.publish`
+5. 添加 `GET /api/chat/runs/{runId}/stream` / `state` / `POST /resume` 端点
+6. 单元/集成测试:构造一次问答,验证 SSE 流式输出 + 持久化
+
+**验证**:用 curl/Postman 触发 `POST /api/chat/sessions/{id}/runs`,订阅 SSE 流,确认事件正常推送。
+
+### 阶段 2:前端骨架(约 1-2 天)
+1. 抽取 `ModelSelect.vue` / `SkillSelect.vue` 组件
+2. 实现 `useChat.js` composable(从 `useWorkflowRunner.js` 抽取核心逻辑)
+3. 实现 `api/chat.js` 封装
+4. 实现 `ChatPage.vue` + 子组件(SessionList / MessageList / MessageItem / Composer)
+5. 路由 + 侧栏菜单注册
+6. **不替换** WorkflowEditor.vue 的内联 `<n-select>`(YAGNI,后续单独重构)
+
+**验证**:完整跑通"新建会话 → 发消息 → 看到思考过程 → 收到回复 → 切换会话 → 切回看到历史"。
+
+### 阶段 3:clarify + 断线重连(约 1 天)
+1. 验证 clarify_request → AskforPanel → 提交答案 → 继续执行 全链路
+2. 验证刷新页面后通过 `lastEventId` 重连续传
+3. 验证关闭浏览器后通过 `GET /state` 轮询恢复 clarify 等待态
+
+### 阶段 4:打磨(按需)
+1. 会话标题自动从首条消息截取
+2. 会话搜索、归档
+3. 助手回复复制/重新生成
+4. Token 用量统计(如果 Hermes Bridge 能返回)
+5. 多标签页锁提示
+
+---
+
+## 9. 风险与开放问题
+
+### 9.1 已识别风险
+
+| 风险 | 缓解措施 |
+|---|---|
+| Hermes Bridge 长时间运行(数小时 clarify 等待)下 session_id 复用可能因 Python 进程重启失效 | Bridge 进程重启后 session_id 失效,下次对话会自动创建新 Agent(Bridge 端的 `_agents` 字典 LRU 清理);前端检测到 "session not found" 错误时,自动新建会话 |
+| `logs_json` 字段过大影响查询性能 | 单条 message 仅存当前轮日志;会话级聚合通过 message 分页查询;超过 1MB 时考虑压缩存储 |
+| Agent 调用工具时访问文件系统可能产生工作目录污染 | Agent 问答场景下传 `workingDir=null`,禁用文件工具;如未来需要,传临时目录 + 复用 `sandbox_patch.py` |
+| 同一用户在多个标签页打开同一会话 | localStorage 简单锁;后期可引入 WebSocket 双向通知 |
+| 多轮对话累积上下文导致 Token 超限 | 复用 Hermes Agent 自身的 `compress_context` 能力(上游已实现),不额外处理 |
+
+### 9.2 开放问题(待用户/评审确认)
+
+1. **是否需要"重新生成回复"功能**(类似 ChatGPT 的 regenerate 按钮)?
+2. **是否需要"消息编辑"功能**(用户修改某条历史消息后重新发送)?
+3. **会话标题**:自动从首条用户消息截取,还是要求用户手动填?
+4. **是否支持文件上传**(用户上传文件作为提问上下文)?
+5. **是否需要导出对话**(Markdown / JSON)?
+
+> 这些都是"好用"层面的功能,不影响基础架构。建议第一版仅实现核心流程(新建会话 → 发消息 → 看思考 → 收回复 → 多轮 → 历史回看),上述增强功能按需迭代。
+
+---
+
+## 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) —— 历史回放工具
+
+### 相关文档
+- [`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))

+ 538 - 0
docs/plans/hermes-thinking-display-overhaul-plan.md

@@ -0,0 +1,538 @@
+# 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 |

+ 647 - 0
docs/reference/dify-workflow-nodes-reference.md

@@ -0,0 +1,647 @@
+# Dify 工作流节点系统参考
+
+> 本文件基于 `D:/projects/agent-management/dify` 1.16.0 前端代码(`web/app/components/workflow/`)梳理,聚焦"工作流编排器中的节点类型"与"每个节点详情面板要填的表单项",作为本仓库后续实现节点 schema / 节点详情面板 / 工作流引擎时的字段对照。
+>
+> 已有 `dify-reference.md` 偏项目整体架构(应用形态、引擎、Plugin Daemon、Agent Runtime 等),本文档聚焦"节点 → 字段"这一层,两份文档互为补充。
+
+---
+
+## 0. 关键源码定位
+
+| 内容 | 文件相对路径 |
+| --- | --- |
+| 节点类型枚举 `BlockEnum`(共 31 个) | `web/app/components/workflow/types.ts:27-60` |
+| 节点分类枚举 `BlockClassification` | `web/app/components/workflow/block-selector/types.ts:40-48` |
+| 节点选择面板分组(BLOCKS 数组) | `web/app/components/workflow/block-selector/constants.tsx` |
+| 节点元数据生成器 `genNodeMetaData` | `web/app/components/workflow/utils/gen-node-meta-data.ts` |
+| 通用节点类型 `CommonNodeType<T>` | `web/app/components/workflow/types.ts:77-119` |
+| 各节点固定输出结构常量 `*_OUTPUT_STRUCT` | `web/app/components/workflow/constants.ts` |
+| 中/英文节点标题与描述 i18n | `web/i18n/zh-Hans/workflow.json` 的 `blocks.*` 与 `tabs.*` |
+| 节点目录约定 | `web/app/components/workflow/nodes/<type>/{default.ts, types.ts, panel.tsx, node.tsx}` |
+| 通用错误处理类型 | `web/app/components/workflow/nodes/_base/components/error-handle/types.ts` |
+| 通用重试类型 | `web/app/components/workflow/nodes/_base/components/retry/types.ts` |
+| 字段组件库 | `web/app/components/workflow/nodes/_base/components/field.tsx` 等 |
+
+---
+
+## 1. 节点分类(5 类)
+
+来自 `BlockClassification`,中文标签来自 i18n `tabs.*`:
+
+| 分类 key | 中文 | 英文 |
+| --- | --- | --- |
+| `-`(默认) | 默认(最常用) | Most Common |
+| `question-understand` | 问题理解 | Question Understand |
+| `logic` | 逻辑 | Logic |
+| `transform` | 转换 | Transform |
+| `utilities` | 工具 | Utilities |
+
+---
+
+## 2. 节点完整清单(31 个,按分类组织)
+
+> 顺序按 `genNodeMetaData` 中传入的 `classification` 字段归位;`sort` 为同类内排序权重。
+
+### 2.1 默认 / 最常用(Default)
+
+| type | 中文名 | 英文名 | 用途 | sort | 源码路径 |
+| --- | --- | --- | --- | --- | --- |
+| `start` | 用户输入 | User Input | 工作流起始节点;定义工作流输入变量;自动暴露 `userinput.query` / `userinput.files` | 0.1 | `nodes/start/` |
+| `start-placeholder` | 工作流开始 | Workflow Start | 占位起始节点;提示用户选择具体起始类型 | 0.05 | `nodes/start-placeholder/` |
+| `llm` | LLM | LLM | 大模型调用节点 | 1 | `nodes/llm/` |
+| `knowledge-retrieval` | 知识检索 | Knowledge Retrieval | 从知识库检索文档片段 | 2 | `nodes/knowledge-retrieval/` |
+| `end` | 输出 | Output / End | 工作流输出节点;声明最终输出变量 | 2.1 | `nodes/end/` |
+| `answer` | 直接回复 | Answer / Direct Answer | Chat 模式下直接给出回复(`isRequired`) | 2.1 | `nodes/answer/` |
+| `agent` | Agent | Agent | 旧版 Agent 节点(基于策略插件) | 3 | `nodes/agent/` |
+| `agent-v2` | Agent | Agent | 新版 Agent 节点(绑定 roster/inline agent) | 3 | `nodes/agent-v2/` |
+| `tool` | 工具 | Tool | 调用任意工具/插件/MCP/工作流作为工具 | -1 | `nodes/tool/` |
+| `datasource` | 数据源 | Data Source | 数据源驱动 flow 的入口(`isStart`, `isRequired`) | -1 | `nodes/data-source/` |
+| `datasource-empty` | 空数据源 | Empty Data Source | 数据源 flow 的空占位节点 | -1 | `nodes/data-source-empty/` |
+| `knowledge-index` | 知识库 | Knowledge Base | 在数据源 flow 中索引知识块(`isRequired`、`isUndeletable`、`isSingleton`、`isTypeFixed`) | 3.1 | `nodes/knowledge-base/` |
+| `trigger-schedule` | 定时触发器 | Schedule Trigger | 基于 cron / 可视化时间触发 | 2 | `nodes/trigger-schedule/` |
+| `trigger-webhook` | Webhook 触发器 | Webhook Trigger | 通过 HTTP 回调触发 | 3 | `nodes/trigger-webhook/` |
+| `trigger-plugin` | 集成触发器 | Integration Trigger | 通过插件事件订阅触发 | 1 | `nodes/trigger-plugin/` |
+
+### 2.2 问题理解(Question Understand)
+
+| type | 中文名 | 用途 | sort | 源码路径 |
+| --- | --- | --- | --- | --- |
+| `question-classifier` | 问题分类器 | 用大模型把用户问题分类到不同分支 | 1 | `nodes/question-classifier/` |
+
+### 2.3 逻辑(Logic)
+
+| type | 中文名 | 用途 | sort | 源码路径 |
+| --- | --- | --- | --- | --- |
+| `if-else` | 条件分支 | IF/ELIF/ELSE 多分支 | 1 | `nodes/if-else/` |
+| `iteration` | 迭代 | 对数组迭代(含子工作流;`isTypeFixed`) | 2 | `nodes/iteration/` |
+| `iteration-start` | 迭代开始 | 迭代节点内部自动生成的起始节点 | -1 | `nodes/iteration-start/` |
+| `loop` | 循环 | 通用循环(条件终止;`isTypeFixed`) | 3 | `nodes/loop/` |
+| `loop-start` | 循环开始 | 循环节点内部自动生成的起始节点 | -1 | `nodes/loop-start/` |
+| `loop-end` | 退出循环 | 循环终止节点(`isSingleton`) | 2 | `nodes/loop-end/` |
+| `human-input` | 人工介入 | 暂停工作流,等待人工表单输入 | 1 | `nodes/human-input/` |
+
+### 2.4 转换(Transform)
+
+| type | 中文名 | 用途 | sort | 源码路径 |
+| --- | --- | --- | --- | --- |
+| `code` | 代码执行 | 执行 Python3 / JavaScript 代码 | 1 | `nodes/code/` |
+| `template-transform` | 模板转换 | 用 Jinja2 模板渲染字符串 | 2 | `nodes/template-transform/` |
+| `variable-aggregator` | 变量聚合器 | 把多路变量聚合为一个输出(目录 `variable-assigner/`) | 3 | `nodes/variable-assigner/` |
+| `document-extractor` | 文档提取器 | 从文件变量提取文本 | 4 | `nodes/document-extractor/` |
+| `assigner` | 变量赋值 | 给会话变量赋值(v2 写入模式) | 5 | `nodes/assigner/` |
+| `parameter-extractor` | 参数提取器 | 用大模型从文本提取结构化参数 | 6 | `nodes/parameter-extractor/` |
+
+### 2.5 工具(Utilities)
+
+| type | 中文名 | 用途 | sort | 源码路径 |
+| --- | --- | --- | --- | --- |
+| `http-request` | HTTP 请求 | 发起 HTTP/HTTPS 请求 | 1 | `nodes/http/` |
+| `list-operator` | 列表操作 | 列表过滤 / 抽取 / 排序 / 限制 | 2 | `nodes/list-operator/` |
+
+> **节点选择面板的实际展示**:`BLOCKS` 数组(`block-selector/constants.tsx`)只展示常用节点;`start-placeholder` / `iteration-start` / `loop-start` / `knowledge-index` / `datasource` / `datasource-empty` / `trigger-plugin` / `tool` 等通过其它入口(工具面板、触发器面板、Start 标签)添加。
+
+---
+
+## 3. 节点配置字段表(按节点详情面板)
+
+### 字段类型缩写
+
+| 缩写 | 含义 |
+| --- | --- |
+| `var-reference` | 变量引用选择器(ValueSelector) |
+| `var-list` | 变量列表(可添加多个,每项 variable 名 + value_selector) |
+| `model-selector` | 模型 + 完成参数(弹窗) |
+| `prompt-editor` | 提示词编辑器(basic / jinja2 两种 edition) |
+| `code-editor` | 代码编辑器(带语法高亮) |
+| `select` / `input` / `number` / `textarea` / `switch` | 基础表单 |
+| `object` | 复杂嵌套对象 |
+| `conditional-list` | 条件列表(含逻辑运算符) |
+
+---
+
+### 3.1 Start(用户输入)`start`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `variables` | `InputVar[]`(var-list,支持 textInput/paragraph/select/number/url/files/json/jsonObject/contexts/iterator/singleFile/multiFiles/loop/checkbox) | 否 | `[]` | 用户自定义输入变量定义 |
+
+> 内置变量(无需配置):`userinput.query`(chat 模式 String)、`userinput.files`(Array[File])。
+
+### 3.2 End(输出)`end`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `outputs` | `Variable[]`(每项 `variable`、`value_selector`) | 是(至少 1 项且变量名/值引用都必填) | `[]` | 工作流最终输出变量 |
+
+### 3.3 Answer(直接回复)`answer`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `answer` | string(textarea + 变量插值,支持 `{{#var#}}`) | 是 | `''` | 回答内容 |
+| `variables` | `Variable[]` | 否 | `[]` | 回答中引用的变量列表 |
+
+### 3.4 LLM `llm`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `model.provider` | string(model-selector) | 是 | `''` | 模型供应商 |
+| `model.name` | string | 是 | `''` | 模型名 |
+| `model.mode` | `AppModeEnum`(CHAT/COMPLETION) | - | `CHAT` | 模式 |
+| `model.completion_params` | object | 否 | `{temperature: 0.7}` | 完成参数(temperature / top_p / max_tokens / stop 等) |
+| `prompt_template` | `PromptItem[]`(chat 模型)或 `PromptItem`(completion 模型) | 是(除非启用 memory) | `[{role:'system', text:''}]` | 提示词模板;每个 PromptItem 含 `role`(system/user/assistant)、`text`、`edition_type`(basic/jinja2)、`jinja2_text` |
+| `prompt_config.jinja2_variables` | `Variable[]` | 否 | - | Jinja2 模式下声明的变量 |
+| `context.enabled` | boolean | 否 | `false` | 启用上下文注入 |
+| `context.variable_selector` | ValueSelector(var-reference,过滤为 contexts) | 启用时必填 | `[]` | 上下文变量 |
+| `memory` | `Memory`(window.enabled/size、role_prefix、query_prompt_template) | 否 | - | 记忆(仅 chat 模式可用) |
+| `vision.enabled` | boolean | 否 | `false` | 启用视觉(图像)输入 |
+| `vision.configs.variable_selector` | ValueSelector | 启用时必填 | - | 视觉变量引用 |
+| `structured_output_enabled` | boolean | 否 | - | 启用结构化输出 |
+| `structured_output` | `{schema: SchemaRoot}`(type=object + properties + required) | 否 | - | 结构化输出 schema |
+| `reasoning_format` | `'tagged' \| 'separated'` | 否 | `'tagged'` | 推理内容格式 |
+
+**固定输出**:`text`(string)、`reasoning_content`(string)、`usage`(object)(来自 `LLM_OUTPUT_STRUCT`)。
+
+### 3.5 Knowledge Retrieval(知识检索)`knowledge-retrieval`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `query_variable_selector` | ValueSelector | 是 | `[]` | 查询变量 |
+| `query_attachment_selector` | ValueSelector | 否 | `[]` | 查询附件(图片等) |
+| `dataset_ids` | string[] | 是 | `[]` | 知识库(至少 1 个) |
+| `retrieval_mode` | `RETRIEVE_TYPE`(multiWay/oneWay) | 否 | `multiWay` | 检索模式 |
+| `multiple_retrieval_config.top_k` | number | 否 | `DATASET_DEFAULT.top_k` | top_k |
+| `multiple_retrieval_config.score_threshold` | number/null | 否 | - | 分数阈值 |
+| `multiple_retrieval_config.reranking_enable` | boolean | 否 | `false` | 启用重排序 |
+| `multiple_retrieval_config.reranking_model` | {provider, model} | multiWay+reranking 时必填 | - | 重排序模型 |
+| `multiple_retrieval_config.reranking_mode` | `RerankingModeEnum` | 否 | - | 重排序模式 |
+| `multiple_retrieval_config.weights` | object | 否 | - | 加权权重 |
+| `single_retrieval_config.model` | ModelConfig | oneWay 时必填 | - | 单路检索模型 |
+| `metadata_filtering_mode` | `MetadataFilteringModeEnum`(disabled/automatic/manual) | 否 | - | 元数据过滤模式 |
+| `metadata_filtering_conditions` | `{logical_operator, conditions: MetadataFilteringCondition[]}` | manual 时必填 | - | 元数据过滤条件 |
+| `metadata_model_config` | ModelConfig | automatic 时必填 | - | 元数据过滤模型 |
+
+**固定输出**:`result`(array[object])(`RETRIEVAL_OUTPUT_STRUCT`)。
+
+### 3.6 Question Classifier(问题分类器)`question-classifier`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `query_variable_selector` | ValueSelector | 是 | `[]` | 输入变量 |
+| `model` | ModelConfig | 是(provider 必填) | `{provider:'', name:'', mode:CHAT, completion_params:{temperature:0.7}}` | 模型 |
+| `classes` | `Topic[]`(id/name/label) | 是(至少 1 个;name 不能空) | `[{id:'1',name:'',label:'CLASS 1'},{id:'2',...}]` | 分类(每类对应一个输出分支) |
+| `instruction` | string | 否 | - | 高级指令 |
+| `memory` | Memory | 否 | - | 记忆 |
+| `vision.enabled` / `vision.configs` | 同 LLM | 否 | `{enabled:false}` | 视觉 |
+
+**固定输出**:`class_name`(string)、`class_label`(string)、`usage`(object)。
+
+### 3.7 IF/ELSE `if-else`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `cases` | `CaseItem[]` | 是(至少 1 个) | `[{case_id:'true', logical_operator:'and', conditions:[]}]` | 条件分支(IF + ELIF*) |
+| `cases[].logical_operator` | `LogicalOperator`(and/or) | 是 | `and` | 同一 case 内多条件的逻辑运算符 |
+| `cases[].conditions[].variable_selector` | ValueSelector | 是 | - | 比较变量 |
+| `cases[].conditions[].comparison_operator` | `ComparisonOperator`(contains/not contains/start with/end with/is/is not/empty/not empty/=/≠/>/</≥/≤/is null/is not null/in/not in/all of/exists/not exists/before/after) | 是 | - | 比较运算符 |
+| `cases[].conditions[].value` | string / string[] / boolean | 是(除 empty/notEmpty 等空值相关运算符) | - | 比较值 |
+| `cases[].conditions[].sub_variable_condition` | CaseItem(递归) | 否 | - | 子变量条件(嵌套) |
+| ELSE | 自动暴露 `false` 分支 | - | - | 不满足所有 case 时走 ELSE |
+
+### 3.8 Code `code`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `variables` | `Variable[]`(每项 variable 名 + value_selector) | 否(每项名/值必填) | `[]` | 输入变量 |
+| `code_language` | `CodeLanguage`(python3/javascript/json) | 否 | `python3` | 语言(panel 仅展示 Python3、JavaScript) |
+| `code` | string(code-editor) | 是 | `''` | 代码 |
+| `outputs` | `OutputVar`(Record<varName, {type: VarType, children: null}>) | 否 | `{}` | 输出变量声明 |
+
+### 3.9 Template Transform(模板转换)`template-transform`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `variables` | `Variable[]` | 否(每项名/值必填) | `[]` | 输入变量 |
+| `template` | string(Jinja2 code-editor) | 是 | `''` | 模板代码(Jinja2 语法) |
+
+**固定输出**:`output`(string)。
+
+### 3.10 HTTP Request `http-request`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `method` | `Method`(get/post/head/patch/put/delete) | 否 | `get` | HTTP 方法 |
+| `url` | string | 是 | `''` | API 地址 |
+| `variables` | `Variable[]`(用于 url/headers/params 插值) | 否 | `[]` | 输入变量 |
+| `authorization.type` | `AuthorizationType`(none/apiKey) | 否 | `none` | 鉴权类型 |
+| `authorization.config` | {type: basic/bearer/custom, api_key, header} | apiKey 时必填 | `null` | 鉴权配置 |
+| `headers` | string(key-value 表格) | 否 | `''` | 请求头 |
+| `params` | string | 否 | `''` | 查询参数 |
+| `body.type` | `BodyType`(none/form-data/x-www-form-urlencoded/raw-text/json/binary) | 否 | `none` | 请求体类型 |
+| `body.data` | `BodyPayload[]`(id/key/type[text\|file]/value/file)或 string(旧) | 否 | `[]` | 请求体内容 |
+| `ssl_verify` | boolean | 否 | `true` | SSL 校验 |
+| `timeout.max_connect_timeout` / `max_read_timeout` / `max_write_timeout` | number(毫秒) | 否 | 0(不限) | 超时 |
+| `retry_config.retry_enabled` | boolean | 否 | `true` | 启用重试 |
+| `retry_config.max_retries` | number | 否 | `3` | 最大重试次数 |
+| `retry_config.retry_interval` | number(毫秒) | 否 | `100` | 重试间隔 |
+
+**固定输出**:`body`(string)、`status_code`(number)、`headers`(object)、`files`(array[file])。
+
+### 3.11 Tool(工具)`tool`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `provider_id` | string | 是 | - | 工具供应商 ID |
+| `provider_type` | `CollectionType`(builtIn/custom/workflow/mcp) | 是 | - | 工具类型 |
+| `provider_name` | string | 是 | - | 供应商名 |
+| `tool_name` | string | 是 | - | 工具名 |
+| `tool_label` | string | 是 | - | 工具显示名 |
+| `tool_parameters` | `ToolVarInputs`(Record<name, {type: variable/constant/mixed, value}>) | 视 schema required | `{}` | 工具参数(动态) |
+| `tool_configurations` | `Record<string, any>` | 视 settingSchema required | `{}` | 工具配置 |
+| `tool_node_version` | string | 否 | `'2'` | 工具节点版本 |
+| `plugin_id` / `plugin_unique_identifier` | string | 否 | - | 插件标识 |
+
+**输出**:默认 `text`(string)、`files`(array[file])、`json`(array[object]);若工具有 `output_schema`,额外暴露其 properties。
+
+### 3.12 Parameter Extractor(参数提取器)`parameter-extractor`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `query` | ValueSelector | 是 | `[]` | 输入变量 |
+| `model` | ModelConfig | 是(provider 必填) | `{provider:'', name:'', mode:CHAT, completion_params:{temperature:0.7}}` | 模型 |
+| `reasoning_mode` | `ReasoningModeType`(prompt/function_call) | 否 | `prompt` | 推理模式 |
+| `parameters` | `Param[]`(name/type/options/description/required) | 是(至少 1 个;name/type/description 必填) | - | 提取参数定义 |
+| `parameters[].type` | `ParamType`(string/number/boolean/select/array[string]/array[number]/array[object]/array[boolean]) | 是 | - | 参数类型 |
+| `instruction` | string | 否 | - | 高级指令 |
+| `memory` / `vision` | 同 LLM | 否 | - | - |
+
+**输出**:每个 parameter 名 + 公共 `__is_success`(number)、`__reason`(string)、`__usage`(object)。
+
+### 3.13 Iteration(迭代)`iteration`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `start_node_id` | string | 自动 | `''` | 内部 start 节点 ID |
+| `iterator_selector` | ValueSelector | 是 | `[]` | 输入(要迭代的数组) |
+| `iterator_input_type` | VarType | - | - | 输入类型缓存 |
+| `output_selector` | ValueSelector | 是 | `[]` | 输出(迭代内部产出的变量) |
+| `output_type` | VarType | - | - | 输出类型 |
+| `is_parallel` | boolean | 否 | `false` | 并行模式 |
+| `parallel_nums` | number | 否 | `10` | 并行数 |
+| `error_handle_mode` | `ErrorHandleMode`(terminated/continue-on-error/remove-abnormal-output) | 否 | `terminated` | 错误处理模式 |
+| `flatten_output` | boolean | 否 | `true` | 是否展平输出 |
+
+> 特点:`isTypeFixed: true`,内含独立子工作流(含 `iteration-start`)。
+
+### 3.14 Loop(循环)`loop`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `start_node_id` | string | 自动 | `''` | 内部 start 节点 ID |
+| `loop_count` | number(1 ≤ n ≤ LOOP_NODE_MAX_COUNT) | 是 | `10` | 循环次数 |
+| `loop_variables` | `LoopVariable[]`(id/label/var_type/value_type/value) | 否(每项 label 必填) | - | 循环变量(迭代间持久化) |
+| `break_conditions` | `Condition[]`(同 if-else Condition 结构) | 是(每项变量/运算符必填) | `[]` | 中断条件 |
+| `logical_operator` | LogicalOperator | 否 | `and` | 多个中断条件之间的逻辑运算符 |
+| `error_handle_mode` | ErrorHandleMode | 否 | - | 错误处理模式 |
+
+> 特点:`isTypeFixed: true`,含 `loop-start` 自动节点。
+
+### 3.15 Loop End / Loop Start / Iteration Start
+
+均为自动辅助节点,无配置字段(默认值 `{}`,checkValid 永远返回 `isValid: true`)。`loop-end` 元数据 `isSingleton: true`。
+
+### 3.16 Variable Aggregator / Variable Assigner `variable-aggregator`(目录 `variable-assigner`)
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `output_type` | VarType | 否 | `any` | 输出类型 |
+| `variables` | `ValueSelector[]`(不启用分组时) | 是(至少 1 个且非空) | `[]` | 聚合的变量列表 |
+| `advanced_settings.group_enabled` | boolean | 否 | `false` | 启用分组 |
+| `advanced_settings.groups` | `({group_name, groupId, output_type, variables: ValueSelector[]})[]` | group_enabled 时必填(至少 1 组) | `[]` | 分组(每组独立聚合) |
+
+### 3.17 Variable Assigner(变量赋值 v2)`assigner`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `version` | `'1' \| '2'` | 否 | `'2'` | 版本 |
+| `items` | `AssignerNodeOperation[]` | 是(每项校验) | `[]` | 赋值操作列表 |
+| `items[].variable_selector` | ValueSelector | 是 | - | 被赋值变量(须指向会话变量) |
+| `items[].input_type` | `AssignerNodeInputType`(variable/constant) | - | - | 输入类型 |
+| `items[].operation` | `WriteMode`(over-write/clear/append/extend/set/+=/-=/*=//= /remove-first/remove-last) | 是 | - | 写入模式 |
+| `items[].value` | any / ValueSelector | 视 operation(除 clear/removeFirst/removeLast 外必填) | - | 设置值 |
+
+### 3.18 Document Extractor(文档提取器)`document-extractor`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `variable_selector` | ValueSelector(file 或 array[file]) | 是 | `[]` | 输入变量 |
+| `is_array_file` | boolean | 否 | `false` | 是否数组文件 |
+
+### 3.19 List Operator / List Filter `list-operator`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `variable` | ValueSelector | 是 | `[]` | 输入变量(数组) |
+| `var_type` / `item_var_type` | VarType | - | - | 类型缓存 |
+| `filter_by.enabled` | boolean | 否 | `false` | 启用过滤 |
+| `filter_by.conditions` | `Condition[]`(key/comparison_operator/value) | 启用时第一项的 key/operator 必填 | `[]` | 过滤条件 |
+| `extract_by.enabled` | boolean | 否 | `false` | 启用抽取 |
+| `extract_by.serial` | string('1'/first/last/...) | 否 | `'1'` | 抽取序号 |
+| `order_by.enabled` | boolean | 否 | `false` | 启用排序 |
+| `order_by.key` | ValueSelector \| string | 否 | `''` | 排序键 |
+| `order_by.value` | `OrderBy`(asc/desc) | 否 | `ASC` | 升降序 |
+| `limit.enabled` | boolean | 否 | `false` | 启用限制 |
+| `limit.size` | number | 否 | `10` | 限制数量 |
+
+### 3.20 Human Input(人工介入)`human-input`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `delivery_methods` | `DeliveryMethod[]`(id/type[webapp/email/slack/teams/discord]/enabled/config) | 是(至少 1 个启用;邮件需 subject/body 含 `{{#url#}}` + recipients 完整) | `[]` | 通知方式 |
+| `form_content` | string | 否 | `''` | 表单内容(说明文字) |
+| `inputs` | `FormInputItem[]`(type=paragraph/select/file/file-list;每项含 output_variable_name) | 否 | `[]` | 表单字段定义 |
+| `user_actions` | `UserAction[]`(id/title/button_style[primary/default/accent/ghost]) | 是(至少 1 个;id/title 不能空且 id 不能重复) | `[]` | 用户操作按钮(决定分支) |
+| `timeout` | number | 否 | `3` | 超时数值 |
+| `timeout_unit` | `'hour' \| 'day'` | 否 | `'day'` | 超时单位 |
+
+**输出**:每个 input 的 `output_variable_name`(类型按 input.type 映射)。
+
+### 3.21 Agent(旧版)`agent`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `agent_strategy_provider_name` | string | 是(动态) | - | 策略供应商 |
+| `agent_strategy_name` | string | 是 | - | 策略名 |
+| `agent_strategy_label` | string | - | - | 策略显示名 |
+| `agent_parameters` | `ToolVarInputs` | 动态(按策略 parameters 中 required) | - | 策略参数(含 model selector / tool selector 等) |
+| `output_schema` | Record<string, any> | - | - | 输出 schema |
+| `memory` | Memory | 否 | - | 记忆 |
+| `version` / `tool_node_version` | string | 否 | `'2'` | 版本 |
+
+**固定输出**:`text`(string)、`files`(array[file])、`json`(object)(`AGENT_OUTPUT_STRUCT`)。
+
+### 3.22 Agent V2 `agent-v2`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `agent_binding` | `AgentBinding`(binding_type: roster_agent/inline_agent + agent_id + current_snapshot_id) | 是(必须有效) | `{binding_type:'inline_agent'}` | Agent 绑定 |
+| `agent_node_kind` | `'dify_agent'` | - | `'dify_agent'` | 节点种类 |
+| `version` | `'2'` | - | `'2'` | 版本 |
+| `agent_task` | string | 否 | - | Agent 任务描述 |
+| `agent_declared_outputs` | `DeclaredOutputConfig[]` | 否 | - | 声明的输出配置 |
+
+> 校验:`hasValidAgentBinding`(roster_agent 须有 agent_id;inline_agent 须有 agent_id + current_snapshot_id)。
+
+### 3.23 Knowledge Base(知识库)`knowledge-index`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `index_chunk_variable_selector` | string[] | 是 | `[]` | 待索引块变量 |
+| `keyword_number` | number | 否 | `10` | 关键词数量 |
+| `retrieval_model.top_k` | number | 否 | `3` | top_k |
+| `retrieval_model.score_threshold_enabled` | boolean | 否 | `false` | 启用分数阈值 |
+| `retrieval_model.score_threshold` | number | 否 | `0.5` | 分数阈值 |
+| `chunk_structure` / `indexing_technique` / `embedding_model` / `embedding_model_provider` / `summary_index_setting` | object/string | 否 | - | 索引细节 |
+
+> 元数据:`isRequired`、`isUndeletable`、`isSingleton`、`isTypeFixed`。
+
+### 3.24 Data Source `datasource`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `plugin_id` | string | 是 | `''` | 插件 ID |
+| `provider_type` | `DatasourceProviderType`(含 `local_file`) | 是 | - | 供应类型 |
+| `provider_name` | string | 是 | - | 供应商名 |
+| `datasource_name` | string | 是 | - | 数据源名 |
+| `datasource_label` | string | - | - | 数据源显示名 |
+| `datasource_parameters` | `ToolVarInputs` | 动态(按 dataSourceInputsSchema required) | `{}` | 数据源参数 |
+| `datasource_configurations` | Record<string, any> | 否 | `{}` | 数据源配置 |
+| `fileExtensions` | string[] | 否 | - | 文件扩展名(local_file 时) |
+
+**输出**:通用输出 + LOCAL_FILE_OUTPUT(local_file 时)+ ragVars + 动态 output_schema。
+
+### 3.25 Trigger Schedule `trigger-schedule`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `mode` | `'visual' \| 'cron'` | 是 | `getDefaultScheduleConfig()` | 调度模式 |
+| `frequency` | `'hourly' \| 'daily' \| 'weekly' \| 'monthly'` | visual 时必填 | - | 频率 |
+| `cron_expression` | string(合法 cron) | cron 模式必填 | `''` | cron 表达式 |
+| `visual_config.time` | string(`h:mm AM/PM`) | daily/weekly/monthly 必填 | - | 时间 |
+| `visual_config.on_minute` | number(0-59) | hourly 必填 | - | 分钟 |
+| `visual_config.weekdays` | string[](sun/mon/.../sat) | weekly 必填 | - | 星期 |
+| `visual_config.monthly_days` | `(number \| 'last')[]`(1-31 或 'last') | monthly 必填 | - | 日期 |
+| `timezone` | string(合法 IANA 时区) | 否(自动填充) | - | 时区 |
+
+### 3.26 Trigger Webhook `trigger-webhook`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `webhook_url` | string | 是 | `''` | Webhook URL(系统生成) |
+| `webhook_debug_url` | string | 否 | - | 调试 URL |
+| `method` | `HttpMethod`(GET/POST/PUT/DELETE/PATCH/HEAD) | 否 | `POST` | 接收的 HTTP 方法 |
+| `content_type` | string | 否 | `application/json` | Content-Type |
+| `headers` | `WebhookHeader[]`(name/required) | 否 | `[]` | 期望的请求头 |
+| `params` | `WebhookParameter[]`(name/type/required) | 否 | `[]` | 查询参数声明 |
+| `body` | `WebhookParameter[]` | 否 | `[]` | 请求体参数声明 |
+| `async_mode` | boolean | 否 | `true` | 异步模式 |
+| `status_code` | number | 否 | `200` | 响应状态码 |
+| `response_body` | string | 否 | `''` | 响应体模板 |
+| `variables` | `Variable[]`(含一个 raw variable 默认项) | 否 | `[createWebhookRawVariable()]` | 输出变量声明 |
+
+### 3.27 Trigger Plugin `trigger-plugin`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `provider_id` / `provider_type` / `provider_name` | string/enum | 是 | - | 插件供应信息 |
+| `plugin_id` | string | 是 | `''` | 插件 ID |
+| `event_name` / `event_label` | string | 是 | `''` | 事件名 |
+| `event_parameters` | `PluginTriggerVarInputs` | 动态(按 triggerInputsSchema required) | `{}` | 事件参数 |
+| `event_configurations` | Record<string, any> | 否 | - | 事件配置 |
+| `output_schema` | Record<string, any> | - | - | 输出 schema |
+| `subscription_id` | string | 是 | - | 订阅实例 ID |
+| `config` | Record<string, any> | 否 | `{}` | 兼容字段 |
+
+**输出**:从 `output_schema` 的 properties 动态构建(支持嵌套 object,递归 `convertJsonSchemaToField`)。
+
+### 3.28 Empty Data Source / Start Placeholder
+
+- `datasource-empty`:无字段,checkValid 永远通过。
+- `start-placeholder`:仅含通用 `title`、`desc`,checkValid 永远返回 invalid 并提示"需要选择具体起始节点"。
+
+---
+
+## 4. 节点通用机制
+
+### 4.1 所有节点隐式继承的通用字段(`CommonNodeType<T>`)
+
+| 字段 | 类型 | 含义 |
+| --- | --- | --- |
+| `title` / `desc` | string | 节点标题与描述(用户可编辑) |
+| `type` | BlockEnum | 节点类型 |
+| `selected` | boolean | 是否选中 |
+| `width` / `height` / `position` | number / XYPosition | 画布位置尺寸 |
+| `isInIteration` / `iteration_id` | boolean / string | 是否在迭代节点内 |
+| `isInLoop` / `loop_id` | boolean / string | 是否在循环节点内 |
+| `error_strategy` | `ErrorHandleTypeEnum`(none/fail-branch/default-value) | 错误处理策略 |
+| `retry_config` | `WorkflowRetryConfig`{retry_enabled, max_retries, retry_interval} | 重试配置 |
+| `default_value` | `DefaultValueForm[]`{key, type, value} | 错误处理为 default-value 时的默认值 |
+| `credential_id` / `subscription_id` / `provider_id` | string | 凭证/订阅/供应商 |
+| `_isSingleRun` / `_runningStatus` 等 | various | 运行时内部状态 |
+
+### 4.2 异常处理 / 重试 / 超时
+
+| 项 | 取值 | 出处 |
+| --- | --- | --- |
+| 错误处理策略 `ErrorHandleTypeEnum` | `none` / `fail-branch` / `default-value` | `_base/components/error-handle/types.ts` |
+| 错误处理模式 `ErrorHandleMode`(迭代/循环节点) | `terminated` / `continue-on-error` / `remove-abnormal-output` | `nodes/_base/types.ts` |
+| 重试配置 `WorkflowRetryConfig` | `{retry_enabled, max_retries, retry_interval}` | `_base/components/retry/types.ts` |
+| 全局默认重试 | `DEFAULT_RETRY_MAX = 3`,`DEFAULT_RETRY_INTERVAL = 100`(ms) | `web/app/components/workflow/constants.ts:296-297` |
+| HTTP 节点私有重试 | `retry_enabled: true, max_retries: 3, retry_interval: 100` | `nodes/http/default.ts` |
+| HTTP 节点私有超时 | `timeout.{max_connect_timeout, max_read_timeout, max_write_timeout}`(ms,默认 0 = 不限) | `nodes/http/default.ts` |
+
+UI 入口:`_base/components/error-handle/error-handle-on-panel.tsx` 与 `_base/components/retry/retry-on-panel.tsx`,各节点 panel 直接引入。
+
+### 4.3 输出变量声明机制(三种)
+
+1. **静态固定输出**(`constants.ts` 中的 `*_OUTPUT_STRUCT` 常量):
+   - `LLM_OUTPUT_STRUCT`:text / reasoning_content / usage
+   - `KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT`:result
+   - `TEMPLATE_TRANSFORM_OUTPUT_STRUCT`:output
+   - `QUESTION_CLASSIFIER_OUTPUT_STRUCT`:class_name / class_label / usage
+   - `HTTP_REQUEST_OUTPUT_STRUCT`:body / status_code / headers / files
+   - `TOOL_OUTPUT_STRUCT`:text / files / json
+   - `HUMAN_INPUT_OUTPUT_STRUCT`:__action_id / __action_value / __rendered_content
+   - `PARAMETER_EXTRACTOR_COMMON_STRUCT`:__is_success / __reason / __usage
+   - `AGENT_OUTPUT_STRUCT`:text / files / json
+   - `FILE_STRUCT`:name/size/type/extension/mime_type/transfer_method/url/related_id
+   - `SUPPORT_OUTPUT_VARS_NODE`(`constants.ts:103-125`)声明哪些节点能被其他节点引用输出
+
+2. **用户在节点面板中声明**:
+   - Code 节点 `outputs: Record<varName, {type}>`
+   - End 节点 `outputs: Variable[]`
+   - Parameter Extractor `parameters: Param[]`(每个 param.name 成为一个输出)
+   - Question Classifier `classes`(每个 class 一个分支,输出仍为 class_name)
+   - Human Input `inputs`(每个 input 的 `output_variable_name`)
+   - Trigger Webhook `variables`
+
+3. **动态从插件 / tool 的 `output_schema` 推断**:
+   - Tool 节点:从 `output_schema.properties` 解析(`tool/default.ts` 的 `getOutputVars`),叠加默认 TOOL_OUTPUT_STRUCT
+   - Data Source 节点:从 `output_schema` 解析(`data-source/default.ts`),叠加 COMMON_OUTPUT / LOCAL_FILE_OUTPUT / ragVars
+   - Trigger Plugin 节点:从 `output_schema` 解析(`trigger-plugin/default.ts`),支持嵌套(递归 `convertJsonSchemaToField`)
+
+`NodeDefault.getOutputVars(payload, allPluginInfoList, ragVariables, utils)` 是统一的输出变量计算入口。
+
+### 4.4 节点元数据特性(`genNodeMetaData`)
+
+| 字段 | 含义 |
+| --- | --- |
+| `classification` | 分类(默认 `-`) |
+| `sort` | 同分类内的排序值 |
+| `type` | BlockEnum |
+| `title` | 默认标题(多数为空,靠 i18n 渲染) |
+| `author` | 默认 `'Dify'`(Loop 节点为 `'AICT-Team'`) |
+| `helpLinkUri` | 文档锚点 |
+| `isRequired` | 是否必需(answer/knowledge-base/datasource 为 true) |
+| `isUndeletable` | 不可删除(knowledge-base、datasource-empty) |
+| `isStart` | 是否可作为起始(start、start-placeholder、3 个 trigger、datasource) |
+| `isSingleton` | 画布上唯一(start、start-placeholder、loop-end、knowledge-base、datasource-empty) |
+| `isTypeFixed` | 类型不可切换(start-placeholder、iteration、loop、knowledge-base、datasource-empty) |
+
+### 4.5 节点目录约定
+
+```
+nodes/<type>/
+  ├── default.ts     # 元数据 + 默认值 + checkValid + getOutputVars
+  ├── types.ts       # TypeScript 类型定义
+  ├── panel.tsx      # 详情面板组件(右侧抽屉)
+  ├── node.tsx       # 画布上的节点卡片渲染
+  ├── index.tsx      # 导出入口(部分节点)
+  └── components/    # 面板内子组件(部分节点)
+```
+
+### 4.6 校验机制
+
+每个节点的 `default.ts` 导出 `checkValid(payload, t, moreDataForCheckValid?)`,返回 `{isValid, errorMessage}`。共同模式:
+- 必填字段:`t('errorMsg.fieldRequired', {field: ...})`
+- i18n 前缀:部分节点用 `'errorMsg'`,部分用空字符串拼接 `'errorMsg.xxx'`
+
+### 4.7 字段组件库(`_base/components/`)
+
+| 组件 | 用途 |
+| --- | --- |
+| `field.tsx` | 字段容器(title/required/tooltip/operations) |
+| `selector.tsx` | 下拉选择 |
+| `input-number-with-slider.tsx` | 数字 + 滑块 |
+| `input-support-select-var.tsx` | 文本输入 + 变量引用切换 |
+| `readonly-input-with-select-var.tsx` | 只读输入 + 变量引用 |
+| `variable/var-list.tsx` | 变量列表 |
+| `variable/var-reference-picker.tsx` | 变量引用选择器 |
+| `variable/output-var-list.tsx` | 输出变量声明列表 |
+| `editor/code-editor.tsx` | 代码编辑器(Python/JS/Jinja2) |
+| `config-vision.tsx` | 视觉配置 |
+| `memory-config.tsx` | 记忆配置 |
+| `file-upload-setting.tsx` | 文件上传设置 |
+| `prompt/` | 提示词编辑器 |
+| `error-handle/error-handle-on-panel.tsx` | 错误处理 |
+| `retry/retry-on-panel.tsx` | 重试配置 |
+
+### 4.8 内置变量与系统变量
+
+- **Start 节点自动暴露**:`userinput.query`(chat 模式 String)、`userinput.files`(Array[File])
+- **系统全局变量**(`constants.ts:35-76` 的 `getGlobalVars`):
+  - `sys.dialogue_count`、`sys.conversation_id`、`sys.user_id`、`sys.app_id`、`sys.workflow_id`、`sys.workflow_run_id`、`sys.timestamp`
+- **VAR_SHOW_NAME_MAP**:`sys.query` → query、`sys.files` → files
+
+### 4.9 节点版本兼容
+
+| 节点 | 版本字段 |
+| --- | --- |
+| `tool` | `tool_node_version: '2'`(默认) |
+| `assigner` | `version: '2'` |
+| `agent-v2` | `version: '2'`、`agent_node_kind: 'dify_agent'` |
+| LLM prompt | 旧版 `edition_type: 'basic'` vs 新版 `'jinja2'` |
+
+---
+
+## 5. 与本仓库(agent-management)的对照
+
+> 平台(极智)和 Dify 在工作流节点体系上高度相似——本仓库在做工作流引擎 / 节点 schema 设计时可作如下对照。
+
+| 维度 | 极智平台(参考 `jizhi-platform-reference.md`) | Dify(本文档) |
+| --- | --- | --- |
+| 节点类型数 | 26 | 31(含辅助 / 占位节点) |
+| 分类 | 基础 / 问题处理 / 逻辑 / 工具 / 文件 / 转换(6 类) | 默认 / 问题理解 / 逻辑 / 转换 / 工具(5 类) |
+| 大模型节点 | 模型 / 短期记忆 / 流式 / JSON / 温度 / 思考模式 / 思考级别 / 系统提示 / 用户输入 / 异常处理 | 模型 / prompt_template(basic+jinja2) / context / memory / vision / structured_output / reasoning_format + 异常处理 |
+| 异常处理 | 处理方式(无 / 默认值 / 异常分支) + 最大重试次数 | error_strategy(none / fail-branch / default-value) + retry_config(retry_enabled / max_retries / retry_interval) |
+| 知识库检索 | 知识库选择 + 推测字段 | query / dataset_ids / retrieval_mode / 重排序 / 元数据过滤(完整字段化) |
+| 条件分支 | 条件表达式(多分支) | cases(IF + ELIF*)+ 隐式 ELSE + 子变量嵌套条件 |
+| 迭代 | 输入列表 + 模式 + 并发 + 子图 | iterator_selector + output_selector + is_parallel + parallel_nums + error_handle_mode + flatten_output |
+| 工具调用 | 工具选择 + Schema 参数 | provider_id/type/name + tool_parameters(动态) + tool_configurations |
+| HTTP 请求 | URL / Method / 重试 / 超时 | method/url/auth/headers/params/body/ssl_verify/timeout(3 项)/retry_config |
+| 人工确认 | 提示文案 + 交互方式 + 超时 + 默认值 | delivery_methods + form_content + inputs + user_actions + timeout |
+| 数据查询 | 自动 SQL / 手动 SQL | (Dify 通过 Tool / Data Source 节点承载,无独立"数据查询"节点) |
+| 触发器 | (自动化模块独立承载,无独立触发节点) | trigger-schedule / trigger-webhook / trigger-plugin(节点级别) |
+| 输出变量声明 | 每节点固定输出 + 用户在节点内声明 | 三种机制:固定常量 / 用户面板声明 / output_schema 动态推断 |
+
+### 设计建议(落地到 agent-management 时)
+
+1. **节点 schema 统一用 TS 类型 + zod 校验**:参考 Dify 的 `types.ts` + `default.ts` 中 `checkValid` 模式,每个节点单独目录维护。
+2. **字段组件库优先抽象**:var-reference / var-list / prompt-editor / code-editor / model-selector / error-handle / retry 等通用组件应抽到 `_base/components/`,避免每个节点重复实现。
+3. **输出变量声明支持三种模式**:固定 / 用户声明 / schema 推断——对工具型节点(如 HTTP / Tool)尤其重要。
+4. **错误处理 + 重试在节点级别通用化**:避免每个节点单独定义。
+5. **触发器 / Start 节点的 `isStart`、`isRequired`、`isSingleton`、`isTypeFixed` 元数据**:用于编排器自动约束(如 Start 只能有一个、Knowledge-Index 类型不可切换)。
+
+---
+
+## 6. 覆盖完整性核对
+
+`BlockEnum` 共 31 个枚举值(`types.ts:27-60`),本报告全部覆盖:
+
+- ✅ Start, StartPlaceholder, End, Answer, LLM, KnowledgeRetrieval, QuestionClassifier, IfElse, Code, TemplateTransform, HttpRequest
+- ✅ VariableAssigner, VariableAggregator(共用 `variable-assigner` 目录), Tool, ParameterExtractor
+- ✅ Iteration, DocExtractor, ListFilter, IterationStart, Assigner
+- ✅ Agent, AgentV2, Loop, LoopStart, LoopEnd
+- ✅ HumanInput, DataSource, DataSourceEmpty, KnowledgeBase
+- ✅ TriggerSchedule, TriggerWebhook, TriggerPlugin
+
+每个节点都列出了:源码相对路径、字段表(变量名 / 类型 / 必填 / 默认 / 说明)、所属分类、元数据特性(`isRequired` / `isSingleton` / `isTypeFixed` 等)。

+ 247 - 0
docs/reference/hermes-agent-upgrade-reference.md

@@ -0,0 +1,247 @@
+# Hermes-Agent 上游升级参考
+
+> 项目位置:`backend/hermes-agent/`(第三方仓库 `NousResearch/hermes-agent` 的本地克隆,已通过父项目 `.gitignore` 排除)
+> 文档目的:记录一次 hermes-agent 上游升级(`git pull`)带来的总量数据、关键变更、与本项目的兼容性影响,便于后续评估升级风险、规划适配工作。
+> 评估时点:2026-08-03
+> 评估范围:基线 `021ed69141`(2026-06-11)→ HEAD `f4604f89`(2026-08-02)
+
+---
+
+## 0. 一句话定位
+
+Hermes-Agent 是 [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent) 仓库提供的智能体运行时,本项目(`agent-management`)通过 `backend/hermes-bridge/` 内的 Python Bridge 进程托管它,再由 Java 侧 `HermesBridgeClient` 通过 SSE 通信完成工作流编排。Bridge 与 hermes-agent 的耦合点主要包括:`agent.clarify_callback` 钩子、`tools.clarify_gateway` 模块、`tools.file_tools` 中的路径解析函数(由 `sandbox_patch.py` 做 monkey-patch 实现沙箱隔离)。
+
+**本次升级的核心影响**:上游已原生支持 clarify 永久等待(`timeout=None`/`<=0`),本项目对 `tools/clarify_gateway.py` 的私有 patch 已被覆盖、可废弃。
+
+---
+
+## 1. 总量数据
+
+| 维度 | 数值 |
+|---|---|
+| 基线 commit | `021ed6914162416522462b009de0bec1513c73a1` |
+| 基线时间 | 2026-06-11 17:22:22 -0400 |
+| 基线 subject | `docs: finish Automation Blueprints terminology rebrand (#44470)` |
+| 当前 HEAD | `f4604f89dec8d225d67d7a68065c1b425fd3732a` |
+| HEAD 时间 | 2026-08-02 22:14:27 -0500 |
+| HEAD subject | `Merge pull request #77275 from NousResearch/bb/tab-reload` |
+| 时间跨度 | 约 52 天 |
+| 提交总数 | **8 936** |
+| 文件变更 | 6 620 个 |
+| 代码变更 | +1 055 298 / −359 700 行 |
+| 涉及 `tools/` 的 commit | 1 376 |
+| 涉及 `agent/` 的 commit | 993 |
+
+> 量级说明:约 52 天内涌入近 9000 个 commit、超过 100 万行新增代码,远超普通迭代节奏。这通常意味着上游在此期间合并了一个或多个长期分支(long-running feature branch),或完成了某次大规模重构。`docs: finish Automation Blueprints terminology rebrand` 这条基线 commit subject 也暗示此前刚刚完成术语统一("Automation Blueprints" 大概率是新品牌名),本次升级见证了术语落地后的特性爆发期。
+
+---
+
+## 2. Commit 类型分布
+
+| 类型 | 数量 | 占比 |
+|---|---|---|
+| `fix` | 4 443 | 49.7% |
+| `feat` | 988 | 11.0% |
+| `Merge PR` | 764 | 8.5% |
+| `test` | 610 | 6.8% |
+| `chore` | 513 | 5.7% |
+| `refactor` | 275 | 3.1% |
+| `docs` | 183 | 2.0% |
+| `perf` | 142 | 1.6% |
+| `fmt` | 118 | 1.3% |
+| `security` | 25 | 0.3% |
+
+**观察**:fix 数量是 feat 的 4.5 倍,说明此阶段上游以密集修复为主,新特性交付与修复并行;security 有 25 条,建议单独过一遍(见第 5 节)。
+
+---
+
+## 3. 新增特性(feat)子系统分布
+
+| 子系统 | feat 数 | 备注 |
+|---|---|---|
+| **desktop** | 279 | 桌面客户端,本轮最大特性波 |
+| **gateway** | 53 | HTTP/SSE 网关与 clarify 重写 |
+| **cli** | 32 | 命令行 |
+| **agent** | 28 | AIAgent 核心抽象 |
+| **relay** | 27 | Hermes Relay 远程协同 |
+| **skills** | 21 | 技能系统 |
+| **kanban** | 20 | Kanban 任务视图 |
+| **dashboard** | 20 | 仪表盘 |
+| **voice** | 17 | 语音输入 |
+| **slack** | 17 | Slack 集成 |
+| **moa** | 15 | Mixture-of-Agents |
+| **tui / ui-tui** | 23 | 终端 UI |
+| **memory** | 12 | 记忆系统 |
+| **cron** | 12 | 定时任务 |
+| **tts** | 10 | 文本转语音 |
+| **tools** | 10 | 工具集 |
+| **compression** | 10 | 上下文压缩 |
+| **mcp** | 9 | Model Context Protocol |
+| **photon** | 8 | iMessage 集成 |
+| **models** | 8 | 模型管理 |
+| **config** | 8 | 配置 |
+| **ci** | 8 | 持续集成 |
+| **secrets** | 7 | 密钥处理 |
+| **providers** | 7 | 模型 provider |
+| **plugins** | 7 | 插件 |
+| **delegation** | 7 | 委托 |
+| **themes** | 6 | 主题 |
+
+> 上表按 feat 数倒序排列前 27 个子系统。desktop 一项就占全部 feat 的 28%,是本轮迭代的主战场。
+
+---
+
+## 4. 关键变更主题
+
+### 4.1 Clarify / 用户交互(与本项目强相关)
+
+这是本次升级中对 `agent-management` 影响最大的主题。
+
+- **`507d479c8c` fix(clarify): one canonical timeout across CLI, TUI/desktop, and gateway (#69774)**(2026-07-22)
+  - 把 CLI / TUI / gateway 三处各自实现的 clarify 超时统一为单一解析器 `tools.clarify_gateway.resolve_clarify_timeout(config)`。
+  - 解析顺序:显式 `clarify.timeout`(向后兼容)→ `agent.clarify_timeout`(默认 3600)→ 兜底 3600。
+  - **关键**:`<= 0` 与 `None` 都被视为"永久不超时",仅靠 1s 切片轮询维持 watchdog 心跳。
+  - **本项目影响**:`backend/hermes-bridge/hermes_bridge.py:403` 的 `wait_for_response(clarify_id, timeout=None)` 直接命中上游新能力,对 `tools/clarify_gateway.py` 的私有 patch 已可废弃。详见 `docs/patches/hermes-agent/README.md`。
+- `10b7ab5cb6` feat(clarify): extend multi-select to gateway text fallback and TUI bridge —— 多选 clarify 扩展到 gateway 文本回退路径。
+- `fe95194c59` feat(photon): render multiple-choice clarify as a native iMessage poll —— 多选 clarify 在 iMessage 上渲染为原生 poll。
+- `c0dd6e1f3f` fix(desktop): stop a clarify card swallowing keys it doesn't bind —— 桌面端 clarify 卡片不再吞键盘事件。
+- `251b668f4d` fix(desktop): let the composer answer past a clarify card —— composer 可越过 clarify 卡片继续回答。
+- `aa40f16d3e` fix(gateway): transcribe clarify voice replies —— 转写 clarify 语音回复。
+- `aacc15b2c9` fix(clarify): raise default clarify_timeout to 3600s (#32762) —— 默认超时从 60s 提升到 3600s。
+- `tools/clarify_gateway.py`:单文件 +202/−21,几乎重写。
+
+### 4.2 Desktop 客户端(279 feat,本轮最大特性波)
+
+近期 merge PR 主线:
+- `bb/tab-reload`、`bb/composer-path-copy`、`bb/composer-link-open`、`bb/composer-placeholder` —— composer(输入框)多轮打磨。
+- `bb/desktop-lockfile-engines`、`bb/npm-engine-outage` —— 桌面运行时引擎隔离与 npm 引擎故障修复。
+- `bb/win-update-lock-handoff`、`ethie/bundled-node-path-windows-layout` —— Windows 升级锁交接与打包 Node 路径修复。
+- `bb/sqlite-repair-locked` —— SQLite 锁修复。
+- `bb/kanban-model-picker` —— Kanban 视图模型选择器。
+- `bb/example-plugin-off-by-default` —— 示例插件默认关闭。
+
+> 与本项目关系:**完全独立**。本项目通过 Bridge 进程调用 hermes-agent,不使用其桌面应用。升级不影响 Bridge 兼容性。
+
+### 4.3 网关与 Bridge(gateway 53 feat)
+
+- HTTP/SSE 网关整体增强。
+- `tools/clarify_gateway.py` 引入 `resolve_clarify_timeout` 统一解析器(见 4.1)。
+- gateway 端 `notify` / `resolve_gateway_clarify` 等 API 形态有调整。
+
+> **建议**:Bridge 侧调用的 `clarify_gateway.register`、`clarify_gateway.resolve_gateway_clarify`、`clarify_gateway.clear_session`、`clarify_gateway.wait_for_response` 这四个 API 升级后是否仍保持原签名,需在新版本上跑一次完整 clarify 流程验证(详见第 6 节)。
+
+### 4.4 Agent 核心(agent 28 feat)
+
+- AIAgent 抽象更新。
+- 委托(delegation)结构化 stall 元数据 + 子 agent 实时状态:`b792bd0529 feat(delegation): structured stall metadata + live per-child status in /agents`。
+- 上下文压缩 ceiling 修复:`06c7f9b26f fix(agent): clarify compress_context ceiling is pre-commit only`。
+- 批量轨迹持久化与池清理:`a1ff62a139 fix: context-length fallback logging, batch trajectory durability, pool cleanup`。
+
+> **建议**:`agent.clarify_callback` 钩子签名(`hermes_bridge.py:451` 用到)若变化会直接破坏 Bridge,需重点验证。
+
+### 4.5 Relay / Skills / MCP / Plugins
+
+- **Relay(27 feat)**:Hermes Relay 远程协同完善;prompts 信任 `run.py` 线程时间戳(不再自我锚定重推导)。
+- **Skills(21 feat)**:技能系统持续打磨。
+- **MCP(9 feat)**:Model Context Protocol 支持。
+- **Plugins(7 feat)**:示例插件默认关闭。
+
+### 4.6 性能(perf 142)
+
+- `9acc4b47f5` perf(state): external-content FTS + tool-row-free trigram index(schema v23)—— 状态索引大重构。
+- `3a69e34702` perf(update): cut redundant network + subprocess work from hermes update —— 升级流程提速。
+
+### 4.7 安全(security 25)
+
+- secrets 处理增强:`fe8c0f7eef fix(secrets): pass OP_LOAD_DESKTOP_APP_SETTINGS through to the op child env`。
+- `/private/var` 路径收窄:`7f4d155159 fix(tools): validate timeout, reject whitespace old_string, narrow /private/var block`。
+- Windows 探针防死锁:`b9dba7eff5 fix(env-probe): stuck Windows probe can no longer deadlock system-prompt builds (#67999)`。
+- Bootstrap 下载超时与 BOM 升级:`73b3a8afe3 fix(bootstrap): download timeouts + BOM upgrade for pre-fix cached scripts (#67193 follow-up)`。
+- git 不再阻塞内部调用:`58708c7066 fix(git): never block internal git calls on credential prompts`。
+
+### 4.8 平台与 Provider
+
+- photon(8 feat):iMessage 集成,多选 clarify 渲染为原生 poll。
+- slack(17 feat)、voice(17 feat)、tts(10 feat)。
+- providers / models:xAI active_provider 契约锁定、Groq STT 语言提示等。
+
+---
+
+## 5. 兼容性影响评估
+
+| 风险点 | 影响层级 | 评估结论 | 建议动作 |
+|---|---|---|---|
+| `wait_for_response(timeout=None)` 语义 | Bridge | ✅ 上游 PR #69774 已原生支持 | 私有 patch 废弃 |
+| `agent.clarify_callback` 钩子签名 | Bridge | ⚠️ 上游 agent 模块 993 commit、28 feat,签名可能有微调 | 跑一次完整 clarify 流程验证 |
+| `clarify_gateway.{register, resolve_gateway_clarify, clear_session, wait_for_response}` API | Bridge | ⚠️ 单文件 +202/−21,几乎重写 | 同上 |
+| `tools.file_tools._resolve_path_for_task` / `_resolve_base_dir` | sandbox_patch.py monkey-patch 目标 | ⚠️ tools/ 涉及 1376 个 commit | 跑一次文件读写流程验证 |
+| `agent.coding_context._git_root` | sandbox_patch.py monkey-patch 目标 | ⚠️ 同上 | 跑一次 git 操作流程验证 |
+| 桌面 / Kanban / Photon / Slack 等子系统 | Bridge | ✅ 完全独立 | 无需动作 |
+
+---
+
+## 6. 升级后建议的回归测试清单
+
+升级 hermes-agent 后,建议按以下清单手动回归(参考 `AGENTS.md`:复杂网页操作不使用 Playwright,交由人工测试):
+
+1. **Bridge 启动**:拉起 Bridge 进程,确认无 monkey-patch 失败日志。
+2. **基础对话**:跑一次不涉及工具调用的智能体对话,验证 `AIAgent.run` 主路径无回归。
+3. **文件读写**:在工作流中触发一次文件读写节点,验证 `sandbox_patch.py` 的 `_resolve_path_for_task` / `_resolve_base_dir` patch 仍生效(路径仍被限制在 workingDir)。
+4. **git 操作**:触发一次需要 `_git_root` 的操作,验证 patch 仍指向正确的 workingDir。
+5. **Clarify 全流程**(关键):在工作流中触发"智能操作"节点的 clarify(用户提问 → 前端弹窗 → 用户回答 → 继续执行),确认:
+   - `clarify_gateway.register` / `wait_for_response(timeout=None)` / `resolve_gateway_clarify` 全链路无异常。
+   - Bridge 进程的 agent 线程在用户回答前持续阻塞(无意外超时返回 None)。
+   - Java 侧 SSE 连接保持(无提前断开)。
+   - 前端断线重连后能恢复 clarify 弹窗状态。
+6. **多 clarify 并发**:同一 run 中先后触发两次 clarify,确认 `_clarify_ids` 集合与 `clear_session` 清理正常。
+7. **24h 兜底**(可选):构造 clarify 长时间不回答场景(如离线 1 小时),确认 Java 侧 `SseEmitter(86_400_000L)` 与 `setReadTimeout(86_400_000)` 未提前超时。
+
+---
+
+## 7. 上游基线信息(升级后)
+
+> 本节内容用于 `docs/patches/hermes-agent/README.md` 的"上游基线"小节同步更新。
+
+| 项 | 值 |
+|---|---|
+| 远程仓库 | https://github.com/NousResearch/hermes-agent |
+| 子项目本地路径 | `backend/hermes-agent/` |
+| 父项目 `.gitignore` 排除规则 | `hermes-agent/` |
+| 当前基线 commit | `f4604f89dec8d225d67d7a68065c1b425fd3732a` |
+| 基线 commit subject | `Merge pull request #77275 from NousResearch/bb/tab-reload` |
+| 基线时间 | 2026-08-02 22:14:27 -0500 |
+| 当前 main 分支跟踪状态 | 正常跟踪 `origin/main`(非 detached HEAD) |
+
+---
+
+## 8. 升级操作回放
+
+本次升级的实际操作记录:
+
+```bash
+# 在父项目根目录
+cd backend/hermes-agent
+
+# 当前工作区有私有 patch(已 apply),先还原
+python ../docs/patches/hermes-agent/apply-patches.py --revert
+
+# 拉取上游
+git pull --ff-only origin main
+
+# 验证 patch 是否仍需要(--check 显示找不到原始锚点,说明上游已重写该段)
+python ../docs/patches/hermes-agent/apply-patches.py --check
+
+# 决策:上游 PR #69774 已原生覆盖能力 → 不再 apply,标记 patch 为废弃
+```
+
+---
+
+## 9. 参考链接
+
+- 上游仓库:https://github.com/NousResearch/hermes-agent
+- 关键 PR:[#69774](https://github.com/NousResearch/hermes-agent/pull/69774) fix(clarify): one canonical timeout across CLI, TUI/desktop, and gateway
+- 私有 patch 记录:[`docs/patches/hermes-agent/README.md`](../patches/hermes-agent/README.md)
+- Bridge 适配层关键文件:
+  - [`backend/hermes-bridge/hermes_bridge.py`](../../backend/hermes-bridge/hermes_bridge.py)
+  - [`backend/hermes-bridge/sandbox_patch.py`](../../backend/hermes-bridge/sandbox_patch.py)
+  - [`backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java`](../../backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java)

+ 1257 - 0
docs/reference/jizhi-platform-reference.md

@@ -0,0 +1,1257 @@
+# 极智·智能体开发和应用平台 参考
+
+> 本文件基于对 `https://szrh.cetccity.com:10503/` 平台的实际访问,并配合对前端 JS bundle(`/workflow-web/assets/index-2e3277ca.js`,约 7.4MB)的源码反查,整理出 26 个工作流节点的完整权威 schema(`initialData` 默认值、`outputVariables` 输出结构、`allowedConnections` 连接方向等),供本仓库后续做工作流引擎、节点 schema 等功能时直接对照落地。
+>
+> 探索日期:2026-08-02。节点字段均来自前端注册表 `Y9e` 与各节点面板组件源码,**非推断**。
+
+---
+
+## 1. 平台总览
+
+| 项 | 值 |
+| --- | --- |
+| 平台名称 | 极智·智能体开发和应用平台(JIZHI · AGENT DEVELOPMENT AND APPLICATION PLATFORM) |
+| 入口 | `https://szrh.cetccity.com:10503/` |
+| 登录方式 | 手机号 + 密码(亦支持验证码登录) |
+| 主菜单 | 概览、智能体、自动化、数据源、模型、知识库、标签管理、其他管理(标签 / 模型 / 错误模式)、系统管理 |
+| 鉴权 Token | 登录成功后写入 `localStorage["Admin-Token"]`,并伴随 `Admin-Refresh-Token` / `Admin-Token-Expire-At` / `xSessionId` / `cryptKeys` / `cryptConf` 等 |
+| API 前缀 | `/jizhiApi/...`(请求体/响应体使用 `cryptConf` 中协商的密钥做加密,仅传 `Admin-Token` 不足以直接调用业务接口) |
+
+> 注:直接用 `fetch('/jizhiApi/...')` 调用业务接口会返回 `{"msg":"当前请求会话中不存在加密秘钥","code":521}`,需要先在浏览器内由前端 SDK 完成密钥协商。如需调试,请在浏览器控制台中复用前端已发出的请求而不是新造一个。
+
+---
+
+## 2. 自动化(Automation)模块
+
+入口:左侧菜单"自动化",对应路由 `/automation`。
+
+### 2.1 列表与创建入口
+
+页面顶部提供两个创建入口:
+
+- **自动创建**:通过自然语言描述生成自动化任务(向导式)。
+- **手动创建**:弹出对话框直接配置。
+
+### 2.2 手动创建对话框字段
+
+打开"手动创建"对话框后,需要填写的字段如下(截图:`automation-manual-create.png`):
+
+| 字段 | 类型 | 是否必填 | 说明 |
+| --- | --- | --- | --- |
+| 任务名称 | 文本 | 是 | 自动化任务的展示名称 |
+| 任务类型 | 单选 | 是 | `Agent` 或 `工作流`,决定后续执行入口 |
+| 目标 | 下拉选择 | 是 | 选择具体的智能体或工作流(依"任务类型"动态过滤) |
+| 模型 | 下拉选择 | 否 | 执行时使用的默认大模型 |
+| 执行计划 | 单选 + 高级 | 是 | `每天` / `每周` / `每月` / `每小时` / `间隔` / `高级`,每种选项会进一步展开时间设置(如每周需选择星期几、每天需选择具体时刻) |
+| 时区 | 下拉选择 | 是 | 默认 `Asia/Shanghai` |
+| 启用 | 开关 | 否 | 创建后是否立即启用 |
+| 文本问题 | 文本域 | 是 | 自动化运行时输入给智能体/工作流的问题内容 |
+| 附件 | 文件上传 | 否 | 上传文件作为输入 |
+
+> "高级"执行计划支持类 cron 表达式配置(具体表达式 UI 未在探索中展开)。
+
+---
+
+## 3. 智能体管理(Agent Management)
+
+入口:左侧菜单"智能体",对应路由 `/agent-management`。
+
+## 3.1 智能体创建表单(基础信息)
+
+点击"新增智能体"后弹出对话框,包含两个 Tab:**基础信息** 与 **Agent配置**(仅当应用类型为 Agent 时显示)。
+
+### 基础信息 Tab 字段(截图:`agent-create-form.png`)
+
+| 分组 | 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- | --- |
+| 基础信息 | 智能体名称 | 文本 | 是 | 显示名称 |
+| 基础信息 | 智能体图标 | 图标选择 | 否 | 20 个内置预设图标 + 自定义上传 |
+| 基础信息 | 应用类型 | 单选 | 是 | `工作流` / `Agent`;切换后会重置后续 Tab |
+| 基础信息 | 排序号 | 数字 | 否 | 在列表中的排序权重 |
+| 基础信息 | 是否启用 | 开关 | 否 | 默认开启 |
+| 工作流(仅工作流类型显示) | 注册为子图 | 开关 | 否 | 开启后该工作流可被其他工作流通过"子图"节点调用 |
+| 分享设置 | 权限类型 | 单选 | 是 | 当前版本仅 `私有` |
+| 分享设置 | 匿名分享 | 开关 | 否 | 关闭(探索期间未开启) |
+| 内容引导 | 开场白 | 文本域 | 否 | 对话开始时显示的欢迎语 |
+| 内容引导 | 功能介绍 | 文本域 | 否 | 智能体能力说明 |
+| 内容引导 | 初始问题 | 列表 | 否 | 推荐问题列表,可多条添加 |
+
+---
+
+## 3.2 Agent 类型专属配置(Agent配置 Tab)
+
+应用类型为 `Agent` 时,"基础信息"提交后会出现 **Agent配置** Tab,包含 6 个子 Tab:
+
+### 3.2.1 运行参数(截图:`agent-config-capabilities.png`)
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| 默认模型 | 下拉选择 | 是 | 下拉列表来自"模型管理"中已配置的模型;可点击"刷新模型"重新拉取 |
+| 多模态补充 | 单选 | 否 | `跟随系统默认` / `开启` / `关闭` |
+| 默认工作路径 | 文本 | 是 | Agent 执行脚本/读写文件时的根目录 |
+| 角色提示词 | 文本域 | 是 | System Prompt,决定 Agent 的角色与行为 |
+| 对话记忆 | 开关 + 子项 | 否 | 启用后可选择"摘要模型" |
+| 长期记忆 | 开关 | 否 | 启用后跨会话保留关键信息 |
+| 子智能体 | 列表 | 否 | 可勾选"综合检索(系统内置)"或选择其他已创建的 Agent |
+
+### 3.2.2 能力 Tab
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| 工作流调用 | 开关 | 允许 Agent 调用其他工作流 |
+| 内置浏览器 | 开关 | 启用平台内置浏览器访问网页 |
+| 客户端浏览器 | 开关 | 使用客户端本地浏览器(需客户端配合) |
+| 工具检索 | 开关 | 启用后 Agent 会按需从工具池中检索并调用 |
+
+### 3.2.3 工具 Tab
+
+工具分为两类:
+
+- **内建工具(10 个)**
+  - 搜索类(5):网页搜索、知识库搜索、图片搜索、视频搜索、新闻搜索
+  - 文件类(2):文件读取、文件写入
+  - 数据库类(3):SQL 查询、NoSQL 查询、向量检索
+- **系统工具(13 个)**
+  - 来源:员工考核系统对接的工具集(人事、考勤、绩效等相关接口)
+
+### 3.2.4 技能 Tab
+
+- 技能总开关:开启后 Agent 可使用已注册的技能(Skill)。
+- 技能列表来自系统的"技能管理"模块,每个技能对应一个可独立执行的能力包。
+
+### 3.2.5 MCP Tab
+
+- 探索期间显示"暂无可绑定 MCP 服务"。MCP(Model Context Protocol)服务在系统管理中先注册后才能在此绑定。
+
+### 3.2.6 插件 Tab
+
+5 个内建插件分组:
+
+| 分组 | 插件 |
+| --- | --- |
+| 文档与 Office | Word/Excel/PPT 生成与解析 |
+| 命令执行 | Shell / Python 沙箱命令执行 |
+| 图表 | 柱状图 / 折线图 / 饼图等可视化生成 |
+| 文件 | 文件转换、压缩、解压 |
+| 基础和交互 | 时间、计算器、单位换算、提醒等 |
+
+---
+
+## 4. 工作流编排
+
+### 4.1 入口与编排器架构
+
+应用类型为"工作流"的智能体,在卡片或编辑页可进入"工作流编排"。系统提供两种编排器:
+
+| 编排器 | 路由 / URL | 实现 | 当前状态 |
+| --- | --- | --- | --- |
+| 工作流编排(默认 Tab) | `/agent-management/edit/{sceneId}/workflow-arrange` | 通过 iframe 嵌入 `/workflow-web/design/{graphId}?returnTo=...&sceneId={sceneId}`,底层为 **React Flow** | 主要使用,功能完整 |
+| 工作流编排(Vue3) | `/workflow-vue3/design/{id}` | Vue3 + Vue Flow 实现 | 部分场景报"工作流不存在",疑似与 graphId/sceneId 不同步 |
+
+> **权限**:只有创建人可以查看和编辑工作流详情;非创建人访问会得到"只有创建人可以查看工作流详情"提示。
+
+### 4.2 编排器顶层控件
+
+| 区域 | 说明 |
+| --- | --- |
+| 顶部 Tab | 对话 / 编辑 / 工作流编排 / 工作流编排(Vue3) / 接口详情 |
+| 左侧节点列表 | 支持搜索;按 6 个分类分组(详见 4.3) |
+| 中部画布 | React Flow 画布;支持撤销/重做、缩放、小地图、导入/导出配置、添加注释 |
+| 右侧节点配置面板 | 选中节点后展开,字段依节点类型而异 |
+| 底部状态栏 | "上次发布 / 上次保存" 时间戳;按钮:`保存`、`发布 vX`、`历史`、`预览`、`配置校验`(带未修复校验数角标) |
+
+### 4.3 节点类型注册表(来源:前端 `Y9e` 元数据 + 分类常量 `Z9e`)
+
+下表列出全部 26 个用户可见节点类型,按 6 类分组。`type` 列为节点在序列化配置中的唯一标识(即 React Flow `node.type`)。
+
+| 分类 | type | 显示名 | 配色 | 描述 |
+| --- | --- | --- | --- | --- |
+| 基础节点 | `llm` | 大模型 | pink | 调用大语言模型,根据提示词生成文本回复。 |
+| 基础节点 | `answer` | 回复 | purple | 将处理结果回复给用户,支持文本和文件输出。 |
+| 基础节点 | `retrieval` | 知识库检索 | orange | 从知识库中检索相关文档,支持语义和关键词混合搜索。 |
+| 基础节点 | `table_retrieval` | 表格知识库检索 | yellow | 从表格知识库中检索结构化数据,支持向量、全文和 NL2SQL 混合检索。 |
+| 基础节点 | `graph_retrieval` | 图谱知识库检索 | green | 从图谱知识库中检索实体与关系,返回子图及上下文摘要。 |
+| 基础节点 | `agent` | Agent | teal | 自主推理并调用工具完成任务的智能体节点。 |
+| 基础节点 | `subgraph` | 子图 | blue | 嵌入已发布的子工作流,实现流程复用和模块化编排。 |
+| 问题处理 | `classifier` | 意图识别 | purple | 利用大模型识别用户意图,将输入分类到预设类别。 |
+| 问题处理 | `param_ext` | 参数提取 | purple | 从用户输入中提取结构化参数,如日期、地点、数量等。 |
+| 问题处理 | `query_rewriter` | 问题改写 | purple | 结合上下文对用户问题进行改写,以提高检索准确率。 |
+| 问题处理 | `query_expansion` | 关键词提取 | purple | 从用户问题中提取关键词,用于后续检索或分析。 |
+| 逻辑 | `ifelse` | 条件分支 | orange | 根据条件表达式判断,将流程路由到不同分支。 |
+| 逻辑 | `loop` | 迭代 | orange | 对列表数据逐项或并行迭代处理,支持内嵌子图。 |
+| 工具 | `list_operation` | 列表操作 | teal | 对列表执行筛选、排序、截取和字段提取等操作。 |
+| 工具 | `http` | HTTP 请求 | teal | 发送 HTTP 请求并获取响应,支持重试和超时配置。 |
+| 工具 | `human` | 人工确认 | teal | 暂停流程等待人工审核或输入,支持多种交互方式。 |
+| 工具 | `tool` | 工具调用 | teal | 调用已注册的外部工具,按 Schema 传参并获取结果。 |
+| 工具 | `auto_db_query` | 数据自动查询 | teal | 由大模型自动生成 SQL 并查询数据库,适合自然语言提问。 |
+| 工具 | `db_query` | 数据查询 | teal | 手动编写 SQL 查询数据库,支持多任务并发执行。 |
+| 文件 | `file_download` | 文件下载 | yellow | 从指定 URL 下载文件,支持单文件和批量模式。 |
+| 文件 | `file_upload` | 文件上传 | yellow | 将文件上传到存储服务,返回访问 URL。 |
+| 文件 | `doc_ext` | 文档提取器 | yellow | 解析单个文件或文件列表,输出文件名与内容。 |
+| 文件 | `official_doc` | 公文生成 | yellow | 按标准格式生成通知、纪要等公文文档。 |
+| 转换 | `var_override` | 变量覆盖 | pink | 创建或覆盖变量的值,用于在节点间传递数据。 |
+| 转换 | `var_transform` | 变量转换 | pink | 将复杂数据结构拆分或转换为独立变量。 |
+| 转换 | `code` | 代码执行 | pink | 执行自定义 Python 或 JavaScript 代码片段。 |
+
+> 另有两个**隐藏节点** `toolretrieval`(工具检索,hidden=true)、`plugin`(插件,hidden=true),不在用户节点列表中,本文档不展开。`llmtool`(LLM 工具)作为 tool 的特殊形态,在面板序列化逻辑中出现,但未单独列入注册表。
+
+### 4.4 节点通用字段与异常处理
+
+#### 4.4.1 通用基础字段(所有节点共享)
+
+下列字段由节点渲染器统一处理,不属于各节点的 `initialData`:
+
+| 字段 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 自动生成 `${type}_${n}` | 节点唯一 ID,例如 `llm_1`、`answer_2` |
+| `type` | string | — | 节点类型 key(见 4.3) |
+| `position` | `{x, y}` | 自动布局 | 画布坐标 |
+| `data.label` | string | 默认显示名 + 序号 | 节点标题,可重命名 |
+| `data.width` | number | 节点默认 | 节点宽度(部分节点为 280) |
+| `data.height` | number | 节点默认 | 节点高度 |
+| `data.selected` | boolean | false | 是否选中 |
+| `data.join_enabled` | boolean | false | "汇聚执行":入边数 ≥ 2 时可开启,开启后等待所有上游到达再执行 |
+| `data.error_policy` | object | `{mode:"none", max_retries:0, ...}` | 异常处理策略(见 4.4.2) |
+
+#### 4.4.2 异常处理(`error_policy`)
+
+面板底部统一渲染的异常处理区块。**注意**:以下 7 类节点禁用 "默认值" 模式,仅支持 "无 / 异常分支":`file_download`、`file_upload`、`doc_ext`、`official_doc`、`var_override`、`var_transform`、`code`。
+
+| 字段 | 类型 | 取值 / 默认 | 说明 |
+| --- | --- | --- | --- |
+| `error_policy.mode` | enum | `none` / `default_value` / `error_branch` | 处理方式:异常时停止 / 走默认值 / 走异常分支 |
+| `error_policy.default_output` | any | — | mode=`default_value` 时使用的兜底输出 |
+| `error_policy.fallback_action` | enum | `stop` | mode=`error_branch` 时若异常分支也未成功,最终动作(停止) |
+| `error_policy.max_retries` | number | `0` | 最大重试次数 |
+| `error_policy.routes` | array | — | mode=`error_branch` 时定义异常分支连线 |
+| (classifier 专属)`error_policy.default_category` | string | — | 当 classifier+default_value 时,配置"默认分类" |
+
+---
+
+### 4.5 节点 schema 详情
+
+> 下文按 4.3 表格顺序逐个列出每个节点的完整字段。每节包含:
+> - **基础属性**:display / color / 描述 / 连接方向(`sources` = 出边方向,`targets` = 入边方向,`*` 表示任意方向)
+> - **initialData**:节点 `data` 中的可配置字段(来自面板序列化白名单 + `Y9e[type].initialData` 默认值)
+> - **inputVariables**:可作为子图输入的变量声明
+> - **outputVariables**:节点输出变量及其类型(部分为函数动态计算,标注 `动态`)
+>
+> 表格中"中文标签"列取自面板组件源码(`label:"..."` 模式直接匹配)。所有未列出的基础字段(`label`、`width`、`height`、`error_policy` 等)默认存在,不再重复。
+
+#### 4.5.1 大模型(`llm`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 大模型 |
+| colorScheme | pink |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `model_id` | string | `""` | 模型(必填,下拉来自模型管理;带"刷新模型列表"按钮) |
+| `model_name` | string | `""` | 模型名(前端展示用,跟随 model_id) |
+| `temperature` | number | `0.1` | 温度(范围 0.0–1.0,步进 0.1) |
+| `system_prompt` | string | `""` | 系统提示(多行,placeholder:输入系统提示,可留空) |
+| `user_prompt` | string | `"{{global.query}}"` | 用户输入(必填,模板表达式) |
+| `stream` | boolean | `true` | 流式输出 |
+| `memory_enabled` | boolean | `false` | 短期记忆(开启后展开记忆配置) |
+| `memory_type` | enum | — | 记忆类型(仅 memory_enabled=true 时显示) |
+| `memory_window` | number | `3` | 最大记忆轮数(1–100) |
+| `json_output` | boolean | `false` | JSON 输出(helpTip:开启后强制模型输出 JSON 格式) |
+| `json_output_type` | enum | `"object"` | 输出类型:`object`(Object 对象) / `array`(Array 数组)(仅 json_output=true 且 stream=false 时显示) |
+| `thinking_mode` | enum | `"default"` | 思考模式:默认(跟随前端) / 开启 / 关闭 / 忽略(不传思考参数) |
+| `thinking_level` | enum | — | 思考级别:默认 / low(轻度) / medium(标准) / high(深度) / xhigh(扩展) / max(极深) / 忽略(不传思考级别) |
+
+**outputVariables**(动态:依赖 `json_output` / `stream` / `json_output_type`)
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `response` | 模型回复 | String(默认);当 `json_output && !stream`:`json_output_type=array` → Array,否则 Object |
+
+#### 4.5.2 回复(`answer`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 回复 |
+| colorScheme | purple |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `input_schema` | string | `""` | 输出内容(模板表达式文本域,支持"插入变量" + 全屏编辑) |
+| `output_files` | array | `[]` | 文件输出(可添加;每项是文件变量引用) |
+
+**outputVariables**(动态:依赖 `output_files`)
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `response` | 回复内容 | String |
+| `files` | 输出文件 | Array[File](仅当 output_files 非空) |
+
+#### 4.5.3 知识库检索(`retrieval`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 知识库检索 |
+| colorScheme | orange |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `query` | string | `"{{global.query}}"` | 意图查询(必填,用于检索召回和重排序) |
+| `queries` | array | `[]` | 检索查询(多路召回查询条件,可不填) |
+| `kb_ids` | array | `[]` | 知识库(多选,不填则查询全部) |
+| `kb_ids_var` | string | — | 知识库变量引用(如 `{{节点.kb_ids}}`) |
+| `kb_ids_mode` | enum | `"select"` | 知识库选择模式:`select`(下拉) / `variable`(变量) |
+| `tags` | array | undefined | 标签过滤 |
+| `tags_var` | string | — | 标签变量引用 |
+| `tags_mode` | enum | `"select"` | 标签选择模式 |
+| `search_mode` | enum | `"hybrid"` | 检索方式:`hybrid`(混合检索) / `vector`(向量检索) / `keyword`(分词检索) |
+| `size` | number | `5` | 最大召回数量 |
+| `enable_reranker` | boolean | `false` | 开启重排 |
+| `knn_score_threshold` | number | `0.8` | 召回阈值(向量相似度阈值,0–1) |
+| `rerank_score_threshold` | number | `0.5` | 重排阈值 |
+| `enable_citation` | boolean | `true` | 输出引用 |
+| `return_full_file_content` | boolean | `false` | 返回文件全文 |
+| `return_to_frontend` | boolean | — | 是否直接回前端 |
+| `connect_timeout` | number | — | 连接超时(秒,高级设置) |
+| `read_timeout` | number | — | 读取超时(秒) |
+| `write_timeout` | number | — | 写入超时(秒) |
+| `retry` | object | — | 失败重试:`{enable, count, interval_ms}` |
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 | 说明 |
+| --- | --- | --- | --- |
+| `retrieval_result` | 检索结果(JSON) | Array | 详见下方 schema |
+| `formatted_result` | 格式化结果(Text) | String | |
+
+`retrieval_result` schema(每项字段):`unique_id`(本次检索唯一ID)、`page_content`(文档内容 LangChain 原始字段)、`content`(内容块完整内容)、`matched_content`(匹配的检索块内容)、`file_name`、`file_title`、`file_type`、`kb_id`(知识库ID)、`source_id`(来源ID 文件ID)、`score`(综合得分)、`recall_source`(召回来源: BM25/VECTOR/HYBRID)、`page_number`(页码)、`chunk_index`(内容块序号)、`meta`(结构化元数据 标题 条文等)、`extra_info`(元数据前缀文本)、`metadata`(LangChain 原始 metadata)。
+
+#### 4.5.4 表格知识库检索(`table_retrieval`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 表格知识库检索 |
+| colorScheme | yellow |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `query` | string | `"{{global.query}}"` | 查询问题(必填,自然语言描述要查询的数据) |
+| `kb_ids` / `kb_ids_var` / `kb_ids_mode` | — | `[]` / — / `"select"` | 同 4.5.3 |
+| `size` | number | `10` | 最大召回量(限制明细数据的返回条数,不影响统计结果) |
+| `search_mode` | enum | `"hybrid_sql"` | 检索模式:`hybrid`(混合检索) / `sql`(SQL检索) / `hybrid_sql`(混合检索+SQL检索) |
+| `knn_score_threshold` | number | `null` | 召回阈值(不设置则使用默认值) |
+| `enable_citation` | boolean | `true` | 输出引用 |
+| `connect_timeout` / `read_timeout` / `write_timeout` / `retry` | — | — | 同 4.5.3 |
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `retrieval_result` | 检索结果(JSON) | Array |
+| `formatted_result` | 格式化结果(Text) | String |
+
+`retrieval_result` schema:`unique_id`(唯一ID)、`table_name`(表名)、`table_id`(表ID)、`row_data`(行数据 Object)、`columns`(列定义 Array)、`source_route`(命中来源)。
+
+#### 4.5.5 图谱知识库检索(`graph_retrieval`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 图谱知识库检索 |
+| colorScheme | green |
+| sources / targets | `[right]` / `[left]` |
+
+> 受 feature flag `G9e` 控制,未开启时该节点从基础节点列表中过滤掉(`X9e.filter(e=>"graph_retrieval"!==e)`)。
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `query` | string | `"{{global.query}}"` | 意图查询 |
+| `kb_ids` / `kb_ids_var` / `kb_ids_mode` | — | `[]` / — / `"select"` | 知识库(图谱)选择 |
+| `tags` | string | `""` | 标签过滤(多标签逗号分隔) |
+| `tags_mode` | enum | `"select"` | 标签选择模式 |
+| `enable_citation` | boolean | `true` | 输出引用 |
+| `max_hops` | number | `4` | 查询深度(跳数) |
+| `max_edges` | number | `200` | 最大边数 |
+| `max_entities` | number | `60` | 最大实体数 |
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `retrieval_result` | 检索结果(JSON) | Array |
+| `formatted_result` | 格式化结果(Text) | String |
+
+`retrieval_result` schema:`unique_id`(唯一ID)、`file_name`(来源文件)、`kb_name`(知识库)、`entities`(实体列表 Array)、`relations`(关系列表 Array)。
+
+#### 4.5.6 Agent(`agent`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | Agent |
+| colorScheme | teal |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `model_id` | string | `""` | 模型(必填) |
+| `system_prompt` | string | `""` | 系统提示 / 系统提示词 |
+| `user_prompt` | string | `"{{global.query}}"` | 用户输入 |
+| `stream` | boolean | `true` | 流式输出(helpTip:开启后 Agent 的回复将实时流式返回) |
+| `reply_result` | boolean | `true` | 回复结果 |
+| `agent_type` | enum | — | Agent 类型 |
+| `thinking_mode` | enum | `"default"` | 思考模式 |
+| `thinking_level` | enum | — | 思考级别 |
+| `multimodal_supplement_enabled` | boolean\|null | `null` | 多模态补充 |
+| `multimodal_supplement_model_id` | number | `-1` | 多模态补充模型(默认 -1 = 跟随主 Agent) |
+| `multimodal_supplement_image_model_id` | number | `-1` | 多模态图片模型 |
+| `multimodal_supplement_audio_model_id` | number | `-1` | 多模态音频模型 |
+| `multimodal_supplement_video_model_id` | number | `-1` | 多模态视频模型 |
+| `tools` | array | — | 工具(多选) |
+| `mcp_server_codes` | array | `[]` | MCP 服务 |
+| `plugin_package_ids` | array | `[]` | 插件包 |
+| `enable_skills` | boolean | `false` | Skill 模式(开启后展开"绑定技能") |
+| `skill_names` | array | `[]` | 绑定技能(未绑定时使用所有有权限的技能) |
+| `browser_usage_enabled` | boolean | `false` | 浏览器使用(beta) |
+| `user_memory_enabled` | boolean | `false` | 长期记忆 |
+| `conversation_memory_enabled` | boolean | `false` | 对话记忆 |
+| `conversation_memory_summary_model_id` | string | — | 摘要模型 |
+| `subagents` | array | `[]` | 子智能体 |
+| `system_subagents` | array | 见下 | 系统内置子智能体(默认含两条系统模板,enabled=false) |
+| `subagents_enabled` | boolean | `true` | 子智能体清单 |
+
+`system_subagents` 默认值:
+
+```js
+[
+  { template_id: "sys.retrieval_grounder",       enabled: false },
+  { template_id: "sys.jizhi_system_steward",     enabled: false }
+]
+```
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `response` | Agent回复 | String |
+
+#### 4.5.7 子图(`subgraph`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 子图 |
+| colorScheme | blue |
+| sources / targets | `[right]` / `[left]` |
+| inputVariables | `["Input"]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `subgraph_id` | string | `""` | 子工作流 ID |
+| `subgraph_name` | string | `""` | 子工作流名称 |
+| `subgraph_wf_id` | string | `""` | 子工作流 wf_id |
+| `input` | string | `""` | 子图输入(映射到子图开始节点) |
+| `input_params` | object | `{}` | 输入参数映射 |
+| `user_files` | string | `""` | 用户文件传递 |
+| `_startNodeInputs` | array | — | (运行时)子图开始节点输入声明 |
+| `_publishedOutputVariables` | array | — | (运行时)子图已发布输出变量 |
+| `_publishedOutputVariablesLoaded` | boolean | — | (运行时)加载标志 |
+| `_endNodeOutputs` | array | — | (运行时)子图结束节点输出 |
+
+**outputVariables**(动态:优先取 `_startNodeInputs` + `_publishedOutputVariables`,否则取 `_endNodeOutputs`,最后 fallback 为 `response:子图输出`)
+
+#### 4.5.8 意图识别(`classifier`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 意图识别 |
+| colorScheme | purple |
+| sources / targets | `[*]` / `[input]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `input_schema` | string | `"{{global.query}}"` | 输入(待分类文本) |
+| `model_id` / `model_name` | string | `""` | 模型(带刷新按钮) |
+| `categories` | array | 见下 | 分类列表(每项 `{category_id, category_name}`,支持"添加分类") |
+| `memory_enabled` | boolean | `false` | 短期记忆 |
+| `memory_type` | enum | — | 记忆类型 |
+| `memory_window` | number | `3` | 最大记忆轮数 |
+| `thinking_mode` | enum | `"default"` | 思考模式 |
+| `thinking_level` | enum | — | 思考级别 |
+| `system_prompt` | string | `""` | 系统提示词(提示词配置,可"恢复默认") |
+| `user_prompt` | string | `""` | 用户提示词 |
+
+`categories` 默认值:
+
+```js
+[
+  { category_id: "category_1",       category_name: "分类1" },
+  { category_id: "others_category",  category_name: "其他情况" }
+]
+```
+
+**outputVariables**(动态:当 error_policy.mode=default_value 时追加两个异常输出)
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `class_name` | 识别结果 | String |
+| `all_classes` | 分类列表 | Array[String] |
+| `default_value` | 异常默认值 | Any(仅 default_value 模式) |
+| `raw_output` | 异常默认原始值 | String(仅 default_value 模式) |
+
+#### 4.5.9 参数提取(`param_ext`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 参数提取 |
+| colorScheme | purple |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `instruction` | string | `""` | 指令(输入提取指令) |
+| `input_schema` | string | `""` | 输入内容 (Input Schema,待提取文本) |
+| `model_id` / `model_name` | string | `""` | 模型 |
+| `parameters` | array | `[]` | 参数列表(详见下方) |
+| `memory_enabled` / `memory_type` / `memory_window` | — | — | 同 4.5.8 |
+| `thinking_mode` / `thinking_level` | — | — | 思考模式 / 级别 |
+| `system_prompt` | string | `""` | 系统提示词 |
+| `user_prompt` | string | `""` | 用户提示词 |
+
+`parameters` 数组每项结构为 `{ [paramName]: { type, description, required, ... } }`,参数 schema 编辑器支持的字段:
+
+| 子字段 | 说明 |
+| --- | --- |
+| 参数名称 | key(如 `user_name`) |
+| 中文名 | label |
+| 类型 | `String` / `Number` / `Array` / `Boolean` / `Object` 等 |
+| 数组元素类型 | 当 type=Array 时的元素类型 |
+| 描述 | description |
+| 必填 | required |
+
+**outputVariables**(动态:根据 `parameters` 列表生成)
+
+每个 parameter 生成一个输出变量:`{name=paramName, label=description||paramName, type=映射后的类型}`。
+类型映射:`str → String`、`number → Number`,其他保持。
+
+#### 4.5.10 问题改写(`query_rewriter`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 问题改写 |
+| colorScheme | purple |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `model_id` / `model_name` | string | `""` | 模型 |
+| `input_schema` | string | `"{{global.query}}"` | 输入(待改写内容) |
+| `stream` | boolean | `false` | 流式输出 |
+| `memory_enabled` / `memory_type` / `memory_window` | — | — | 短期记忆 |
+| `thinking_mode` / `thinking_level` | — | — | 思考模式 / 级别 |
+| `system_prompt` | string | `""` | 系统提示词 |
+| `user_prompt` | string | `""` | 用户提示词 |
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `output` | 改写结果 | String |
+
+#### 4.5.11 关键词提取(`query_expansion`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 关键词提取 |
+| colorScheme | purple |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**:与 `query_rewriter` 完全一致(同字段集合,不同语义)。
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `output` | 提取结果 | String |
+
+#### 4.5.12 条件分支(`ifelse`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 条件分支 |
+| colorScheme | orange |
+| sources / targets | `[]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `cases` | array | `[]` | 分支条件列表(详见下方) |
+
+`cases` 每个分支结构:
+
+```ts
+{
+  case_id: string,       // 分支 ID
+  label: string,         // 分支名(默认"分支 N")
+  conditions: Array<{    // 条件列表(AND 关系)
+    variable: string,    // 比较变量(来自上游变量选择)
+    operator: enum,      // 操作符:=, ≠, >, <, ≥, ≤, 包含, 不包含, 开始于, 结束于, 为空, 不为空
+    value: string        // 比较值
+  }>,
+  logical_operator: "AND" | "OR"  // 多条件间的逻辑关系
+}
+```
+
+UI 操作:每分支一个出口连线;"添加分支"按钮新增 case;"添加条件"在分支内新增 condition。
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `result` | 判断结果 | String |
+
+> 画布上每个分支还会产生一个独立的输出把手(handle),通过 `error_policy.routes`/`cases` 路由。
+
+#### 4.5.13 迭代(`loop`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 迭代 |
+| colorScheme | orange |
+| sources / targets | `[right]` / `[left]` |
+| inputVariables | `["Input"]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `input_variable` | string | `""` | 输入列表变量(必填,选择要迭代的列表变量) |
+| `input_variables` | array | `[]` | 多输入列表 |
+| `is_parallel` | boolean | `false` | 并发运行 |
+| `concurrency` | number | — | 并发度(并行执行的线程数量 1–16) |
+| `outputs` | array | `[]` | 输出变量配置(详见下方) |
+| `subgraph_config` | object | — | 内嵌子图配置(包含 `nodes`/`edges` 等子图完整定义) |
+
+`outputs` 每项:
+
+```ts
+{
+  name: string,    // 变量名
+  label: string,   // 中文名
+  type: string     // 类型(如 String、Number、Array[Object] 等)
+}
+```
+
+UI 提供"添加输出"按钮配置 outputs;提示文案:"下游节点可通过变量引用迭代内部节点的输出(聚合为数组)"。
+
+**outputVariables**(动态:基于 `outputs` + 内部子图节点输出聚合)
+
+至少包含:
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `response` | 循环响应 | String |
+| `results` | 循环结果 | Array |
+
+随后追加每个 `outputs` 项(type 自动包装为 `Array[...]`)+ 内部节点输出(聚合为 `Array`,name 形如 `${nodeId}.${varName}`)。
+
+#### 4.5.14 列表操作(`list_operation`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 列表操作 |
+| colorScheme | teal |
+| defaultWidth | 280 |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `input_variable` | string | `""` | 输入变量(要操作的列表) |
+| `input_variables` | array | `[]` | 多输入列表 |
+| `input_type` | string | `""` | 输入类型(运行时根据上游推导,如 `Array[File]`、`Array[Object]`) |
+| `filter_type` | enum | `"all"` | 过滤匹配模式:`all`(全部满足) / `any`(任一满足) |
+| `enable_filter` | boolean | `false` | 启用过滤 |
+| `conditions` | array | `[]` | 过滤条件列表(结构同 ifelse.conditions) |
+| `enable_sort` | boolean | `false` | 启用排序 |
+| `sort_by` | string | `""` | 排序字段 |
+| `sort_order` | enum | `"asc"` | 排序方向:`asc` / `desc` |
+| `enable_limit` | boolean | `false` | 启用截取 |
+| `limit` | number | `0` | 截取数量 |
+| `limit_type` | enum | `"top_n"` | 截取模式:`top_n`(前 N 条) / `bottom_n`(后 N 条) |
+| `enable_extract` | boolean | `false` | 启用字段提取 |
+| `extract_field` | string | `""` | 提取字段(单字段模式) |
+| `extract_fields` | array | `[]` | 提取字段列表(每项 `{field, type, label}`) |
+
+**outputVariables**(动态:依赖 `extract_fields` / `extract_field` / `input_type`)
+
+基础输出(始终存在):
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `result` | 过滤结果 | Array(type 与 extract 配置相关) |
+| `first_record` | 第一条记录 | Any / Object |
+| `last_record` | 最后一条记录 | Any / Object |
+
+当 `input_type` 为 `Array[File]`/`File[]` 且未启用 extract 时,追加:
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `result_info` | 过滤结果信息 | Array[FileInfo] |
+| `first_record_info` | 第一条记录信息 | FileInfo |
+| `last_record_info` | 最后一条记录信息 | FileInfo |
+
+#### 4.5.15 HTTP 请求(`http`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | HTTP 请求 |
+| colorScheme | teal |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `url` | string | `""` | API 地址 |
+| `method` | enum | `"GET"` | 请求方法:`GET` / `POST` / `PUT` / `DELETE` / `PATCH` / `HEAD` |
+| `headers` | object | `{}` | 请求头(键值表) |
+| `params` | object | `{}` | 请求参数(query 键值表) |
+| `body` | string | `""` | 请求体(按 body_type 切换形态) |
+| `body_type` | enum | `"none"` | 请求体类型:`none` / `form-data` / `JSON` / `raw` / `binary` |
+| `body_form_data` | array | — | body_type=form-data 时的键值表(含 File 类型支持) |
+| `body_urlencoded` | object | — | body_type=urlencoded 时的键值表 |
+| `body_json` | object | — | body_type=JSON 时的对象(placeholder: `{ 'key': 'value' }`) |
+| `body_raw` | string | — | body_type=raw 时的原始内容 |
+| `body_binary` | string | — | body_type=binary 时的文件变量引用 |
+| `http2` | boolean | — | 协议:`HTTP/1.1` / `HTTP/2` |
+| `ssl_verify` | boolean | `true` | SSL 验证 |
+| `timeout_connect` | number | `5` | 连接超时(秒) |
+| `timeout_read` | number | `30` | 读取超时(秒) |
+| `timeout_write` | number | `30` | 写入超时(秒) |
+| `retry` | object | `{enable:false, count:3, interval:1000}` | 失败重试配置(启用重试 / 重试次数 / 重试间隔 ms) |
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `status_code` | 状态码 | Number |
+| `body` | 响应体 | String |
+| `headers` | 响应头 | Object |
+| `files` | 文件列表 | Array[File] |
+| `files_info` | 文件信息列表 | Array[FileInfo] |
+
+#### 4.5.16 人工确认(`human`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 人工确认 |
+| colorScheme | teal |
+| sources / targets | `[*]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `interaction_type` | enum | `"context_input"` | 交互类型:`context_input`(上下文输入) / `external_system`(外部系统处理) |
+| `title` | string | `""` | 标题(如:需要用户确认订单信息) |
+| `reason` | string | `""` | 原因 (reason),向用户说明需要介入的原因 |
+| `question` | string | `""` | 问题 (question),向用户提出的具体问题 |
+| `questions` | array | `[]` | 结构化问题(每项详见下方;空数组时执行页显示自由输入框) |
+| `output_variables` | array | `[]` | 输出变量配置(每项 `{name, label, type, scope}`) |
+| `external_system` | object | — | 外部系统配置(系统名称、系统地址) |
+| `routes` | array | `[]` | 输出分支(如:已完成 / 未完成) |
+
+`questions` 每项结构:
+
+```ts
+{
+  id: string,              // 兼容 ID(可选)
+  tab_title: string,       // Tab 标题
+  content: string,         // 问题内容
+  options?: Array<{        // 预置选项(空时该问题仅展示补充说明输入)
+    name: string,          // 选项名称
+    description: string,   // 选项说明
+  }>,
+  allow_multiple: boolean, // 允许多选
+  allow_supplement: boolean // 允许补充
+}
+```
+
+**outputVariables**(动态:依赖 `interaction_type` / `questions` / `output_variables`)
+
+基础输出(始终存在):
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `action` | 操作类型 | String |
+| `data` | 操作数据 | Any |
+
+`interaction_type=context_input` 且 `questions.length>0` 时追加:
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `response` | 人工回复 | String |
+| `answers` | 结构化答案 | Object |
+| `supplements` | 补充说明 | Object |
+
+`interaction_type=external_system` 时追加 `result`(处理结果, String)。否则追加 `output_variables` 配置的每个变量(type=String)。
+
+#### 4.5.17 工具调用(`tool`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 工具调用 |
+| colorScheme | teal |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `tool_id` | string | `""` | 工具 ID(选择工具,搜索框:名称/ID/分类/描述) |
+| `tool_title` | string | `""` | 工具中文名(跟随 tool_id) |
+| `inputs` | object | `{}` | 工具入参(按工具 input schema 动态生成的表单) |
+| `inputs_mode` | enum | `"form"` | 输入模式:`form`(表单) / 其他 |
+| `output_schema` | object\|string | — | 工具输出 schema(来自工具定义,JSON Schema) |
+| `input_schema` | object\|string | — | 工具输入 schema(用于推导文件类型入参) |
+| `thinking_mode` | enum | `"default"` | 思考模式 |
+| `thinking_level` | enum | — | 思考级别 |
+| `llm_params_enabled` | boolean | `false` | LLM 参数模式开关 |
+| `llm_model_id` | string | `""` | LLM 模型(llm_params_enabled=true 时) |
+| `llm_prompt` | string | `""` | LLM 提示词 |
+| `llm_retry_max` | number | `3` | LLM 最大重试 |
+
+**outputVariables**(动态:基于 `output_schema` JSON Schema 递归生成,深度上限 5)
+
+默认 fallback:`[{name:"result", label:"执行结果", type:"String"}]`。
+
+支持识别 file / array[file] / object / array[object] 类型,自动展开 `result` + `file_info` / `files` + `files_info` 等配套输出。
+
+#### 4.5.18 数据自动查询(`auto_db_query`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 数据自动查询 |
+| colorScheme | teal |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `datasource_code` | string | `""` | 数据源(必填,下拉来自 `getDatasourceList()`) |
+| `selected_tables` | array | `[]` | 表选择(多选) |
+| `execute_until` | enum | `"schema_only"` | 执行到:`schema_only`(仅生成 schema) / `generate_sql`(生成 SQL) / `execute_all`(执行到底) |
+| `step_generate_sql` | boolean | `false` | 分步:生成 SQL |
+| `step_execute_query` | boolean | `false` | 分步:执行查询 |
+| `model_id` | string | `""` | 模型 |
+| `system_prompt` | string | `""` | 系统提示 |
+| `user_prompt` | string | `""` | 用户输入 |
+| `user_requirements` | string | `""` | 自然语言问题(用户的查询需求) |
+| `max_rows` | number | `0` | 最大返回行数(0=不限制) |
+| `timeout` | number | `0` | 超时(秒,0=不限制) |
+| `thinking_mode` | enum | `"default"` | 思考模式 |
+| `thinking_level` | enum | — | 思考级别 |
+
+**outputVariables**(动态:依赖 `execute_until` / `step_generate_sql` / `step_execute_query`)
+
+| 变量 | 标签 | 类型 | 出现条件 |
+| --- | --- | --- | --- |
+| `table_schemas` | 表结构信息 | Array[Object] | 始终 |
+| `generated_sql` | 生成的SQL | Object(schema: `{sql, explanation}`) | step_generate_sql 或 execute_until ∈ {generate_sql, execute_all} |
+| `query_result` | 查询结果 | Object(schema 见下) | step_execute_query 或 execute_until=execute_all |
+
+`query_result` schema:`columns`(列信息 Array[{name, label, type}])、`rows`(数据行 Array[Object])、`total`(总行数 Number)、`executionTime`(执行耗时 Number)。
+
+`table_schemas` 元素 schema:`tableName`/`table_name`(表名)、`tableComment`/`comment`(表注释)、`columns`(列信息 Array[{name, columnName, type, dataType, nullable, ableBeNull, comment, columnComment}])。
+
+#### 4.5.19 数据查询(`db_query`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 数据查询 |
+| colorScheme | teal |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `datasource_code` | string | `""` | 数据源(必填) |
+| `tasks` | array | `[]` | 任务列表(详见下方) |
+| `concurrent` | boolean | `false` | 并发执行 |
+| `concurrency_limit` | number | `1` | 并发任务数上限 |
+| `timeout` | number | `0` | 超时(秒) |
+
+`tasks` 每项结构:
+
+```ts
+{
+  name: string,            // 任务名
+  sql: string,             // SQL 文本
+  output_variable: string  // 输出变量名(默认 task_${name})
+}
+```
+
+**outputVariables**(动态:每个 task 生成一个输出)
+
+每任务输出:
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `${output_variable}` | 任务结果: `${name}` | Object |
+
+每输出 schema:`columns`(列信息 Array[{name, type, label}])、`rows`(数据行 Array[Object])、`total`(总行数)、`executionTime`(执行耗时)、`error_message`(错误说明)。
+
+#### 4.5.20 文件下载(`file_download`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 文件下载 |
+| colorScheme | yellow |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `download_urls` | string | `""` | 下载链接(必填,多行,支持变量如 `{{node_id.url}}`) |
+| `output_mode` | enum | `"single"` | 输出模式:`single`(单个文件) / `multiple`(文件数组) |
+| `timeout_connect` | number | `30` | 连接超时(秒) |
+| `timeout_read` | number | `30` | 读取超时(秒) |
+| `timeout_write` | number | `30` | 写入超时(秒) |
+
+**outputVariables**(动态:依赖 `output_mode`)
+
+| 模式 | 变量 | 标签 | 类型 |
+| --- | --- | --- | --- |
+| single | `file` | 文件 | File |
+| single | `file_info` | 文件信息 | FileInfo |
+| multiple | `files` | 文件数组 | Array[File] |
+| multiple | `files_info` | 文件信息数组 | Array[FileInfo] |
+
+`FileInfo` schema:`url`、`fileName`、`filePath`、`size`、`mimeType`、`extension`。
+
+#### 4.5.21 文件上传(`file_upload`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 文件上传 |
+| colorScheme | yellow |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `files_input` | string | `""` | 文件输入(必填,选择文件或文件列表变量) |
+| `upload_mode` | enum | `"single"` | 上传模式:`single`(单文件) / `batch`(批量) |
+| `timeout` | number | `60` | 超时时间(秒) |
+
+**outputVariables**(动态:依赖 `upload_mode`)
+
+| 模式 | 变量 | 标签 | 类型 |
+| --- | --- | --- | --- |
+| single | `url` | 文件URL | String |
+| single | `file_info` | 文件信息 | FileInfo |
+| single | `result` | 上传结果 | Object |
+| batch | `urls` | URL数组 | Array[String] |
+| batch | `files_info` | 文件信息数组 | Array[FileInfo] |
+| batch | `results` | 上传结果 | Array[Object] |
+
+#### 4.5.22 文档提取器(`doc_ext`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 文档提取器 |
+| colorScheme | yellow |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `input_file` | string | `""` | 输入文件(必填,选择文件或文件列表变量) |
+| `prefer_html_table` | boolean | `true` | 偏好HTML表格 |
+| `input_type` | string | `""` | 输入类型(运行时推导:单文件 / Array[File] 等) |
+| `connect_timeout` | number | — | 连接超时(秒) |
+| `read_timeout` | number | — | 读取超时(秒) |
+| `write_timeout` | number | — | 写入超时(秒) |
+
+**outputVariables**(动态:依赖 `input_type`)
+
+| 输入类型 | 输出 |
+| --- | --- |
+| 单文件 | `file_name`(文件名 String)、`file_content`(文件内容 String)、`error`(错误信息 String) |
+| Array[File] | `result`(解析结果 Array[Object],schema 同单文件三字段) |
+
+#### 4.5.23 公文生成(`official_doc`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 公文生成 |
+| colorScheme | yellow |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `documentType` | enum | `"tongzhi"` | 文种类型(详见下方枚举) |
+| `category` | enum | `"standard"` | 公文分类:`standard`(标准文 下行文) / `upstream`(上行文) / `other`(其他) |
+| `issueOrg` | string | `""` | 发文机关名称(必填) |
+| `issueNumber` | string | `""` | 发文字号 |
+| `copyNumber` | string | `""` | 份号(可选) |
+| `secretLevel` | enum | `""` | 密级:`""`/`secret`(秘密)/`confidential`(机密)/`top_secret`(绝密) |
+| `secretPeriod` | string | `""` | 保密期限 |
+| `urgencyLevel` | enum | `""` | 紧急程度:`""`/`urgent`(加急)/`very_urgent`(特急) |
+| `signers` | string | `""` | 签发人(多人逗号分隔) |
+| `title` | string | `""` | 标题(必填) |
+| `mainRecipient` | string | `""` | 主送机关 |
+| `content` | string | `""` | 正文内容(必填,支持变量引用) |
+| `attachments` | string | `""` | 附件名称(多个附件逗号分隔) |
+| `issueOrgName` | string | `""` | 发文机关署名 |
+| `issueDate` | string | `""` | 成文日期(如:2024年1月1日) |
+| `attendees` | string | `""` | 出席人员 |
+| `observers` | string | `""` | 列席人员(可选) |
+| `ccOrgs` | string | `""` | 抄送机关 |
+| `printOrg` | string | `""` | 印发机关 |
+| `printDate` | string | `""` | 印发日期 |
+| `showIssueNumber` | boolean | `true` | 显示发文字号 |
+| `showMainRecipient` | boolean | `true` | 显示主送机关 |
+| `showSigners` | boolean | `true` | 显示签发人 |
+| `showIssueOrgName` | boolean | `true` | 显示发文机关署名 |
+| `showIssueDate` | boolean | `true` | 显示成文日期 |
+| `showAttendees` | boolean | `true` | 显示出席人员 |
+
+`documentType` 枚举(15 种):`tongzhi`(通知)、`tongbao`(通报)、`baogao`(报告)、`qingshi`(请示)、`pifu`(批复)、`yijian`(意见)、`jueyi`(决议)、`jueding`(决定)、`gongbao`(公报)、`gonggao`(公告)、`tonggao`(通告)、`mingling`(命令/令)、`yian`(议案)、`jiyao`(纪要)、`gonghan`(函)。
+
+UI 分组:版头信息、落款信息、纪要专用(仅纪要文种显示)、版记信息。
+
+**outputVariables**
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `files` | 文件数组 | Array[File] |
+| `files_info` | 文件信息数组 | Array[FileInfo] |
+
+#### 4.5.24 变量覆盖(`var_override`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 变量覆盖 |
+| colorScheme | pink |
+| defaultWidth | 280 |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `variables` | array | `[]` | 变量配置列表(详见下方;支持"添加变量") |
+| `width` | number | `280` | 节点宽度 |
+
+`variables` 每项结构:
+
+```ts
+{
+  target: string,       // 变量名 (Key)
+  label: string,        // 变量名称(展示用)
+  type: string,         // 变量类型:String / Number / Boolean / Object / Array / File / FileInfo 等
+  value: any,           // 变量值(支持变量引用 {{...}})
+  required: boolean,    // 必填
+  options: string[],    // 下拉选项(type=enum 时)
+  allowed_types: string[] // 允许的文件类型(type=File 时)
+}
+```
+
+**outputVariables**(动态:根据 `variables` 列表生成)
+
+每个 `variables` 项生成一个输出变量 `{name=target, label=label||target, type}`。
+当 type 为 `File` / `Array[File]` / `FileInfo` / `Array[FileInfo]` 时,额外追加 `${target}_info` 配套 FileInfo 输出。
+
+#### 4.5.25 变量转换(`var_transform`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 变量转换 |
+| colorScheme | pink |
+| defaultWidth | 280 |
+| sources / targets | `[right]` / `[left]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `input_variable` | string | `""` | 输入变量(选择要展开的变量 Object 或 Array[Object]) |
+| `input_type` | string | `""` | 输入类型(运行时推导) |
+| `input_schema` | array | `[]` | 输入 schema(来自上游,每项 `{name, label, type}`) |
+| `outputs` | array | `[]` | 输出变量结构(详见下方) |
+| `flatten` | boolean | `false` | 平铺变量 |
+| `width` | number | `280` | 节点宽度 |
+
+`outputs` 每项:
+
+```ts
+{
+  name: string,    // 字段名
+  label: string,   // 中文名
+  type: string     // 类型(可空,未指定时从 input_schema 推导)
+}
+```
+
+UI 提供"添加"按钮新增输出,"选择字段"从上游 schema 拉取。
+
+**outputVariables**(动态:依赖 `flatten` / `input_type` / `outputs`)
+
+- `flatten=true`:直接平铺每个 output 为独立变量(type 自动包装为 `Array[...]`,当 input_type 是 Array 时)
+- `flatten=false` 且 input_type 以 `Array` 开头:单个 `result`(展开结果, Array[Object]) 输出,schema 为 outputs 映射
+- `flatten=false` 且 input_type 为 Object:每个 output 一对一输出(File 系追加 `_info` 配套)
+
+#### 4.5.26 代码执行(`code`)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 代码执行 |
+| colorScheme | pink |
+| sources / targets | `[left, right]` / `[left, right]` |
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 中文标签 / 说明 |
+| --- | --- | --- | --- |
+| `language` | enum | `"python3"` | 语言:`python3`(Python) / `javascript`(JavaScript) |
+| `code` | string | `""` | 代码正文(多行编辑器) |
+| `args` | array | `[]` | 入参列表(每项 `{name, type}`,作为函数参数注入) |
+| `outputs` | array | `[{name:"result", type:"String"}]` | 输出变量声明(每项 `{name, type, label?}`) |
+
+**outputVariables**(动态:基于 `outputs`;若空则 fallback 为 `[{name:"result", label:"result", type:"String"}]`)
+
+每个 `outputs` 项生成 `{name, label=label||name, type}` 输出。
+
+#### 4.5.27 开始节点(`start`,系统节点)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 开始 |
+| colorScheme | green |
+| sources / targets | `[right]` / `[]`(无入边) |
+
+不可删除,每张图有且仅有一个。无 `initialData`,但内置 `inputs` 数组(用户可在"基础信息 → 内容引导"配置额外的输入字段)。
+
+**outputVariables**(系统注入的"全局变量")
+
+| 变量 | 标签 | 类型 |
+| --- | --- | --- |
+| `query` | 用户问题 | String |
+| `token` | 用户Token | String |
+| `retrieval_counter` | 检索计数器 | Integer |
+| `files` | 用户文件 | Array[File](schema: file_name, url, file_path, mime_type, extension, size) |
+| `files_info` | 用户文件信息 | Array[FileInfo](schema: url, fileName, filePath, size, mimeType, extension) |
+| `user_id` | 用户ID | String |
+| `dept_id` | 部门ID | String |
+| `tenant_id` | 租户ID | String |
+
+> 模板表达式中以 `{{global.query}}`、`{{global.files}}` 等方式引用。
+
+#### 4.5.28 结束节点(`end`,系统节点)
+
+| 属性 | 值 |
+| --- | --- |
+| display | 结束 |
+| colorScheme | red |
+| sources / targets | `[]`(无出边) / `[left]` |
+
+不可删除,每张图有且仅有一个。无 `initialData` 默认字段,画布上默认显示"输出全部变量"。
+
+**initialData**
+
+| 字段 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| `output_variables` | array | `[]` | 显式声明输出;空数组 = 输出全部上游变量。每项 `{nodeId, name, label, type}` |
+
+**outputVariables**:`[]`(结束节点不向下游输出,是流程终点)。
+
+---
+
+## 5. 关键 URL / 路由速查
+
+| 用途 | URL |
+| --- | --- |
+| 登录 | `/login` |
+| 自动化列表 | `/automation` |
+| 智能体列表 | `/agent-management` |
+| 智能体编辑(含 Tab) | `/agent-management/edit/{sceneId}/{tab?}`,`tab` 取值:`dialog`(对话) / `edit`(编辑) / `workflow-arrange`(工作流编排) / `interface`(接口详情) |
+| 工作流编排 iframe | `/workflow-web/design/{graphId}?returnTo=...&sceneId={sceneId}` |
+| Vue3 工作流编排 | `/workflow-vue3/design/{id}` |
+
+> `{sceneId}` 是智能体 ID;`{graphId}` 是工作流图 ID,二者不一定相同(一个智能体可对应一个发布图与多个历史版本图)。
+
+---
+
+## 6. 探索方法与已知限制
+
+### 6.1 节点 schema 数据来源
+
+本文档 4.3 / 4.4 / 4.5 各节中的字段表来自 **前端 JS bundle 的静态分析**,并非 UI 截图推断:
+
+1. 主 bundle `/workflow-web/assets/index-21045ea8.js`(~631KB)只是引导代码。
+2. 真正的应用代码在按需加载的 chunk `/workflow-web/assets/index-2e3277ca.js`(~7.4MB,含 11657 个中文字符)中。
+3. 在该 chunk 中可直接定位到 **节点元数据注册表 `Y9e`**(起始位置约 7213314,结束于约 7243000),其中按节点 type 顺序声明了每个节点的:
+   - `display`(中文名)、`colorScheme`、`description`、`allowedConnections`(sources/targets)
+   - `initialData`:默认配置对象(本文 4.5 各节"initialData"表的字段名与默认值即取自此处)
+   - `inputVariables` / `outputVariables`:输入输出变量声明(部分为函数动态计算,函数源码完整保留在 bundle 中)
+   - `properties`:字段校验器引用
+4. 节点分类常量 `Z9e` 紧随其后(位置约 7243700),与 UI 左侧节点列表的 6 个分类完全一致。
+5. 节点配置面板的中文表单标签(如"短期记忆""流式输出""输出引用"等)从各面板组件的 `label:"..."` JSX 属性中抽取,与 4.5 各节"中文标签"列对应。
+
+> 因此 4.5 中所有字段名、默认值、输出变量 schema 均为**权威数据**,可直接用于后端 schema 建模。
+
+### 6.2 已知限制
+
+1. **业务接口需要密钥协商**:直接 `fetch('/jizhiApi/...')` 会返回 `{"msg":"当前请求会话中不存在加密秘钥","code":521}`;如需通过 API 获取已有工作流的节点配置作为参考,需要先在前端 SDK 完成 `cryptKeys` / `cryptConf` 协商,或在浏览器控制台中复用前端已发出的请求。
+2. **Vue3 编排器对部分工作流报"工作流不存在"**:用新建工作流的 ID 进入 Vue3 编排器会失败,需使用默认的"工作流编排"Tab(React Flow)。
+3. **拖拽添加节点不稳定**:在 headless 浏览器中通过 `page.mouse` / `frame.dragTo` 触发 React Flow 的 HTML5 DnD 事件不易成功(多种合成事件方案均未生效)。但本次通过 JS bundle 静态分析已绕开此限制。
+4. **图谱检索节点的 feature flag**:`graph_retrieval` 节点受常量 `G9e` 控制;若生产环境未开启,该节点会从基础节点列表中过滤掉(`X9e.filter(e=>"graph_retrieval"!==e)`),但 `Y9e` 注册表中仍保留其完整定义。
+5. **测试残留**:为获取完整编辑权限,本次探索在平台创建过一个名为 `node-fields-exploration` 的工作流(智能体 ID `7782`),探索完成后已按 `/agent-management → 我的 → 工作流筛选 → 卡片"更多管理" → 删除智能体` 流程清理完毕(删除后"我的"工作流列表显示"暂无智能体")。
+
+---
+
+## 7. 与本仓库(agent-management)的对照建议
+
+下表把平台能力映射到本仓库后端可能的实现模块,便于后续做工作流引擎与节点 schema 时对照:
+
+| 平台能力 | 建议映射到本仓库的位置 |
+| --- | --- |
+| 智能体基础信息 + Agent 配置(运行参数 / 能力 / 工具 / 技能 / MCP / 插件) | `agent` 实体 + 配套的 `agent_capability` / `agent_tool_binding` 等关联表 |
+| 自动化任务(cron / 间隔) | `automation_task` 表 + 调度器(Quartz / Spring Scheduling) |
+| 工作流编排(节点 + 边 + 配置) | `workflow_graph` 表(存储 React Flow 序列化结果)+ 节点执行器(Strategy / Chain 模式) |
+| 节点配置 schema | 每个节点类型对应一个 `NodeHandler` 实现,定义输入 / 输出变量声明与执行逻辑(参考 4.5 / 4.6 的字段表) |
+| 节点异常处理 | `NodeExceptionConfig`(处理方式、最大重试次数),所有节点 Handler 共享 |
+| 工作流版本管理 | `workflow_graph_version`(每次发布生成一份快照,运行时按版本号加载) |
+| 子图 | `workflow_subgraph_binding`:节点 ID → 子工作流版本 ID |
+
+> 本仓库中"错误模式管理"相关功能(已扩展为 25 条内置模式并支持模糊匹配)对应到平台"其他管理 → 错误模式"页面,二者命名一致,可直接对齐。

+ 32 - 0
frontend/src/api/workflow.js

@@ -49,6 +49,38 @@ export function runWorkflow(nameOrId, inputs, files = [], paths = []) {
   })
 }
 
+/**
+ * v1.1:断线重连入口 —— 按 runId 重新订阅 SSE。
+ * 客户端 reader.read() 抛错时记录 lastEventId 后通过此端点续接。
+ * 返回原生 fetch Response,需自行解析 SSE 流(与 runWorkflow 一致)。
+ *
+ * @param {string} runId       工作流运行 ID
+ * @param {number} lastEventId 上次收到的最大事件 id(来自 SSE event.id 字段)
+ */
+export function reconnectRunStream(runId, lastEventId) {
+  const url = `/api/workflows/runs/${runId}/stream`
+    + (lastEventId != null ? `?lastEventId=${lastEventId}` : '')
+  return fetch(url)
+}
+
+/**
+ * v1.1:查询运行状态 + 当前未答暂停点。
+ * 用户离开几小时后回来时,前端先调此端点:
+ * 若 status=RUNNING 且 currentPause 非空,立即恢复 Askfor UI;
+ * 然后再调 reconnectRunStream 续接 SSE。
+ */
+export function getRunState(runId) {
+  return request.get(`/workflows/runs/${runId}/state`)
+}
+
+/**
+ * v1.1:提交用户对 clarify_request 的回答。
+ * 唤醒后端 Bridge 中阻塞等待的 Hermes Agent 线程,工作流继续执行。
+ */
+export function submitClarifyAnswer(runId, clarifyId, answer) {
+  return request.post(`/workflows/runs/${runId}/resume`, { clarifyId, answer })
+}
+
 /**
  * 自动关联分析:识别 Skill/SmartAction 节点隐式引用的其他 Skill
  */

+ 58 - 0
frontend/src/components/agent-thinking/AgentThinkingPanel.vue

@@ -0,0 +1,58 @@
+<script setup>
+/**
+ * v1.1:Hermes Agent 思考过程展示容器(深色风格)
+ *
+ * 整合 3 个区域(ThinkingLive 已提到 WorkflowEditor.vue 的 .nrc-thinking 上方):
+ *   ③ TodoListPanel   —— 待办清单(位于工具调用上方)
+ *   ④ ToolCalls       —— 工具调用(current + history)
+ *   ⑤ AskforPanel     —— 全局唯一用户确认(内嵌)
+ *
+ * 还包含 RunReconnectBadge 显示断线重连状态。
+ */
+import ToolCalls from './ToolCalls.vue'
+import TodoListPanel from './TodoListPanel.vue'
+import AskforPanel from './AskforPanel.vue'
+import RunReconnectBadge from './RunReconnectBadge.vue'
+
+defineProps({
+  // 当前节点结果对象(必填)
+  node: { type: Object, required: true },
+  // v1.1:全局 Askfor 暂停点(来自 runner.pendingAsk)
+  pendingAsk: { type: Object, default: null },
+  // v1.1:全局重连状态(来自 runner.reconnecting)
+  reconnecting: { type: Boolean, default: false },
+})
+
+const emit = defineEmits(['submit-clarify'])
+</script>
+
+<template>
+  <div class="agent-thinking-panel">
+    <RunReconnectBadge :reconnecting="reconnecting" />
+
+    <!-- ③ 待办清单:放在工具调用上方,running 时强制展开,完成后自动收起 -->
+    <TodoListPanel
+      :todos="node.todos || []"
+      :node-running="node.status === 'running'"
+    />
+
+    <!-- ④ 工具调用:传节点状态,running 时强制展开历史 -->
+    <ToolCalls
+      :current-tool="node.currentTool || null"
+      :history="node.toolHistory || []"
+      :node-running="node.status === 'running'"
+    />
+
+    <!-- ⑤ Askfor 内嵌(pendingAsk 与当前节点绑定时显示) -->
+    <AskforPanel :pending="pendingAsk" @submit="answer => emit('submit-clarify', answer)" />
+  </div>
+</template>
+
+<style scoped>
+/* v1.1:Agent 思考过程容器,深色风格(与 .nrc-thinking 视觉一致)
+   容器本身不占额外 padding/margin,让子组件(ToolCalls/TodoList/Askfor)
+   的 margin 直接生效,与 .nrc-thinking 在 .node-result-card 中对齐 */
+.agent-thinking-panel {
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+}
+</style>

+ 217 - 0
frontend/src/components/agent-thinking/AskforPanel.vue

@@ -0,0 +1,217 @@
+<script setup>
+/**
+ * v1.1:区域 ⑤ —— Askfor 用户确认(内嵌式)
+ * 全局唯一(同 run 同时只有一个 pending),嵌在 AgentThinkingPanel 内。
+ *
+ * 行为:
+ * - 展示 question + choices 列表(按钮形式,宽度一致)
+ * - 自定义输入行与选项按钮外观/高度一致,下方独立一行
+ * - 用户点击选项 → 直接提交该选项值
+ * - 用户在输入框输入并提交 → 提交自定义文本
+ */
+import { ref, computed } from 'vue'
+import { NInput } from 'naive-ui'
+
+const props = defineProps({
+  pending: { type: Object, default: null }, // {clarifyId, question, choices}
+})
+const emit = defineEmits(['submit'])
+
+const customText = ref('')
+
+const options = computed(() => {
+  return Array.isArray(props.pending?.choices) ? [...props.pending.choices] : []
+})
+
+function submitChoice(c) {
+  emit('submit', c)
+}
+
+function submitCustom() {
+  const text = customText.value.trim()
+  if (!text) return
+  emit('submit', text)
+  customText.value = ''
+}
+</script>
+
+<template>
+  <div v-if="pending" class="askfor-inline">
+    <div class="header">
+      <span class="title">需要您的确认</span>
+    </div>
+    <div class="question">{{ pending.question }}</div>
+
+    <!-- 选项按钮:始终显示,宽度一致 -->
+    <div v-if="options.length" class="choices">
+      <button
+        v-for="(c, i) in options"
+        :key="i"
+        class="choice-btn"
+        @click="submitChoice(c)"
+      >
+        {{ c }}
+      </button>
+    </div>
+
+    <!-- 自定义输入行:与选项按钮高度/外观一致 -->
+    <div class="custom-row">
+      <NInput
+        v-model:value="customText"
+        class="custom-input"
+        placeholder="或输入自定义回答..."
+        :bordered="false"
+        @keydown.enter.prevent="submitCustom"
+      />
+      <button
+        class="submit-btn"
+        :disabled="!customText.trim()"
+        @click="submitCustom"
+      >
+        提交
+      </button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* v1.1:内嵌式 Askfor,深色风格统一
+   整个面板使用同一字体 + 同一号字(选项按钮与输入框完全一致) */
+.askfor-inline {
+  border: 1px solid rgba(168, 85, 247, 0.4);
+  border-radius: 6px;
+  background: rgba(168, 85, 247, 0.05);
+  padding: 10px 12px;
+  margin: 8px 10px 4px;
+  /* 容器统一字体,子元素 NInput/button 全部继承 */
+  font-size: 12px;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+  line-height: 1.5;
+}
+.header {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-bottom: 6px;
+}
+.title {
+  font-weight: 600;
+  font-size: 12px;
+  color: #c084fc;
+}
+.question {
+  padding: 8px 10px;
+  background: rgba(255, 255, 255, 0.03);
+  border-radius: 4px;
+  margin-bottom: 8px;
+  color: var(--text-secondary, #b0b0b0);
+  white-space: pre-wrap;
+  word-break: break-word;
+  font-size: 12px;
+  border-left: 2px solid rgba(168, 85, 247, 0.5);
+}
+.choices {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  margin-bottom: 4px;
+}
+.choice-btn {
+  /* 与 custom-row 同高度、同内边距、同宽度、同字体、同字号 */
+  padding: 5px 12px;
+  min-height: 32px;
+  border: 1px solid rgba(168, 85, 247, 0.3);
+  border-radius: 3px;
+  background: rgba(255, 255, 255, 0.03);
+  cursor: pointer;
+  text-align: left;
+  transition: all 0.15s;
+  font-size: 12px;
+  color: var(--text-primary, #e0e0e0);
+  font-family: inherit; /* 继承 .askfor-inline 的字体 */
+  width: 100%;
+}
+.choice-btn:hover {
+  border-color: #c084fc;
+  background: rgba(168, 85, 247, 0.12);
+  color: #c084fc;
+}
+
+/* 自定义输入行:与选项按钮外观一致,左侧输入 + 右侧提交按钮 */
+.custom-row {
+  display: flex;
+  align-items: stretch;
+  margin-top: 4px;
+  min-height: 32px;
+  border: 1px solid rgba(168, 85, 247, 0.3);
+  border-radius: 3px;
+  background: rgba(255, 255, 255, 0.03);
+  overflow: hidden;
+  transition: all 0.15s;
+  font-family: inherit; /* 继承字体 */
+  font-size: 12px;
+}
+.custom-row:focus-within {
+  border-color: #c084fc;
+  background: rgba(168, 85, 247, 0.08);
+}
+.custom-row :deep(.n-input) {
+  flex: 1;
+  background: transparent;
+  font-family: inherit !important;
+  font-size: 12px !important;
+  /* NInput 容器本身零内边距,避免与内部输入元素的 padding 叠加产生"中文 padding"错觉 */
+  padding: 0 !important;
+}
+/* NInput 的 wrapper 也是零内边距,让输入元素的 padding 成为唯一水平留白 */
+.custom-row :deep(.n-input .n-input-wrapper) {
+  padding: 0 !important;
+  border: none !important;
+  min-height: 32px !important;
+}
+.custom-row :deep(.n-input .n-input__textarea-el),
+.custom-row :deep(.n-input .n-input__input-el) {
+  font-size: 12px !important;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace !important;
+  color: var(--text-primary, #e0e0e0);
+  padding: 5px 12px;
+  min-height: 32px !important;
+  line-height: 1.5;
+  resize: none;
+}
+/* placeholder 同样字体同字号 */
+.custom-row :deep(.n-input .n-input__placeholder),
+.custom-row :deep(.n-input textarea::placeholder) {
+  font-size: 12px !important;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace !important;
+}
+/* placeholder 与真实输入光标对齐:因为外层 .n-input / .n-input-wrapper 的 padding
+   被清零(见上),只有最内层 input 元素有 padding-left:12px,导致绝对定位的
+   placeholder 元素贴在最左边、与输入光标错位。这里给 placeholder 补上同样的左 padding。 */
+.custom-row :deep(.n-input .n-input__placeholder) {
+  padding-left: 12px !important;
+}
+
+/* 提交按钮:纯色背景 + 白字,小尺寸,与"保存"按钮主色一致 */
+.submit-btn {
+  flex-shrink: 0;
+  padding: 0 14px;
+  min-height: 32px;
+  border: none;
+  border-radius: 0;
+  background: var(--color-primary, #2563EB);
+  color: #ffffff;
+  font-size: 12px;
+  font-family: inherit; /* 继承字体,与选项/输入框一致 */
+  cursor: pointer;
+  transition: background 0.15s;
+}
+.submit-btn:hover:not(:disabled) {
+  background: var(--color-primary-light, #3B82F6);
+}
+.submit-btn:disabled {
+  background: rgba(255, 255, 255, 0.1);
+  color: var(--text-tertiary, #666);
+  cursor: not-allowed;
+}
+</style>

+ 44 - 0
frontend/src/components/agent-thinking/RunReconnectBadge.vue

@@ -0,0 +1,44 @@
+<script setup>
+/**
+ * v1.1:断线重连状态徽章
+ * 内嵌在 AgentThinkingPanel 顶部,显示当前是否正在重连。
+ */
+const props = defineProps({
+  reconnecting: { type: Boolean, default: false },
+})
+</script>
+
+<template>
+  <div v-if="reconnecting" class="badge">
+    <span class="spinner"></span>
+    <span>正在重连...</span>
+  </div>
+</template>
+
+<style scoped>
+/* v1.1:重连徽章,深色风格(黄色警告色) */
+.badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  padding: 2px 8px;
+  margin: 4px 10px;
+  background: rgba(251, 191, 36, 0.12);
+  color: #fbbf24;
+  border: 1px solid rgba(251, 191, 36, 0.3);
+  border-radius: 10px;
+  font-size: 10px;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+}
+.spinner {
+  width: 7px;
+  height: 7px;
+  border: 1.5px solid rgba(251, 191, 36, 0.3);
+  border-top-color: #fbbf24;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+</style>

+ 52 - 0
frontend/src/components/agent-thinking/ThinkingLive.vue

@@ -0,0 +1,52 @@
+<script setup>
+/**
+ * v1.1:区域 ② —— 实时状态(覆盖式)
+ * 显示 Hermes 当前正在做什么,如"压缩上下文中"、"第 N 轮 LLM 调用"、"正在调用 read_file"。
+ * 空状态时折叠,避免占用空间。
+ */
+import { computed } from 'vue'
+
+const props = defineProps({
+  status: { type: String, default: '' },
+})
+
+const isEmpty = computed(() => !props.status || !props.status.trim())
+</script>
+
+<template>
+  <div v-if="!isEmpty" class="thinking-live">
+    <span class="dot"></span>
+    <span class="text">{{ status }}</span>
+  </div>
+</template>
+
+<style scoped>
+/* v1.1:实时状态行,深色风格(紫色脉冲点 + 等宽字体) */
+.thinking-live {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 10px;
+  font-size: 11px;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+  color: var(--text-secondary, #b0b0b0);
+  margin: 4px 10px 6px;
+}
+.dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #c084fc;
+  box-shadow: 0 0 6px rgba(168, 85, 247, 0.6);
+  animation: pulse 1.4s ease-in-out infinite;
+  flex-shrink: 0;
+}
+.text {
+  font-weight: 500;
+  color: #c084fc;
+}
+@keyframes pulse {
+  0%, 100% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(0.8); }
+}
+</style>

+ 149 - 0
frontend/src/components/agent-thinking/TodoListPanel.vue

@@ -0,0 +1,149 @@
+<script setup>
+/**
+ * v1.1:区域 ④ —— 待办清单
+ * 渲染 Hermes todo 工具输出的完整 JSON(结构:[{content, status, activeForm}],
+ * 也兼容 Hermes 内置 todo_tool 返回的 {todos: [...], summary: {...}} 包装)。
+ *
+ * status 取值:pending / in_progress / completed / cancelled。
+ *
+ * 视觉与交互完全对齐 ToolCalls:
+ *   - header:"待办清单(completed / total)" + 展开箭头
+ *   - 节点 running 时强制展开;完成后自动收起;用户点击 header 可手动切换
+ */
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+  todos: { type: Array, default: () => [] },
+  nodeRunning: { type: Boolean, default: false },
+})
+
+const expanded = ref(false)
+
+const total = computed(() => props.todos.length)
+const completed = computed(() => props.todos.filter(t => t.status === 'completed').length)
+
+// 节点 running 时强制展开;变为非 running 时自动收起
+watch(() => props.nodeRunning, (running) => {
+  expanded.value = running
+}, { immediate: true })
+
+function statusLabel(s) {
+  return { completed: '已完成', in_progress: '进行中', pending: '待办', cancelled: '已取消' }[s] || s
+}
+</script>
+
+<template>
+  <div v-if="total > 0" class="todo-panel" :class="{ live: nodeRunning }">
+    <div class="todo-header" @click="expanded = !expanded">
+      <span class="todo-tag">待办清单</span>
+      <span class="todo-count">({{ completed }} / {{ total }})</span>
+      <span class="todo-expand" :class="{ expanded }">▶</span>
+    </div>
+    <ul v-if="expanded" class="todo-list">
+      <li v-for="(t, i) in todos" :key="i" :class="['todo-item', t.status]">
+        <div class="body">
+          <div class="content">{{ t.content || t.title || '(未命名)' }}</div>
+          <div v-if="t.activeForm" class="active-form">{{ t.activeForm }}</div>
+        </div>
+        <span class="status-tag">{{ statusLabel(t.status) }}</span>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<style scoped>
+/* v1.1:待办清单外框 —— 与 .tool-calls-wrap 完全一致的视觉语言 */
+.todo-panel {
+  margin: 6px 10px;
+  border-radius: 6px;
+  background: rgba(168, 85, 247, 0.05);
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  overflow: hidden;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+}
+.todo-panel.live {
+  border-color: rgba(168, 85, 247, 0.5);
+  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
+}
+
+/* header —— 与 .tool-calls-header 对齐 */
+.todo-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 10px;
+  font-size: 11px;
+  background: rgba(168, 85, 247, 0.08);
+  cursor: pointer;
+  user-select: none;
+}
+
+/* "待办清单" —— 与 .tool-calls-tag 一致:紫色加粗 */
+.todo-tag {
+  color: #c084fc;
+  font-weight: 600;
+}
+
+/* "(X / X)" —— 与 .tool-calls-count 一致 */
+.todo-count {
+  color: #888;
+  font-size: 10px;
+}
+
+/* 展开箭头 —— 与 .tool-calls-expand 一致 */
+.todo-expand {
+  margin-left: auto;
+  font-size: 10px;
+  color: var(--text-tertiary, #666);
+  transition: transform 0.2s;
+  flex-shrink: 0;
+}
+.todo-expand.expanded {
+  transform: rotate(90deg);
+}
+
+.todo-list {
+  list-style: none;
+  padding: 4px 0;
+  margin: 0;
+}
+.todo-item {
+  display: flex;
+  align-items: flex-start;
+  gap: 6px;
+  padding: 4px 10px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
+  font-size: 11px;
+}
+.todo-item:last-child {
+  border-bottom: none;
+}
+.todo-item.completed {
+  opacity: 0.55;
+}
+.todo-item.completed .content {
+  text-decoration: line-through;
+}
+.todo-item.in_progress {
+  background: rgba(168, 85, 247, 0.08);
+}
+.body {
+  flex: 1;
+}
+.content {
+  color: var(--text-primary, #e0e0e0);
+}
+.active-form {
+  color: var(--text-tertiary, #666);
+  font-size: 10px;
+  font-style: italic;
+}
+.status-tag {
+  font-size: 9px;
+  padding: 1px 5px;
+  background: rgba(168, 85, 247, 0.15);
+  border-radius: 2px;
+  color: #c084fc;
+  flex-shrink: 0;
+}
+</style>

+ 253 - 0
frontend/src/components/agent-thinking/ToolCalls.vue

@@ -0,0 +1,253 @@
+<script setup>
+/**
+ * v1.1:区域 ③ —— 工具调用
+ * - 整体外框样式与"思考过程"区块(.nrc-thinking)一致
+ * - header:"工具调用" + "(N)" 计数,与"思考过程 / XX 字"结构对齐
+ * - currentTool:当前执行中的工具(始终展开)
+ * - history:已完成的工具,默认折叠(仅显示 header),点击 header 展开看详情
+ * - 节点 running 时(nodeRunning=true):历史强制展开,便于实时观察
+ * - 节点完成时(nodeRunning=false):自动收起历史
+ * - 历史完整显示(不再截断为最近 5 条)
+ */
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+  currentTool: { type: Object, default: null }, // {name, argsSummary, result}
+  history: { type: Array, default: () => [] },
+  nodeRunning: { type: Boolean, default: false },
+})
+
+// 历史整体折叠状态:默认 false(仅显示切换器)
+const historyExpanded = ref(false)
+// 每个工具卡片的独立展开状态:key=工具名+索引,value=true/false(默认 false 即收起)
+const cardExpanded = ref({})
+
+const historyCount = computed(() => props.history.length)
+
+// 节点 running 时强制展开历史;变为非 running 时自动收起
+watch(() => props.nodeRunning, (running) => {
+  historyExpanded.value = running
+}, { immediate: true })
+
+function toggleCard(key) {
+  cardExpanded.value[key] = !cardExpanded.value[key]
+}
+
+function isCardExpanded(key) {
+  return cardExpanded.value[key] === true
+}
+
+// 历史变化时重置卡片展开状态,避免错位
+watch(() => props.history, () => {
+  cardExpanded.value = {}
+}, { deep: false })
+</script>
+
+<template>
+  <!-- v-if 守卫:无任何工具调用数据时不渲染外框,避免出现空白圆角矩形 -->
+  <div
+    v-if="historyCount > 0 || currentTool"
+    class="tool-calls-wrap"
+    :class="{ live: nodeRunning }"
+  >
+    <!-- header:与"思考过程"结构对齐 -->
+    <div
+      v-if="historyCount > 0 || currentTool"
+      class="tool-calls-header"
+      :class="{ clickable: historyCount > 0 }"
+      @click="historyCount > 0 && (historyExpanded = !historyExpanded)"
+    >
+      <span class="tool-calls-tag">工具调用</span>
+      <span v-if="nodeRunning && currentTool" class="tool-calls-live">执行中…</span>
+      <span v-else class="tool-calls-count">({{ historyCount + (currentTool ? 1 : 0) }})</span>
+      <span
+        v-if="historyCount > 0"
+        class="tool-calls-expand"
+        :class="{ expanded: historyExpanded }"
+      >▶</span>
+    </div>
+
+    <div v-if="historyExpanded || currentTool" class="tool-calls-body">
+      <!-- 当前执行中的工具:始终展开 -->
+      <div v-if="currentTool" class="tool-card current">
+        <div class="header">
+          <span class="spinner"></span>
+          <span class="name">{{ currentTool.name }}</span>
+          <span class="status">执行中</span>
+        </div>
+        <div v-if="currentTool.argsSummary" class="args">{{ currentTool.argsSummary }}</div>
+      </div>
+
+      <!-- 已完成的工具调用列表(全部展示) -->
+      <template v-if="historyExpanded">
+        <div
+          v-for="(t, i) in history"
+          :key="i"
+          class="tool-card history"
+        >
+          <div class="header clickable" @click.stop="toggleCard(`h-${i}`)">
+            <span class="toggle-arrow small" :class="{ expanded: isCardExpanded(`h-${i}`) }">▶</span>
+            <span class="name">{{ t.name }}</span>
+            <span class="status done">已完成</span>
+          </div>
+          <div v-if="isCardExpanded(`h-${i}`)">
+            <div v-if="t.argsSummary" class="args">{{ t.argsSummary }}</div>
+            <div v-if="t.result" class="result">{{ t.result }}</div>
+          </div>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* v1.1:工具调用区外框 —— 与 .nrc-thinking 完全一致的视觉语言 */
+.tool-calls-wrap {
+  margin: 6px 10px;
+  border-radius: 6px;
+  background: rgba(168, 85, 247, 0.05);
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  overflow: hidden;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+}
+.tool-calls-wrap.live {
+  border-color: rgba(168, 85, 247, 0.5);
+  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
+}
+
+/* header —— 与 .nrc-thinking-header 对齐 */
+.tool-calls-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 10px;
+  font-size: 11px;
+  background: rgba(168, 85, 247, 0.08);
+}
+.tool-calls-header.clickable {
+  cursor: pointer;
+  user-select: none;
+}
+
+/* "🔧 工具调用" —— 与 .nrc-thinking-tag 一致:紫色加粗 */
+.tool-calls-tag {
+  color: #c084fc;
+  font-weight: 600;
+}
+
+/* "执行中…" 实时提示 —— 与 .nrc-thinking-live 一致:粉色脉冲 */
+.tool-calls-live {
+  color: #ec4899;
+  font-size: 10px;
+  animation: tc-pulse 1.2s ease-in-out infinite;
+}
+
+/* "(N)" 计数 —— 与 .nrc-thinking-static 一致:灰色小字 */
+.tool-calls-count {
+  color: #888;
+  font-size: 10px;
+}
+
+/* 展开箭头 —— 与 .nrc-thinking-expand 一致 */
+.tool-calls-expand {
+  margin-left: auto;
+  font-size: 10px;
+  color: var(--text-tertiary, #666);
+  transition: transform 0.2s;
+  flex-shrink: 0;
+}
+.tool-calls-expand.expanded {
+  transform: rotate(90deg);
+}
+
+@keyframes tc-pulse {
+  0%, 100% { opacity: 0.55; }
+  50% { opacity: 1; }
+}
+
+/* body —— 工具卡片列表 */
+.tool-calls-body {
+  padding: 6px 8px;
+}
+.tool-card {
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  border-radius: 4px;
+  background: rgba(168, 85, 247, 0.04);
+  padding: 5px 8px;
+  margin-bottom: 4px;
+  font-size: 11px;
+}
+.tool-card:last-child {
+  margin-bottom: 0;
+}
+.tool-card.current {
+  border-color: rgba(168, 85, 247, 0.6);
+  background: rgba(168, 85, 247, 0.1);
+  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
+}
+.header {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.header.clickable {
+  cursor: pointer;
+  user-select: none;
+}
+.header.clickable:hover .name {
+  color: #c084fc;
+}
+.name {
+  font-weight: 600;
+  color: var(--text-primary, #e0e0e0);
+}
+.status {
+  margin-left: auto;
+  color: #c084fc;
+  font-size: 10px;
+}
+.status.done {
+  color: var(--text-tertiary, #666);
+}
+.args, .result {
+  margin-top: 3px;
+  white-space: pre-wrap;
+  word-break: break-all;
+  max-height: 80px;
+  overflow-y: auto;
+  color: var(--text-tertiary, #666);
+  font-size: 10px;
+  padding-left: 4px;
+  border-left: 1px solid rgba(168, 85, 247, 0.2);
+}
+.result {
+  color: #10b981;
+  border-left-color: rgba(16, 185, 129, 0.3);
+}
+.spinner {
+  width: 8px;
+  height: 8px;
+  border: 1.5px solid rgba(168, 85, 247, 0.3);
+  border-top-color: #c084fc;
+  border-radius: 50%;
+  animation: tc-spin 0.8s linear infinite;
+  flex-shrink: 0;
+}
+@keyframes tc-spin {
+  to { transform: rotate(360deg); }
+}
+
+/* 卡片内展开箭头 */
+.toggle-arrow {
+  display: inline-block;
+  font-size: 9px;
+  transition: transform 0.2s;
+  flex-shrink: 0;
+}
+.toggle-arrow.small {
+  font-size: 8px;
+}
+.toggle-arrow.expanded {
+  transform: rotate(90deg);
+}
+</style>

+ 328 - 48
frontend/src/composables/useWorkflowRunner.js

@@ -1,6 +1,7 @@
 import { ref, reactive } from 'vue'
 import { useMessage } from 'naive-ui'
-import { runWorkflow } from '../api/workflow'
+import { runWorkflow, reconnectRunStream, getRunState, submitClarifyAnswer } from '../api/workflow'
+import { stripHermesThinkingDecoration } from '../utils/hermesThinking'
 
 /**
  * 判断对象是否为节点输出 envelope `{status, message, data}`。
@@ -50,16 +51,34 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
   const showRunDialog = ref(false)
   const showResult = ref(false)
   const workflowResult = ref(null)
+  /**
+   * 当前运行元数据 { runId, workingDirFiles }。
+   * 这两个字段不再注入 output 节点 envelope,而是作为 workflow-run 的属性由后端 SSE 推送 /
+   * 持久化到 WorkflowRun 实体。前端在"下载工作空间"按钮旁独立展示,与下载入口出现时机一致。
+   */
+  const runMeta = ref(null)
   const nodeStatuses = reactive({})
   const activeTab = ref('props')
   const nodeResults = reactive([])
-  const expandedLogs = reactive({})
   const expandedContext = reactive({})
-  // 思考过程折叠状态:key=nodeId,value=true/false。未设置默认展开。
+  // 思考过程折叠状态:key=nodeId,value=true/false。未设置默认折叠(避免运行结果页过长)。
   // 折叠时仅隐藏 UI,streamingThinking 仍持续累加,展开后可继续看到完整内容。
   const expandedThinking = reactive({})
+  // 执行日志折叠状态:同 expandedThinking。节点 running 时强制展开,完成时自动收起(清空 key)。
+  const expandedLogs = reactive({})
   const lastRunId = ref(null)
   const lastRunWorkflowId = ref(null)
+  // v1.1:断线重连相关状态
+  // lastEventId:从 SSE 帧的 id: 字段提取,用于断线重连续接
+  // reconnecting:是否正在重连(控制 UI 提示)
+  const lastEventId = ref(null)
+  const reconnecting = ref(false)
+  // 重连失败次数上限:超过则退化为 GET /state 轮询
+  const MAX_RECONNECT_ATTEMPTS = 5
+  // v1.1:全局唯一 Askfor 暂停点 {clarifyId, question, choices}
+  const pendingAsk = ref(null)
+  // v1.1:轮询定时器(重连耗尽后启动)
+  const pollTimer = ref(null)
 
   function resetState() {
     Object.keys(nodeStatuses).forEach(k => delete nodeStatuses[k])
@@ -81,26 +100,50 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
     running.value = true
     showResult.value = false
     workflowResult.value = null
+    runMeta.value = null
     resetState()
     clearNodeStatusClasses()
     // 清空折叠状态(不影响本轮首次默认展开)
     Object.keys(expandedThinking).forEach(k => delete expandedThinking[k])
+    Object.keys(expandedLogs).forEach(k => delete expandedLogs[k])
+    // 重置断线重连状态
+    lastEventId.value = null
+    reconnecting.value = false
     // 启动时单次切到「执行结果」Tab;后续不再强制切换,允许用户自由回到「节点属性」
     activeTab.value = 'result'
 
+    let response
     try {
-      const response = await runWorkflow(
+      response = await runWorkflow(
         nameOrId,
         inputs,
         uploadFiles.map(f => f.file),
         uploadFiles.map(f => f.relativePath)
       )
       if (!response.ok) throw new Error(`HTTP ${response.status}`)
+    } catch (e) {
+      message.error('工作流启动失败: ' + e.message)
+      running.value = false
+      return
+    }
 
-      const reader = response.body.getReader()
-      const decoder = new TextDecoder()
-      let buffer = ''
+    // 进入 SSE 消费循环(首次或重连都走同一段逻辑)
+    await consumeSSEStream(response)
+  }
+
+  /**
+   * v1.1:消费 SSE 流。
+   * - 解析 event.id / event.data 双字段,记录 lastEventId 用于断线重连
+   * - reader.read() 抛错时不立即报失败,自动尝试通过 GET /runs/{runId}/stream 续连
+   * - workflow_complete / workflow_error 事件结束循环
+   */
+  async function consumeSSEStream(response) {
+    const reader = response.body.getReader()
+    const decoder = new TextDecoder()
+    let buffer = ''
+    let attempts = 0
 
+    try {
       while (true) {
         const { done, value } = await reader.read()
         if (done) break
@@ -112,22 +155,142 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
         for (const part of parts) {
           const lines = part.split('\n')
           let data = null
+          let eventId = null
           for (const line of lines) {
-            if (line.startsWith('data:')) {
+            if (line.startsWith('id:')) {
+              eventId = line.substring(3).trim()
+            } else if (line.startsWith('data:')) {
               try { data = JSON.parse(line.substring(5).trim()) } catch { /* skip */ }
             }
           }
+          if (eventId != null) {
+            lastEventId.value = Number(eventId)
+            // 收到事件即清零重连计数(说明连接健康)
+            attempts = 0
+          }
+          // comment 帧心跳(":keep-alive")已被 split('\n\n') 切割掉,正常忽略
           if (data) handleSSEEvent(data)
         }
       }
+      // 流正常结束:检查最终状态,若未完成则尝试重连(可能后端早完成但 SSE 已断)
+      if (lastRunId.value && !showResult.value && !isTerminal()) {
+        await tryReconnect(attempts)
+      }
     } catch (e) {
-      message.error('工作流执行失败: ' + e.message)
+      // reader.read() 抛错:网络断开,尝试重连
+      if (lastRunId.value && !isTerminal()) {
+        message.warning('连接中断,尝试重连...')
+        await tryReconnect(attempts)
+      } else {
+        message.error('工作流执行失败: ' + e.message)
+      }
     } finally {
+      if (!reconnecting.value) {
+        running.value = false
+      }
+    }
+  }
+
+  /**
+   * 判断当前工作流是否已终止(complete / error),用于决定是否值得重连。
+   */
+  function isTerminal() {
+    return showResult.value || workflowResult.value !== null
+  }
+
+  /**
+   * v1.1:尝试通过 GET /runs/{runId}/stream 重连。
+   * 失败 MAX_RECONNECT_ATTEMPTS 次后回退到 GET /state 轮询。
+   */
+  async function tryReconnect(attemptsBase) {
+    if (!lastRunId.value) return
+    reconnecting.value = true
+    let attempts = attemptsBase
+    while (attempts < MAX_RECONNECT_ATTEMPTS) {
+      attempts++
+      try {
+        await new Promise(r => setTimeout(r, 3000)) // 等 3 秒
+        const resp = await reconnectRunStream(lastRunId.value, lastEventId.value)
+        if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
+        reconnecting.value = false
+        message.success('已重新连接')
+        await consumeSSEStream(resp)
+        return
+      } catch (e) {
+        message.warning(`重连失败 (${attempts}/${MAX_RECONNECT_ATTEMPTS}): ${e.message}`)
+      }
+    }
+    // 重连耗尽:回退到轮询 GET /state
+    reconnecting.value = false
+    message.warning('重连失败,回退到状态轮询')
+    await pollRunState()
+  }
+
+  /**
+   * v1.1:回退方案 —— 每 30 秒轮询 GET /state,发现 RUNNING 且 currentPause 则恢复 Askfor UI,
+   * 发现 SUCCESS / FAILED 则结束运行。
+   */
+  async function pollRunState() {
+    if (!lastRunId.value) {
       running.value = false
+      return
     }
+    const poll = async () => {
+      try {
+        const res = await getRunState(lastRunId.value)
+        const state = res?.data || res
+        if (state.currentPause && state.status === 'RUNNING') {
+          // 恢复 Askfor UI(按 runId 维度存)。后端返回的 nodeId 用于前端按节点过滤展示,
+          // 避免其他节点(如 userInput)被错误地渲染出 AskforPanel。
+          if (!pendingAsk.value) {
+            pendingAsk.value = {
+              nodeId: state.currentPause.nodeId || null,
+              clarifyId: state.currentPause.clarifyId,
+              question: state.currentPause.question,
+              choices: safeParseChoices(state.currentPause.choices)
+            }
+          }
+        }
+        if (state.status === 'SUCCESS' || state.status === 'FAILED') {
+          running.value = false
+          if (state.status === 'SUCCESS') {
+            message.success('工作流执行完成(来自轮询)')
+          } else {
+            message.error('工作流执行失败(来自轮询)')
+          }
+          return true // 终止轮询
+        }
+        return false
+      } catch (e) {
+        return false
+      }
+    }
+    const terminated = await poll()
+    if (terminated || !running.value) return
+    pollTimer.value = setInterval(async () => {
+      const done = await poll()
+      if (done || !running.value) {
+        clearInterval(pollTimer.value)
+        pollTimer.value = null
+      }
+    }, 30000)
+  }
+
+  function safeParseChoices(s) {
+    if (!s) return []
+    try {
+      const v = typeof s === 'string' ? JSON.parse(s) : s
+      return Array.isArray(v) ? v : []
+    } catch { return [] }
   }
 
   function handleSSEEvent(event) {
+    // v1.1:从任意事件提取 runId,便于断线重连
+    if (event.runId && !lastRunId.value) {
+      lastRunId.value = event.runId
+      lastRunWorkflowId.value = workflowIdRef.value
+    }
+
     if (event.type === 'node_status' && event.nodeId) {
       const status = event.status?.toLowerCase() || 'running'
       nodeStatuses[event.nodeId] = status
@@ -136,6 +299,14 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
 
       const info = nodeTypeInfo[node?.type] || { color: '#666', typeLabel: '未知' }
       const existing = nodeResults.find(r => r.nodeId === event.nodeId)
+      // v1.1:节点完成(success/failed/skipped/no_run)时自动收起思考过程、清空实时状态
+      // 节点 running 时不动 expandedThinking(首次出现默认折叠,但 v-show 表达式会强制展开)
+      const isFinished = ['success', 'failed', 'skipped', 'no_run'].includes(status)
+      if (isFinished) {
+        delete expandedThinking[event.nodeId]
+        // 同步收起执行日志(运行中默认展开,完成后默认收起;用户仍可手动展开)
+        delete expandedLogs[event.nodeId]
+      }
       const update = {
         nodeId: event.nodeId,
         type: node?.type,
@@ -149,6 +320,11 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
         contextSnapshot: event.contextSnapshot || existing?.contextSnapshot || null,
         // 保留此前累加的流式 thinking 内容(节点完成后仍可见思考历史)
         streamingThinking: existing?.streamingThinking || '',
+        // v1.1:节点完成后清空实时状态(如 "reasoning..."),保留 currentTool/history/todos
+        currentStatus: isFinished ? '' : (existing?.currentStatus || ''),
+        currentTool: existing?.currentTool || null,
+        toolHistory: existing?.toolHistory || [],
+        todos: existing?.todos || [],
         color: info.color,
         typeLabel: info.typeLabel
       }
@@ -158,52 +334,155 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
         nodeResults.push(update)
       }
     } else if (event.type === 'node_stream' && event.nodeId) {
-      // 节点执行过程中的流式增量(Hermes Agent 的 thinking 等)
-      // thinking 内容按 nodeId 聚合累加,不产生独立条目
-      if (event.kind === 'thinking') {
-        const delta = event.content || ''
-        if (!delta) return
-        const existing = nodeResults.find(r => r.nodeId === event.nodeId)
-        if (existing) {
-          existing.streamingThinking = (existing.streamingThinking || '') + delta
-        } else {
-          const node = getNodes.value.find(n => n.id === event.nodeId)
-          const info = nodeTypeInfo[node?.type] || { color: '#666', typeLabel: '未知' }
-          nodeResults.push({
-            nodeId: event.nodeId,
-            type: node?.type,
-            label: node?.data?.label || '节点',
-            status: 'running',
-            streamingThinking: delta,
-            color: info.color,
-            typeLabel: info.typeLabel
-          })
+      // v1.1:按 kind 分发,分别维护 5 个区域的状态
+      const existing = ensureNodeResult(event.nodeId)
+      if (!existing) return
+
+      const kind = event.kind
+      const content = event.content || ''
+      const toolName = event.toolName || null
+
+      if (kind === 'thinking') {
+        // ① 追加型思考过程
+        if (content) existing.streamingThinking = (existing.streamingThinking || '') + content
+      } else if (kind === 'status') {
+        // ② 实时型思考过程(覆盖式)
+        // 剥离 Hermes KawaiiSpinner 拼接的前导颜文字与尾部省略号
+        existing.currentStatus = stripHermesThinkingDecoration(content)
+      } else if (kind === 'step') {
+        // ② 也作为实时状态展示(iteration+prev_tools)
+        try {
+          const obj = JSON.parse(content)
+          existing.currentStatus = `第 ${obj.iteration} 轮 LLM 调用`
+        } catch { /* ignore */ }
+      } else if (kind === 'tool_progress') {
+        // ②/③ 工具细粒度进度
+        try {
+          const obj = JSON.parse(content)
+          if (obj.event_type === 'tool.started' && obj.name) {
+            existing.currentStatus = `正在调用 ${obj.name}`
+          }
+        } catch { /* ignore */ }
+      } else if (kind === 'tool_call') {
+        // ③ 工具调用:上方覆盖(currentTool),unshift 到历史
+        if (existing.currentTool) {
+          existing.toolHistory.unshift(existing.currentTool)
+        }
+        existing.currentTool = { name: toolName, argsSummary: content, result: null }
+      } else if (kind === 'tool_result') {
+        // ③ 工具返回:清空 currentTool,更新 history[0].result
+        if (existing.currentTool) {
+          existing.currentTool.result = content
+          existing.toolHistory.unshift(existing.currentTool)
+          existing.currentTool = null
         }
-        // 注意:不在此处切换 activeTab,避免流式输出过程中锁定 UI 不让用户回到「节点属性」
+      } else if (kind === 'todo_update') {
+        // ④ TODO List:兼容两种格式
+        //   - 数组:[{content, status, activeForm}, ...]
+        //   - 对象:{todos: [...], summary: {...}}(Hermes 内置 todo_tool 实际返回此结构)
+        try {
+          const parsed = JSON.parse(content)
+          const arr = Array.isArray(parsed) ? parsed : (parsed?.todos || [])
+          existing.todos = Array.isArray(arr) ? arr : []
+        } catch { /* ignore */ }
+      } else if (kind === 'clarify_request') {
+        // ⑤ Askfor:全局唯一 pendingAsk(同 run 同时只有一个 pending)
+        // 注意:携带 event.nodeId 用于 UI 按 nodeId 过滤——只有触发了 clarify 的节点
+        // 才会显示 AskforPanel,其他节点(如 userInput)不应被波及。
+        try {
+          const obj = JSON.parse(content)
+          if (obj.clarify_id) {
+            pendingAsk.value = {
+              nodeId: event.nodeId || null,
+              clarifyId: obj.clarify_id,
+              question: obj.question || '',
+              choices: Array.isArray(obj.choices) ? obj.choices : []
+            }
+          }
+        } catch { /* ignore */ }
       }
-      // tool_call / tool_result 已通过节点完成后的 logs 显示,此处无需重复处理
+      // 注意:不在此处切换 activeTab,避免流式输出过程中锁定 UI 不让用户回到「节点属性」
     } else if (event.type === 'workflow_complete') {
       workflowResult.value = event.output
       showResult.value = true
-      if (event.output?._runId) {
-        lastRunId.value = event.output._runId
-        lastRunWorkflowId.value = workflowIdRef.value
-      } else if (event.runId) {
+      // runId / workingDirFiles 由后端 SSE 事件直接携带(已不再注入 envelope.data)
+      if (event.runId) {
         lastRunId.value = event.runId
-        lastRunWorkflowId.value = workflowIdRef.value
       }
+      runMeta.value = {
+        runId: event.runId || null,
+        workingDirFiles: Array.isArray(event.workingDirFiles) ? event.workingDirFiles : null,
+      }
+      lastRunWorkflowId.value = workflowIdRef.value
       message.success('工作流执行完成')
     } else if (event.type === 'workflow_error') {
       message.error('执行失败: ' + (event.error || '未知错误'))
     }
   }
 
+  /**
+   * v1.1:确保 nodeResults 中存在指定 nodeId 的条目(流式事件可能先于 node_status 到达)。
+   * 返回该条目或 null(getNodes 中查不到节点时)。
+   */
+  function ensureNodeResult(nodeId) {
+    let existing = nodeResults.find(r => r.nodeId === nodeId)
+    if (existing) return existing
+    const node = getNodes.value.find(n => n.id === nodeId)
+    const info = nodeTypeInfo[node?.type] || { color: '#666', typeLabel: '未知' }
+    existing = {
+      nodeId,
+      type: node?.type,
+      label: node?.data?.label || '节点',
+      status: 'running',
+      streamingThinking: '',
+      currentStatus: '',
+      currentTool: null,
+      toolHistory: [],
+      todos: [],
+      color: info.color,
+      typeLabel: info.typeLabel
+    }
+    nodeResults.push(existing)
+    return existing
+  }
+
+  /**
+   * v1.1:用户提交 Askfor 答案。
+   * 调 POST /runs/{runId}/resume → 后端唤醒 Hermes Agent 阻塞线程。
+   */
+  async function submitClarify(answer) {
+    if (!pendingAsk.value || !lastRunId.value) return
+    const { clarifyId } = pendingAsk.value
+    try {
+      const res = await submitClarifyAnswer(lastRunId.value, clarifyId, answer)
+      // request 拦截器已剥到 data;兼容 res.data.resolved
+      const resolved = res?.resolved ?? res?.data?.resolved ?? false
+      if (resolved) {
+        pendingAsk.value = null
+        message.success('已提交回答,工作流继续执行')
+      } else {
+        message.warning('回答未生效,可能 clarify 已过期')
+        pendingAsk.value = null
+      }
+    } catch (e) {
+      message.error('提交回答失败: ' + e.message)
+    }
+  }
+
   function closeResult() {
     showResult.value = false
     resetState()
-    Object.keys(expandedLogs).forEach(k => delete expandedLogs[k])
     clearNodeStatusClasses()
     activeTab.value = 'props'
+    // v1.1:清理重连与轮询资源
+    pendingAsk.value = null
+    lastEventId.value = null
+    reconnecting.value = false
+    if (pollTimer.value) {
+      clearInterval(pollTimer.value)
+      pollTimer.value = null
+    }
+    Object.keys(expandedLogs).forEach(k => delete expandedLogs[k])
   }
 
   function openRunDialog(inputFields, formData) {
@@ -220,10 +499,6 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
     return { running: '执行中', success: '成功', failed: '失败', skipped: '跳过', no_run: '未运行' }[s] || s
   }
 
-  function logTypeIcon(type) {
-    return { TOOL_CALL: '🔧', TOOL_RESULT: '📋', THINKING: '💭', ERROR: '❌', INFO: 'ℹ️' }[type] || '•'
-  }
-
   function logTypeLabel(type) {
     return { TOOL_CALL: '调用工具', TOOL_RESULT: '工具返回', THINKING: '思考', ERROR: '错误', INFO: '信息' }[type] || type
   }
@@ -243,10 +518,6 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
     return logs.filter(l => l.type !== 'THINKING')
   }
 
-  function toggleLogs(nodeId) {
-    expandedLogs[nodeId] = !expandedLogs[nodeId]
-  }
-
   function toggleContext(nodeId) {
     expandedContext[nodeId] = !expandedContext[nodeId]
   }
@@ -255,6 +526,10 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
     expandedThinking[nodeId] = !expandedThinking[nodeId]
   }
 
+  function toggleLogs(nodeId) {
+    expandedLogs[nodeId] = !expandedLogs[nodeId]
+  }
+
   /**
    * 格式化 contextSnapshot 中的值:
    * - 对象/数组:JSON.stringify(缩进 2)
@@ -272,25 +547,30 @@ export function useWorkflowRunner({ getNodes, nodeTypeInfo, workflowIdRef }) {
     showRunDialog,
     showResult,
     workflowResult,
+    runMeta,
     nodeStatuses,
     activeTab,
     nodeResults,
-    expandedLogs,
     expandedContext,
     expandedThinking,
+    expandedLogs,
     lastRunId,
     lastRunWorkflowId,
+    // v1.1:断线重连与 Askfor
+    lastEventId,
+    reconnecting,
+    pendingAsk,
     startRun,
+    submitClarify,
     openRunDialog,
     cancelRun,
     closeResult,
     statusText,
-    logTypeIcon,
     logTypeLabel,
     formatTimestamp,
-    toggleLogs,
     toggleContext,
     toggleThinking,
+    toggleLogs,
     formatContextValue
   }
 }

+ 103 - 0
frontend/src/utils/hermesThinking.js

@@ -0,0 +1,103 @@
+/**
+ * Hermes Agent 实时思考过程的颜文字过滤。
+ *
+ * 背景:Hermes 的 KawaiiSpinner 会给 thinking_callback 拼接前导颜文字与尾部省略号,
+ * 形如 `(◔_◔) pondering...`。在严肃业务场景下需要剥离这些装饰,仅保留动词本身。
+ *
+ * 数据来源:backend/hermes-agent/agent/display.py
+ *   - KAWAII_THINKING(15 个默认思考颜文字)
+ *   - 各皮肤(ares / poseidon / sisyphus / charizard)覆盖的思考颜文字
+ *   - KAWAII_WAITING(等待动画用的颜文字,作为兜底也一并剥离)
+ *
+ * 使用:在 SSE 处理 status 事件时调用 stripHermesThinkingDecoration(content)。
+ */
+
+// 默认 15 个思考颜文字(display.py: KAWAII_THINKING)
+const DEFAULT_THINKING_FACES = [
+  '(。•́︿•̀。)',
+  '(◔_◔)',
+  '(¬‿¬)',
+  '( •_•)>⌐■-■)',
+  '(⌐■_■)',
+  '(´・_・`)',
+  '◉_◉',
+  '(°ロ°)',
+  '( ˘⌣˘)♡',
+  'ヽ(>∀<☆)☆',
+  '٩(๑❛ᴗ❛๑)۶',
+  '(⊙_⊙)',
+  '(¬_¬)',
+  '( ͡° ͜ʖ ͡°)',
+  'ಠ_ಠ',
+]
+
+// 各皮肤覆盖的思考颜文字(display.py: skin_engine.py 中的 SKIN_THINKING_FACES)
+const SKIN_THINKING_FACES = [
+  // ares
+  '(⚔)', '(⛨)', '(▲)', '(⌁)', '(<>)',
+  // poseidon
+  '(Ψ)', '(∿)', '(≈)', '(◌)',
+  // sisyphus
+  '(◉)', '(◬)', '(○)', '(●)',
+  // charizard
+  '(✦)', '(◇)', '(🔥)',
+]
+
+// 等待动画用的颜文字(display.py: KAWAII_WAITING,作为兜底)
+const WAITING_FACES = [
+  '( ̄∇ ̄)', '( ̄ー ̄)', '(─‿─)', '(︶ω︶)', '( ̄︶ ̄)',
+  '( ︶。。)', `(˘。˘)`, `(•‿•)`, `(◕‿◕)`, `(✿◡‿◡)`,
+]
+
+// 合并去重,并按长度降序排列(优先匹配长串,避免前缀误判)
+const ALL_FACES = Array.from(new Set([
+  ...DEFAULT_THINKING_FACES,
+  ...SKIN_THINKING_FACES,
+  ...WAITING_FACES,
+])).sort((a, b) => b.length - a.length)
+
+/**
+ * 剥离 Hermes 实时思考过程文本中的装饰:
+ * 1. 反复剥离前导的已知颜文字(精确匹配)
+ * 2. 兜底剥离前导的圆括号包裹片段(如新增皮肤未及时登记时)
+ * 3. 剥离尾部省略号(中英文)
+ * 4. 去除首尾空白
+ *
+ * @param {string} text 原始文本,如 `(◔_◔) pondering...`
+ * @returns {string} 清洗后的文本,如 `pondering`
+ */
+export function stripHermesThinkingDecoration(text) {
+  if (!text) return ''
+  let s = String(text)
+
+  // 1. 反复剥离前导的已知颜文字(按长度降序匹配,避免短颜文字吃掉长颜文字的前缀)
+  let changed = true
+  while (changed) {
+    changed = false
+    for (const face of ALL_FACES) {
+      if (s.startsWith(face)) {
+        s = s.slice(face.length)
+        changed = true
+        break
+      }
+    }
+    // 去掉前导空白后再尝试下一轮(颜文字之间可能由空格分隔)
+    const trimmed = s.trimStart()
+    if (trimmed !== s) {
+      s = trimmed
+      changed = true
+    }
+  }
+
+  // 2. 兜底:剥离剩余的前导圆括号片段(如 `(xxx)` 或 `(xx)(yy)`)
+  //    覆盖未来皮肤新增颜文字未及时登记的场景
+  s = s.replace(/^(?:\([^)]*\)\s*)+\s*/, '')
+
+  // 3. 剥离尾部省略号(ASCII `...` 与中文 `……`、单点 `…`)
+  s = s.replace(/(?:\.{3,}|…+|……)+$/g, '')
+
+  return s.trim()
+}
+
+// 仅用于内部测试与调试(生产可忽略)
+export const __HERMES_FACES__ = ALL_FACES

+ 155 - 0
frontend/src/utils/runResultReplay.js

@@ -0,0 +1,155 @@
+/**
+ * 运行记录回放 utils:从持久化的 logs JSON 中还原运行时态数据。
+ *
+ * 后端 HermesBridgeClient.java 在 SSE 推送的同时,已将所有事件持久化到
+ * WorkflowRunNode.logs(JSON 数组)。前端运行记录页从这里还原:
+ *   - streamingThinking:拼接所有 THINKING 日志的 message
+ *   - toolHistory:从 TOOL_CALL / TOOL_RESULT 按时间顺序配对
+ *   - todos:取最后一条 TODO_UPDATE 的 detail(JSON 字符串),兼容数组 / {todos: [...]}
+ *   - nonThinkingLogs:过滤掉 THINKING 类型后的日志列表(用于执行日志面板)
+ *
+ * 这样运行记录页能完整复现编辑器"运行结果"Tab 中除"实时态"外的所有内容。
+ */
+
+/**
+ * 安全解析 JSON 字符串,失败返回 null。
+ */
+function parseJson(str) {
+  if (!str) return null
+  try { return JSON.parse(str) } catch { return null }
+}
+
+/**
+ * 从日志数组中提取思考过程(THINKING 类型日志的 message 拼接)。
+ * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
+ * @returns {string} 完整思考过程文本;无则返回空串
+ */
+export function extractStreamingThinking(logsJson) {
+  const logs = parseJson(logsJson)
+  if (!Array.isArray(logs)) return ''
+  return logs
+    .filter(l => l.type === 'THINKING')
+    .map(l => l.message || '')
+    .join('')
+}
+
+/**
+ * 从日志数组中还原工具调用历史(按时间顺序的 [{name, argsSummary, result}])。
+ *
+ * 配对规则:
+ *   - TOOL_CALL 日志:message=toolName,detail=argsSummary
+ *   - TOOL_RESULT 日志:message=toolName,detail=resultSummary
+ *   - 相邻的 TOOL_CALL → TOOL_RESULT 配对一个完整工具调用
+ *   - 若 TOOL_CALL 后跟另一个 TOOL_CALL(无 result),仍作为一条记录入栈,result=null
+ *
+ * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
+ * @returns {Array<{name:string, argsSummary:string, result:string|null}>}
+ */
+export function extractToolHistory(logsJson) {
+  const logs = parseJson(logsJson)
+  if (!Array.isArray(logs)) return []
+  const history = []
+  let pending = null  // 等待 result 的 tool_call
+  for (const log of logs) {
+    if (log.type === 'TOOL_CALL') {
+      // 上一个 tool_call 没 result,直接入栈
+      if (pending) {
+        history.push(pending)
+      }
+      pending = {
+        name: log.message || '(unknown)',
+        argsSummary: log.detail || '',
+        result: null,
+      }
+    } else if (log.type === 'TOOL_RESULT') {
+      if (pending) {
+        pending.result = log.detail || ''
+        history.push(pending)
+        pending = null
+      } else {
+        // 异常:tool_result 无对应 tool_call,作为孤儿记录
+        history.push({
+          name: log.message || '(unknown)',
+          argsSummary: '',
+          result: log.detail || '',
+        })
+      }
+    }
+  }
+  // 兜底:最后一个未配对的 tool_call
+  if (pending) {
+    history.push(pending)
+  }
+  // 编辑器实时展示是 unshift 顺序(最新在前),保持一致
+  return history.reverse()
+}
+
+/**
+ * 从日志数组中取最后一条 TODO_UPDATE,解析出 todos 数组。
+ * 兼容两种格式:
+ *   - 数组:[{content, status, ...}, ...]
+ *   - 对象:{todos: [...], summary: {...}}(Hermes 内置 todo_tool 实际返回结构)
+ *
+ * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
+ * @returns {Array} todos 数组;无则返回空数组
+ */
+export function extractLatestTodos(logsJson) {
+  const logs = parseJson(logsJson)
+  if (!Array.isArray(logs)) return []
+  // 从后往前找第一条 TODO_UPDATE
+  for (let i = logs.length - 1; i >= 0; i--) {
+    if (logs[i].type === 'TODO_UPDATE') {
+      const detail = logs[i].detail || ''
+      try {
+        const parsed = JSON.parse(detail)
+        const arr = Array.isArray(parsed) ? parsed : (parsed?.todos || [])
+        return Array.isArray(arr) ? arr : []
+      } catch { /* ignore */ }
+    }
+  }
+  return []
+}
+
+/**
+ * 过滤掉 THINKING 类型日志:思考过程已在 .nrc-thinking 集中展示,
+ * 执行日志面板再显示会造成内容重复。
+ * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
+ * @returns {Array} 非 THINKING 类型的日志数组
+ */
+export function extractNonThinkingLogs(logsJson) {
+  const logs = parseJson(logsJson)
+  if (!Array.isArray(logs)) return []
+  return logs.filter(l => l.type !== 'THINKING')
+}
+
+/**
+ * 将节点(来自后端 WorkflowRunNode)转换为编辑器 nodeResults 中的节点格式。
+ *
+ * 用于让运行记录页直接复用 AgentThinkingPanel 等组件:
+ *   - status: 后端返回的是大写(SUCCESS/FAILED/...),转换为小写以匹配组件判断
+ *   - streamingThinking / toolHistory / todos 从 logs 还原
+ *   - currentStatus / currentTool 留空(节点已完成,无实时态)
+ *
+ * @param {Object} node 后端 WorkflowRunNode(含 logs / output / contextSnapshot)
+ * @returns {Object} 编辑器节点格式
+ */
+export function buildReplayNode(node) {
+  const logsJson = node?.logs || ''
+  return {
+    nodeId: node?.nodeId,
+    type: node?.nodeType,
+    label: node?.label || node?.nodeId || '节点',
+    status: (node?.status || '').toLowerCase(),
+    output: node?.output ? parseJson(node.output) : null,
+    logs: extractNonThinkingLogs(logsJson),
+    contextSnapshot: node?.contextSnapshot ? parseJson(node.contextSnapshot) : null,
+    error: node?.error || '',
+    // 从持久化 logs 还原的运行时态
+    streamingThinking: extractStreamingThinking(logsJson),
+    toolHistory: extractToolHistory(logsJson),
+    todos: extractLatestTodos(logsJson),
+    // 已完成节点无实时态
+    currentStatus: '',
+    currentTool: null,
+  }
+}

+ 257 - 94
frontend/src/views/workflow/RunHistory.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { ref, reactive, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted } from 'vue'
 import { NEmpty, NSpin, NIcon, NPopconfirm, NButton, useMessage } from 'naive-ui'
 import { getWorkflowsWithRuns, getRunDetail, deleteRun } from '../../api/runHistory'
 import {
@@ -16,6 +16,9 @@ import {
   DownloadOutline
 } from '@vicons/ionicons5'
 import { unwrapNodeOutput, nodeOutputStatus, nodeOutputMessage } from '../../composables/useWorkflowRunner'
+import AgentThinkingPanel from '../../components/agent-thinking/AgentThinkingPanel.vue'
+import ThinkingLive from '../../components/agent-thinking/ThinkingLive.vue'
+import { buildReplayNode } from '../../utils/runResultReplay'
 
 const message = useMessage()
 
@@ -25,7 +28,7 @@ const selectedRun = ref(null)
 const runDetail = ref(null) // { run, nodes }
 const expandedLogs = reactive({})
 const expandedContext = reactive({})
-/** 思考过程折叠状态:key=nodeId,未设置默认展开 */
+/** 思考过程折叠状态:key=nodeId,未设置默认收起(与 WorkflowEditor 保持一致) */
 const expandedThinking = reactive({})
 /** 折叠的智能体(workflowId 集合),默认全部展开 */
 const collapsedWfs = reactive({})
@@ -44,6 +47,43 @@ const nodeTypeInfo = {
   output:      { color: '#EF4444', typeLabel: '输出',     icon: ExitOutline }
 }
 
+/**
+ * 把后端 WorkflowRunNode 列表一次性转为编辑器节点格式(含 streamingThinking / toolHistory / todos / logs)。
+ * 用 computed 缓存,避免模板中每次访问都重新解析 logs JSON。
+ */
+const replayNodes = computed(() => {
+  const nodes = runDetail.value?.nodes
+  if (!Array.isArray(nodes)) return []
+  return nodes.map(n => {
+    const r = buildReplayNode(n)
+    // 补充模板需要的元信息(颜色、图标引用、类型标签),便于直接 v-for 渲染
+    const info = nodeTypeInfo[n.nodeType] || {}
+    r.color = info.color || '#666'
+    r.icon = info.icon || PlayCircleOutline
+    r.typeLabel = info.typeLabel || n.nodeType
+    // 保留原始 output / contextSnapshot 字符串供 parseJson 复用(envelope 判定等)
+    r._rawOutput = n.output || ''
+    r._rawContextSnapshot = n.contextSnapshot || ''
+    return r
+  })
+})
+
+/**
+ * 当前运行的元数据 { runId, workingDirFiles }。
+ * 重构后这两个字段不再注入 output 节点 envelope,而是作为 workflow-run 的属性持久化:
+ *   - runId:直接读 runDetail.run.runId(8 位 UUID 字符串)
+ *   - workingDirFiles:从 runDetail.run.workingDirFiles JSON 字符串解析为 List<String>
+ */
+const outputMeta = computed(() => {
+  const run = runDetail.value?.run
+  if (!run) return { runId: null, workingDirFiles: null }
+  const filesRaw = parseJson(run.workingDirFiles)
+  return {
+    runId: run.runId || null,
+    workingDirFiles: Array.isArray(filesRaw) ? filesRaw : null,
+  }
+})
+
 onMounted(loadData)
 
 async function loadData() {
@@ -143,33 +183,18 @@ function formatContextValue(value) {
   return String(value)
 }
 
-function logTypeIcon(type) {
-  return { TOOL_CALL: '🔧', TOOL_RESULT: '📋', THINKING: '💭', ERROR: '❌', INFO: 'ℹ️' }[type] || '•'
-}
-
 function logTypeLabel(type) {
   return { TOOL_CALL: '调用工具', TOOL_RESULT: '工具返回', THINKING: '思考', ERROR: '错误', INFO: '信息' }[type] || type
 }
 
 /**
- * 从日志数组中提取思考过程内容(THINKING 类型日志)。
- * 工作流编辑器的运行结果是实时流式累加,运行记录页则从持久化日志中还原。
- */
-function extractThinking(logsJson) {
-  const logs = parseJson(logsJson)
-  if (!Array.isArray(logs)) return null
-  const thinking = logs.filter(l => l.type === 'THINKING').map(l => l.message || '').join('')
-  return thinking || null
-}
-
-/**
- * 过滤掉 THINKING 类型日志:思考过程已在节点卡片上方集中展示,
- * 列表展开时再显示会造成内容重复(流式 token 拼接的多条 message 与上方完整思考重复)。
+ * 格式化日志时间戳为 HH:mm:ss。
+ * 与 useWorkflowRunner.js 中 formatTimestamp 实现保持一致。
  */
-function nonThinkingLogs(logsJson) {
-  const logs = parseJson(logsJson)
-  if (!Array.isArray(logs)) return []
-  return logs.filter(l => l.type !== 'THINKING')
+function formatTimestamp(ts) {
+  if (!ts) return ''
+  const d = new Date(ts)
+  return d.toLocaleTimeString('zh-CN', { hour12: false })
 }
 
 /**
@@ -267,70 +292,102 @@ function downloadWorkspace(run) {
           <!-- 节点结果列表 -->
           <div class="detail-nodes">
             <div
-              v-for="node in runDetail.nodes"
-              :key="node.id"
+              v-for="r in replayNodes"
+              :key="r.nodeId"
               class="node-result-card"
-              :style="{ borderLeftColor: nodeTypeInfo[node.nodeType]?.color || '#666' }"
+              :style="{ borderLeftColor: r.color }"
             >
-              <div class="nrc-header" @click="toggleLogs(node.nodeId)" style="cursor: pointer">
-                <div class="nrc-icon" :style="{ background: nodeTypeInfo[node.nodeType]?.color || '#666' }">
-                  <n-icon size="12"><component :is="nodeTypeInfo[node.nodeType]?.icon || PlayCircleOutline" /></n-icon>
+              <div class="nrc-header">
+                <div class="nrc-icon" :style="{ background: r.color }">
+                  <n-icon size="12"><component :is="r.icon" /></n-icon>
                 </div>
                 <div class="nrc-info">
-                  <span class="nrc-label">{{ node.label || node.nodeId }}</span>
-                  <span class="nrc-type">{{ nodeTypeInfo[node.nodeType]?.typeLabel || node.nodeType }}</span>
+                  <span class="nrc-label">{{ r.label }}</span>
+                  <span class="nrc-type">{{ r.typeLabel }}</span>
+                </div>
+                <span class="nrc-status" :class="statusClass(r.status)">{{ statusText(r.status) }}</span>
+              </div>
+              <!-- 实时状态位(节点已完成,currentStatus 为空,组件自动隐藏) -->
+              <ThinkingLive :status="r.currentStatus || ''" />
+
+              <!-- 流式思考过程(从持久化 THINKING 日志还原)
+                   运行记录页节点均为完成态,由 expandedThinking 控制(默认折叠) -->
+              <div v-if="r.streamingThinking" class="nrc-thinking">
+                <div class="nrc-thinking-header" @click="toggleThinking(r.nodeId)" style="cursor: pointer">
+                  <span class="nrc-thinking-tag">思考过程</span>
+                  <span class="nrc-thinking-static">({{ r.streamingThinking.length }} 字)</span>
+                  <span class="nrc-thinking-expand" :class="{ expanded: expandedThinking[r.nodeId] === true }">▶</span>
+                </div>
+                <pre v-show="expandedThinking[r.nodeId] === true"
+                  class="nrc-thinking-body">{{ r.streamingThinking }}</pre>
+              </div>
+
+              <!-- Hermes 工具调用 / 待办清单 / Askfor(运行记录页无 pendingAsk) -->
+              <AgentThinkingPanel
+                v-if="r.toolHistory.length || (r.todos || []).length"
+                :node="r"
+              />
+
+              <!-- 执行日志(与"思考过程"/"工具调用"同款框)
+                   默认折叠;用户点击 header 可手动切换展开/收起。 -->
+              <div v-if="r.logs.length" class="nrc-logs">
+                <div class="nrc-logs-header" @click="toggleLogs(r.nodeId)" style="cursor: pointer">
+                  <span class="nrc-logs-tag">执行日志</span>
+                  <span class="nrc-logs-count">({{ r.logs.length }})</span>
+                  <span class="nrc-logs-expand" :class="{ expanded: expandedLogs[r.nodeId] === true }">▶</span>
+                </div>
+                <div v-show="expandedLogs[r.nodeId] === true" class="nrc-logs-body">
+                  <div class="nrc-log" v-for="(log, idx) in r.logs" :key="idx"
+                    :class="'log-type-' + (log.type || 'info').toLowerCase()">
+                    <span class="nrc-log-time">{{ formatTimestamp(log.timestamp) }}</span>
+                    <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
+                    <span class="nrc-log-msg">{{ log.message }}</span>
+                    <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
+                  </div>
                 </div>
-                <span v-if="node.logs && nonThinkingLogs(node.logs).length" class="nrc-log-count">{{ nonThinkingLogs(node.logs).length }} 条日志</span>
-                <span class="nrc-status" :class="statusClass(node.status)">{{ statusText(node.status) }}</span>
-                <span v-if="node.logs && nonThinkingLogs(node.logs).length" class="nrc-expand" :class="{ expanded: expandedLogs[node.nodeId] }">▶</span>
               </div>
-              <div class="nrc-vars" v-if="parseJson(node.output)">
+
+              <div class="nrc-vars" v-if="parseJson(r._rawOutput)">
                 <!-- envelope 失败(status=400)时显示红色徽章 + message -->
-                <div v-if="nodeOutputStatus(parseJson(node.output)) === 400" class="nrc-env-fail">
+                <div v-if="nodeOutputStatus(parseJson(r._rawOutput)) === 400" class="nrc-env-fail">
                   <span class="nrc-env-badge failed">失败</span>
-                  <span class="nrc-env-msg">{{ nodeOutputMessage(parseJson(node.output)) }}</span>
+                  <span class="nrc-env-msg">{{ nodeOutputMessage(parseJson(r._rawOutput)) }}</span>
                 </div>
-                <template v-if="Object.keys(unwrapNodeOutput(parseJson(node.output))).length">
-                  <div class="nrc-var" v-for="(value, key) in unwrapNodeOutput(parseJson(node.output))" :key="key">
-                    <span class="nrc-var-key" :style="{ color: nodeTypeInfo[node.nodeType]?.color || '#666' }">{{ key }}</span>
+                <template v-if="Object.keys(unwrapNodeOutput(parseJson(r._rawOutput))).length">
+                  <div class="nrc-var" v-for="(value, key) in unwrapNodeOutput(parseJson(r._rawOutput))" :key="key">
+                    <span class="nrc-var-key" :style="{ color: r.color }">{{ key }}</span>
                     <span class="nrc-var-value">{{ typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) }}</span>
                   </div>
                 </template>
               </div>
-              <!-- 思考过程(从持久化日志中提取并集中展示),支持折叠 -->
-              <div v-if="extractThinking(node.logs)" class="nrc-thinking">
-                <div class="nrc-thinking-header" @click.stop="toggleThinking(node.nodeId)" style="cursor: pointer">
-                  <span class="nrc-thinking-tag">💭 思考过程</span>
-                  <span class="nrc-thinking-static">{{ extractThinking(node.logs).length }} 字</span>
-                  <span class="nrc-thinking-expand" :class="{ expanded: expandedThinking[node.nodeId] !== false }">▶</span>
-                </div>
-                <pre v-show="expandedThinking[node.nodeId] !== false" class="nrc-thinking-body">{{ extractThinking(node.logs) }}</pre>
-              </div>
               <!-- 完整上下文快照(节点执行后的全部 variables,debug 视图) -->
-              <div v-if="parseJson(node.contextSnapshot)" class="nrc-ctx-toggle" @click.stop="toggleContext(node.nodeId)">
-                <span class="nrc-ctx-icon" :class="{ expanded: expandedContext[node.nodeId] }">▶</span>
-                <span class="nrc-ctx-label">查看完整上下文({{ Object.keys(parseJson(node.contextSnapshot)).length }} 个变量)</span>
+              <div v-if="parseJson(r._rawContextSnapshot)" class="nrc-ctx-toggle" @click="toggleContext(r.nodeId)">
+                <span class="nrc-ctx-icon" :class="{ expanded: expandedContext[r.nodeId] }">▶</span>
+                <span class="nrc-ctx-label">查看完整上下文({{ Object.keys(parseJson(r._rawContextSnapshot)).length }} 个变量)</span>
               </div>
-              <div v-if="expandedContext[node.nodeId] && parseJson(node.contextSnapshot)" class="nrc-ctx-panel">
-                <div class="nrc-ctx-var" v-for="(value, key) in parseJson(node.contextSnapshot)" :key="key">
+              <div v-if="expandedContext[r.nodeId] && parseJson(r._rawContextSnapshot)" class="nrc-ctx-panel">
+                <div class="nrc-ctx-var" v-for="(value, key) in parseJson(r._rawContextSnapshot)" :key="key">
                   <span class="nrc-ctx-key">{{ key }}</span>
                   <span class="nrc-ctx-value">{{ formatContextValue(value) }}</span>
                 </div>
               </div>
-              <div v-if="node.error" class="nrc-error">{{ node.error }}</div>
-              <div v-if="expandedLogs[node.nodeId] && nonThinkingLogs(node.logs).length" class="nrc-logs">
-                <div class="nrc-log" v-for="(log, idx) in nonThinkingLogs(node.logs)" :key="idx" :class="'log-type-' + (log.type || 'info').toLowerCase()">
-                  <span class="nrc-log-icon">{{ logTypeIcon(log.type) }}</span>
-                  <span class="nrc-log-msg">{{ log.message }}</span>
-                  <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
-                  <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
-                </div>
-              </div>
+              <div v-if="r.error" class="nrc-error">{{ r.error }}</div>
             </div>
           </div>
 
           <!-- 工作空间下载:下载本次运行的工作目录 zip(应用 .agentignore 过滤) -->
           <div class="detail-footer">
+            <!-- 元数据:运行ID + 工作空间文件列表,与编辑器 .nrc-clear-row 出现时机一致 -->
+            <div v-if="outputMeta.runId" class="run-meta-id">
+              <span class="run-meta-label">运行ID:</span>
+              <code class="run-meta-value">{{ outputMeta.runId }}</code>
+            </div>
+            <details v-if="outputMeta.workingDirFiles" class="run-meta-files">
+              <summary>工作空间文件列表({{ outputMeta.workingDirFiles.length }})</summary>
+              <ul class="run-meta-files-list">
+                <li v-for="f in outputMeta.workingDirFiles" :key="f">{{ f }}</li>
+              </ul>
+            </details>
             <n-button quaternary size="small" @click="downloadWorkspace(runDetail.run)">
               <template #icon>
                 <n-icon size="14"><DownloadOutline /></n-icon>
@@ -544,12 +601,84 @@ function downloadWorkspace(run) {
 
 .detail-footer {
   display: flex;
-  justify-content: center;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
   padding: 16px 0 4px;
   margin-top: 12px;
   border-top: 1px dashed var(--border-color, rgba(255,255,255,0.08));
 }
 
+/* 元数据:运行ID */
+.run-meta-id {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 12px;
+  color: var(--text-secondary, #aaa);
+}
+
+.run-meta-label {
+  color: var(--text-secondary, #888);
+}
+
+.run-meta-value {
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+  font-size: 11px;
+  color: var(--primary, #c084fc);
+  background: rgba(168, 85, 247, 0.08);
+  padding: 1px 6px;
+  border-radius: 3px;
+}
+
+/* 元数据:工作空间文件列表(<details> 折叠) */
+.run-meta-files {
+  width: 100%;
+  max-width: 640px;
+  font-size: 12px;
+  background: rgba(255, 255, 255, 0.02);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  border-radius: 4px;
+  padding: 4px 10px;
+}
+
+.run-meta-files > summary {
+  cursor: pointer;
+  user-select: none;
+  color: var(--primary, #c084fc);
+  list-style: none;
+}
+
+.run-meta-files > summary::-webkit-details-marker {
+  display: none;
+}
+
+.run-meta-files > summary::before {
+  content: '▸';
+  display: inline-block;
+  margin-right: 6px;
+  transition: transform 0.15s;
+}
+
+.run-meta-files[open] > summary::before {
+  transform: rotate(90deg);
+}
+
+.run-meta-files-list {
+  margin: 6px 0 2px;
+  padding-left: 20px;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+  font-size: 11px;
+  color: var(--text-secondary, #ccc);
+  max-height: 220px;
+  overflow-y: auto;
+}
+
+.run-meta-files-list li {
+  line-height: 1.6;
+  word-break: break-all;
+}
+
 .detail-header {
   margin-bottom: 16px;
   padding-bottom: 12px;
@@ -646,24 +775,6 @@ function downloadWorkspace(run) {
 .nrc-status.status-failed    { background: rgba(239,68,68,0.15);  color: #f87171; }
 .nrc-status.status-skipped   { background: rgba(107,114,128,0.15); color: #9ca3af; }
 
-.nrc-log-count {
-  font-size: 10px;
-  padding: 2px 6px;
-  border-radius: 4px;
-  background: rgba(99,102,241,0.1);
-  color: #818cf8;
-  flex-shrink: 0;
-}
-
-.nrc-expand {
-  font-size: 10px;
-  color: var(--text-tertiary, #666);
-  transition: transform 0.2s;
-  flex-shrink: 0;
-}
-
-.nrc-expand.expanded { transform: rotate(90deg); }
-
 .nrc-vars { padding: 6px 10px; }
 
 /* envelope 失败提示条:status=400 时显示在变量列表顶部 */
@@ -722,13 +833,15 @@ function downloadWorkspace(run) {
   overflow-y: auto;
 }
 
-/* 思考过程展示(与 WorkflowEditor 实时思考区样式保持一致) */
+/* 流式思考过程区块(与 WorkflowEditor 保持一致:紫色框 + 等宽字体) */
 .nrc-thinking {
   margin: 6px 10px;
   border-radius: 6px;
   background: rgba(168, 85, 247, 0.05);
   border: 1px solid rgba(168, 85, 247, 0.2);
   overflow: hidden;
+  /* 与 .tool-calls-wrap / .todo-panel / .nrc-logs 保持一致的等宽字体 */
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
 }
 
 .nrc-thinking-header {
@@ -746,7 +859,7 @@ function downloadWorkspace(run) {
 }
 
 .nrc-thinking-static {
-  color: var(--text-tertiary, #888);
+  color: #888;
   font-size: 10px;
 }
 
@@ -766,11 +879,12 @@ function downloadWorkspace(run) {
   padding: 8px 12px;
   max-height: 320px;
   overflow: auto;
-  font-family: 'Cascadia Code', 'Fira Code', monospace;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
   font-size: 11px;
-  color: var(--text-secondary, #ccc);
+  line-height: 1.6;
+  color: var(--text-secondary, #b0b0b0);
   white-space: pre-wrap;
-  word-break: break-all;
+  word-break: break-word;
 }
 
 .nrc-error {
@@ -840,22 +954,69 @@ function downloadWorkspace(run) {
 }
 
 .nrc-logs {
-  border-top: 1px solid rgba(255,255,255,0.04);
-  padding: 6px 10px;
-  max-height: 400px;
+  margin: 6px 10px;
+  border-radius: 6px;
+  background: rgba(168, 85, 247, 0.05);
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  overflow: hidden;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+}
+
+/* 日志区 header:与 .nrc-thinking-header 视觉一致 */
+.nrc-logs-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 10px;
+  font-size: 11px;
+  background: rgba(168, 85, 247, 0.08);
+  cursor: pointer;
+  user-select: none;
+}
+
+.nrc-logs-tag {
+  color: #c084fc;
+  font-weight: 600;
+}
+
+.nrc-logs-count {
+  color: #888;
+  font-size: 10px;
+}
+
+/* 展开箭头:与 .nrc-thinking-expand 一致 */
+.nrc-logs-expand {
+  margin-left: auto;
+  font-size: 10px;
+  color: var(--text-tertiary, #666);
+  transition: transform 0.2s;
+  flex-shrink: 0;
+}
+
+.nrc-logs-expand.expanded {
+  transform: rotate(90deg);
+}
+
+.nrc-logs-body {
+  max-height: 360px;
   overflow-y: auto;
 }
 
 .nrc-log {
-  padding: 4px 0;
-  border-bottom: 1px solid rgba(255,255,255,0.02);
+  padding: 5px 10px;
+  border-bottom: 1px solid rgba(168, 85, 247, 0.1);
   font-size: 11px;
   line-height: 1.5;
 }
 
 .nrc-log:last-child { border-bottom: none; }
 
-.nrc-log-icon { margin-right: 4px; }
+.nrc-log-time {
+  font-family: inherit;
+  font-size: 10px;
+  color: var(--text-tertiary, #555);
+  margin-right: 6px;
+}
 
 .nrc-log-tag {
   font-size: 10px;
@@ -873,13 +1034,15 @@ function downloadWorkspace(run) {
 .log-type-info .nrc-log-tag { background: rgba(107,114,128,0.15); color: #9ca3af; }
 
 .nrc-log-msg {
+  display: block;
+  margin-top: 2px;
   color: var(--text-secondary, #ccc);
   word-break: break-all;
+  white-space: pre-wrap;
 }
 
 .nrc-log-detail {
   margin-top: 3px;
-  margin-left: 22px;
   font-family: 'Cascadia Code', 'Fira Code', monospace;
   font-size: 10px;
   color: var(--text-tertiary, #888);

+ 183 - 45
frontend/src/views/workflow/WorkflowEditor.vue

@@ -32,6 +32,8 @@ import ConditionNode from '../../components/workflow/nodes/ConditionNode.vue'
 import SmartActionNode from '../../components/workflow/nodes/SmartActionNode.vue'
 import VirtualGroupNode from '../../components/workflow/nodes/VirtualGroupNode.vue'
 import CodeEditor from '../../components/skill/CodeEditor.vue'
+import AgentThinkingPanel from '../../components/agent-thinking/AgentThinkingPanel.vue'
+import ThinkingLive from '../../components/agent-thinking/ThinkingLive.vue'
 import { inferNewEdge, refreshMappingsForNode, getNodeOutputs, getNodeInputs, getReachablePredecessors, getReachableSuccessors, IO_TYPES } from '../../utils/ioInference'
 import { useVersionHistory } from '../../composables/useVersionHistory'
 import { useWorkflowRunner, unwrapNodeOutput, nodeOutputStatus, nodeOutputMessage } from '../../composables/useWorkflowRunner'
@@ -129,12 +131,14 @@ const {
 
 // ========== 工作流运行状态 ==========
 const {
-  running, showRunDialog, showResult, workflowResult,
-  nodeStatuses, activeTab, nodeResults, expandedLogs, expandedContext, expandedThinking,
+  running, showRunDialog, showResult, workflowResult, runMeta,
+  nodeStatuses, activeTab, nodeResults, expandedContext, expandedThinking, expandedLogs,
   lastRunId, lastRunWorkflowId,
-  startRun, openRunDialog, cancelRun, closeResult,
-  statusText, logTypeIcon, logTypeLabel, formatTimestamp, toggleLogs,
-  toggleContext, toggleThinking, formatContextValue
+  // v1.1:Askfor 与断线重连
+  pendingAsk, reconnecting,
+  startRun, submitClarify, openRunDialog, cancelRun, closeResult,
+  statusText, logTypeLabel, formatTimestamp,
+  toggleContext, toggleThinking, toggleLogs, formatContextValue
 } = useWorkflowRunner({
   getNodes,
   nodeTypeInfo: {
@@ -2275,7 +2279,7 @@ function applyGraphData(graphData) {
             <div v-if="nodeResults.length === 0" class="empty-hint">暂无运行结果</div>
             <div v-for="r in nodeResults" :key="r.nodeId" :id="'node-result-' + r.nodeId" class="node-result-card"
               :class="{ highlighted: highlightedNodeId === r.nodeId }" :style="{ borderLeftColor: r.color }">
-              <div class="nrc-header" @click="toggleLogs(r.nodeId)" style="cursor: pointer">
+              <div class="nrc-header">
                 <div class="nrc-icon" :style="{ background: r.color }">
                   <n-icon size="12">
                     <component :is="nodeTypeInfo[r.type]?.icon" />
@@ -2285,23 +2289,56 @@ function applyGraphData(graphData) {
                   <span class="nrc-label">{{ r.label }}</span>
                   <span class="nrc-type">{{ r.typeLabel }}</span>
                 </div>
-                <span v-if="r.logs && r.logs.length" class="nrc-log-count">{{ r.logs.length }} 条日志</span>
                 <span class="nrc-status" :class="'status-' + r.status">{{ statusText(r.status) }}</span>
-                <span v-if="r.logs && r.logs.length" class="nrc-expand"
-                  :class="{ expanded: expandedLogs[r.nodeId] }">▶</span>
               </div>
+              <!-- v1.1:实时状态(覆盖式,节点完成后由 useWorkflowRunner 清空,自动隐藏)
+                   独立放在流式思考过程之上,便于一眼看到当前节点正在做什么 -->
+              <ThinkingLive :status="r.currentStatus || ''" />
+
               <!-- 流式思考过程(实时累加,节点完成后保留为历史)
+                   节点 running 时强制展开;非 running 时由 expandedThinking 控制(默认折叠)。
                    折叠时仅隐藏 body,streamingThinking 仍持续累加,展开后可继续输出 -->
               <div v-if="r.streamingThinking" class="nrc-thinking" :class="{ live: r.status === 'running' }">
                 <div class="nrc-thinking-header" @click="toggleThinking(r.nodeId)" style="cursor: pointer">
-                  <span class="nrc-thinking-tag">💭 思考过程</span>
+                  <span class="nrc-thinking-tag">思考过程</span>
                   <span v-if="r.status === 'running'" class="nrc-thinking-live">实时输出中…</span>
-                  <span v-else class="nrc-thinking-static">{{ r.streamingThinking.length }} 字</span>
-                  <span class="nrc-thinking-expand" :class="{ expanded: expandedThinking[r.nodeId] !== false }">▶</span>
+                  <span v-else class="nrc-thinking-static">({{ r.streamingThinking.length }} 字)</span>
+                  <span class="nrc-thinking-expand" :class="{ expanded: r.status === 'running' || expandedThinking[r.nodeId] === true }">▶</span>
                 </div>
-                <pre v-show="expandedThinking[r.nodeId] !== false"
+                <pre v-show="r.status === 'running' || expandedThinking[r.nodeId] === true"
                   class="nrc-thinking-body">{{ r.streamingThinking }}</pre>
               </div>
+
+              <!-- v1.1:Hermes 工具调用 / TODO / Askfor
+                   仅当节点产生了任一 v1.1 字段时渲染,避免对老节点造成空 UI。
+                   Askfor 额外要求 nodeId 匹配 —— 只在触发 clarify 的那个节点上展示选项,
+                   其他节点(如 userInput)即便 pendingAsk 全局非空也不应渲染。 -->
+              <AgentThinkingPanel
+                v-if="r.currentTool || (r.toolHistory && r.toolHistory.length) || (r.todos && r.todos.length) || (pendingAsk && pendingAsk.nodeId === r.nodeId)"
+                :node="r"
+                :pending-ask="pendingAsk"
+                :reconnecting="reconnecting"
+                @submit-clarify="submitClarify"
+              />
+              <!-- 执行日志:与"思考过程"/"工具调用"同款框,置于工具调用下方、最终结果上方
+                   节点 running 时强制展开;完成后默认收起(expandedLogs 清空)。
+                   用户点击 header 可手动切换展开/收起。 -->
+              <div v-if="r.logs && r.logs.length" class="nrc-logs" :class="{ live: r.status === 'running' }">
+                <div class="nrc-logs-header" @click="toggleLogs(r.nodeId)" style="cursor: pointer">
+                  <span class="nrc-logs-tag">执行日志</span>
+                  <span class="nrc-logs-count">({{ r.logs.length }})</span>
+                  <span class="nrc-logs-expand" :class="{ expanded: r.status === 'running' || expandedLogs[r.nodeId] === true }">▶</span>
+                </div>
+                <div v-show="r.status === 'running' || expandedLogs[r.nodeId] === true" class="nrc-logs-body">
+                  <div class="nrc-log" v-for="(log, idx) in r.logs" :key="idx"
+                    :class="'log-type-' + (log.type || 'info').toLowerCase()">
+                    <span class="nrc-log-time">{{ formatTimestamp(log.timestamp) }}</span>
+                    <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
+                    <span class="nrc-log-msg">{{ log.message }}</span>
+                    <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
+                  </div>
+                </div>
+              </div>
               <div class="nrc-vars" v-if="unwrapNodeOutput(r.output) && Object.keys(unwrapNodeOutput(r.output)).length">
                 <!-- envelope 失败(status=400)时显示红色徽章 + message -->
                 <div v-if="nodeOutputStatus(r.output) === 400" class="nrc-env-fail">
@@ -2326,19 +2363,19 @@ function applyGraphData(graphData) {
                   <span class="nrc-ctx-value">{{ formatContextValue(value) }}</span>
                 </div>
               </div>
-              <!-- 执行日志 -->
-              <div v-if="expandedLogs[r.nodeId] && r.logs && r.logs.length" class="nrc-logs">
-                <div class="nrc-log" v-for="(log, idx) in r.logs" :key="idx"
-                  :class="'log-type-' + (log.type || 'info').toLowerCase()">
-                  <span class="nrc-log-icon">{{ logTypeIcon(log.type) }}</span>
-                  <span class="nrc-log-time">{{ formatTimestamp(log.timestamp) }}</span>
-                  <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
-                  <span class="nrc-log-msg">{{ log.message }}</span>
-                  <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
-                </div>
-              </div>
             </div>
             <div v-if="showResult && workflowResult" class="nrc-clear-row">
+              <!-- 元数据:运行ID + 工作空间文件列表,作为 workflow-run 的属性由 SSE 推送,与"下载工作目录"出现时机一致 -->
+              <div v-if="runMeta?.runId" class="run-meta-id">
+                <span class="run-meta-label">运行ID:</span>
+                <code class="run-meta-value">{{ runMeta.runId }}</code>
+              </div>
+              <details v-if="runMeta?.workingDirFiles" class="run-meta-files">
+                <summary>工作空间文件列表({{ runMeta.workingDirFiles.length }})</summary>
+                <ul class="run-meta-files-list">
+                  <li v-for="f in runMeta.workingDirFiles" :key="f">{{ f }}</li>
+                </ul>
+              </details>
               <a v-if="lastRunId && lastRunWorkflowId"
                 :href="`/api/workflows/${lastRunWorkflowId}/runs/${lastRunId}/download`" class="text-btn"
                 style="text-decoration:none; color: var(--primary)">下载工作目录</a>
@@ -2967,6 +3004,8 @@ function applyGraphData(graphData) {
   background: rgba(168, 85, 247, 0.05);
   border: 1px solid rgba(168, 85, 247, 0.2);
   overflow: hidden;
+  /* 与 .tool-calls-wrap / .todo-panel / .nrc-logs 保持一致的等宽字体 */
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
 }
 
 .nrc-thinking.live {
@@ -3133,42 +3172,143 @@ function applyGraphData(graphData) {
 }
 
 .nrc-clear-row {
-  text-align: center;
+  /* 元数据 + 操作按钮纵向堆叠,整体居中 */
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
   padding: 10px 0 4px;
   border-top: 1px solid rgba(255, 255, 255, 0.06);
   margin-top: 8px;
 }
 
-.nrc-log-count {
-  font-size: 10px;
-  padding: 2px 6px;
+/* 元数据:运行ID */
+.run-meta-id {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 12px;
+  color: var(--text-secondary, #aaa);
+}
+
+.run-meta-label {
+  color: var(--text-secondary, #888);
+}
+
+.run-meta-value {
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+  font-size: 11px;
+  color: var(--primary, #c084fc);
+  background: rgba(168, 85, 247, 0.08);
+  padding: 1px 6px;
+  border-radius: 3px;
+}
+
+/* 元数据:工作空间文件列表(<details> 折叠) */
+.run-meta-files {
+  width: 100%;
+  max-width: 640px;
+  font-size: 12px;
+  background: rgba(255, 255, 255, 0.02);
+  border: 1px solid rgba(255, 255, 255, 0.06);
   border-radius: 4px;
-  background: rgba(99, 102, 241, 0.1);
-  color: #818cf8;
-  flex-shrink: 0;
+  padding: 4px 10px;
 }
 
-.nrc-expand {
+.run-meta-files > summary {
+  cursor: pointer;
+  user-select: none;
+  color: var(--primary, #c084fc);
+  list-style: none;
+}
+
+.run-meta-files > summary::-webkit-details-marker {
+  display: none;
+}
+
+.run-meta-files > summary::before {
+  content: '▸';
+  display: inline-block;
+  margin-right: 6px;
+  transition: transform 0.15s;
+}
+
+.run-meta-files[open] > summary::before {
+  transform: rotate(90deg);
+}
+
+.run-meta-files-list {
+  margin: 6px 0 2px;
+  padding-left: 20px;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+  font-size: 11px;
+  color: var(--text-secondary, #ccc);
+  max-height: 220px;
+  overflow-y: auto;
+}
+
+.run-meta-files-list li {
+  line-height: 1.6;
+  word-break: break-all;
+}
+
+.nrc-logs {
+  margin: 6px 10px;
+  border-radius: 6px;
+  background: rgba(168, 85, 247, 0.05);
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  overflow: hidden;
+  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
+}
+
+.nrc-logs.live {
+  border-color: rgba(168, 85, 247, 0.5);
+  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
+}
+
+/* 日志区 header:与 .nrc-thinking-header 视觉一致 */
+.nrc-logs-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 10px;
+  font-size: 11px;
+  background: rgba(168, 85, 247, 0.08);
+  cursor: pointer;
+  user-select: none;
+}
+
+.nrc-logs-tag {
+  color: #c084fc;
+  font-weight: 600;
+}
+
+.nrc-logs-count {
+  color: #888;
+  font-size: 10px;
+}
+
+/* 展开箭头:与 .nrc-thinking-expand 一致 */
+.nrc-logs-expand {
+  margin-left: auto;
   font-size: 10px;
   color: var(--text-tertiary, #666);
   transition: transform 0.2s;
   flex-shrink: 0;
 }
 
-.nrc-expand.expanded {
+.nrc-logs-expand.expanded {
   transform: rotate(90deg);
 }
 
-.nrc-logs {
-  border-top: 1px solid rgba(255, 255, 255, 0.04);
-  padding: 6px 10px;
-  max-height: 400px;
+.nrc-logs-body {
+  max-height: 360px;
   overflow-y: auto;
 }
 
 .nrc-log {
-  padding: 4px 0;
-  border-bottom: 1px solid rgba(255, 255, 255, 0.02);
+  padding: 5px 10px;
+  border-bottom: 1px solid rgba(168, 85, 247, 0.1);
   font-size: 11px;
   line-height: 1.5;
 }
@@ -3177,12 +3317,8 @@ function applyGraphData(graphData) {
   border-bottom: none;
 }
 
-.nrc-log-icon {
-  margin-right: 4px;
-}
-
 .nrc-log-time {
-  font-family: 'Cascadia Code', 'Fira Code', monospace;
+  font-family: inherit;
   font-size: 10px;
   color: var(--text-tertiary, #555);
   margin-right: 6px;
@@ -3223,13 +3359,15 @@ function applyGraphData(graphData) {
 }
 
 .nrc-log-msg {
+  display: block;
+  margin-top: 2px;
   color: var(--text-secondary, #ccc);
   word-break: break-all;
+  white-space: pre-wrap;
 }
 
 .nrc-log-detail {
   margin-top: 3px;
-  margin-left: 22px;
   font-family: 'Cascadia Code', 'Fira Code', monospace;
   font-size: 10px;
   color: var(--text-tertiary, #888);

+ 144 - 0
prompt.md

@@ -1471,3 +1471,147 @@ docs/rag 下的也根据分类放到那几个目录下,文件名称统一。
 
 ---
 
+探索 https://szrh.cetccity.com:10503/automation 页面。如需登录,使用手机号“18810576024”和密码“Wsj4126872”。重点关注创建智能体时要填的表单项、创建工作流时要填的表单项、工作流中的节点类型、各节点要填的表单项等内容。形成文档,放到 docs/reference 下。
+
+---
+
+将上述截图放到 temp 目录下;然后调研 dify 目录,针对其工作流中的节点类型、各节点要填的表单项等内容。形成文档,放到 docs/reference 下。
+
+---
+
+jizhi-platform-reference.md 中,具体写明所有工作流节点的参数,不要省略。
+
+---
+
+/plan 接下来,我要对智能操作、技能的思考过程展示进行较大规模的改动。目前而言,框架基于Hermes进行agent操作。我需要提取Hermes输出中的以下内容进行展示:
+1. 追加型思考过程(已有)
+2. 实时型思考过程(不断变化,例如正在进行XXX、下一步:XXX等内容)
+3. 工具调用(上方一个区域显示当前正在调用的工具(若有),下方一个区域显示已调用过的工具记录)
+4. TODO List(显示当前智能体规划的TODO项,包括已完成和未完成)
+5. Askfor区域,用于模型遇到需用户确认的问题时显示选项,让用户选择。特别地,最后一项应为用户自定义输入,在用户不同意所有选项时使用。
+
+---
+
+探索 dify 项目,告诉我:dify 的 Agent V2 节点,如果遇到需要用户输入的情况,它是如何处理的?是否会暂停整个工作流?
+
+---
+
+再给我详细解释Q1中的同步和异步机制,以及对于工作流本身和Hermes Bridge分别有什么影响
+
+---
+
+使用同步阻塞方案。另外,我不希望AskFor有超时,因为用户可能让智能体运行后去干其他工作,几小时后才回来查看结果。如果2分钟就超时,会直接导致任务失败。给出解决方案。
+
+---
+
+1. 相比于之前,智能体运行展示处,多了一个白色的框,但内部的输出完全等同于原先思考过程的流式输出,这完全没必要。
+2. 我执行智能操作,明显包含 read_file、write_file 等工具调用,但是没有输出。
+3. 实时思考过程(非流式输出的部分)现在也看不到。
+4. Askfor 功能,不要弹出对话框,我希望在智能体运行展示处进行选项选择;另外,如果用户选择自由输入,其他选项不需要隐藏。
+5. 实现 WorkflowRunRepository 的 findByRunId 方法。
+6. 风格保持与原先一致,深色风格。
+7. 我还未验证 TODO List 功能,待我验证后给出修改结论。
+
+---
+
+很好,现在:
+1. 增加折叠功能,并将已完成的工具调用、思考过程等默认折叠。
+2. Askfor 中,用户自定义输入的框,与其他选项大小保持一致,“提交”按钮也小一些,用纯色按钮+白色字体实现,按钮背景色与工作流的“保存”一致。
+
+---
+
+1. 将“历史调用”修改为“工具调用”;
+2. “历史调用”和“实时思考过程”,在节点运行时默认展开,节点运行完毕后自动收起;
+3. 为每个工具调用框增加展开收起功能,“已完成”的工具调用默认收起;
+4. 节点运行完毕后,隐藏实时思考过程(如 reasoning...)
+5. clarify 工具中,用户自定义输入框,与其他选项使用同一字体和字号。
+
+---
+
+1. 参考“思考过程”,将“工具调用”外围也围上相同的框,“工具调用”字体与“思考过程”一致,思考过程后的“XX字”与“工具调用”后的(X)字体一致,且也用()括起来;
+2. 查看 Hermes-agent 源码,统计它在输出实时思考过程时可能包含的所有颜文字,然后配置到前端 config.js 中,在输出时过滤掉,虽然看着很可爱,但是不严肃。
+3. clarify 工具中,用户自定义输入框,输入时前方会有一个中文汉字的 padding,去掉。
+
+---
+
+1. “思考过程”和“工具调用”前面,不加 emoji 表情;再次确认“工具调用”四个字的字体与“思考过程”一致;思考过程后的“XX字”修改为“(XX字)”。
+2. “工具调用”框 四周 Padding 与“思考过程”框保持一致;
+3. 节点开始运行后,初始时会有一个奇怪的圆角矩形,检查为什么;
+4. 现在点击“XX 条日志”,会在下方打开日志记录;我建议与“思考过程”和“工具调用”展示形式保持一致;每个条目前不加 emoji 表情。
+
+---
+
+“需要您的确认”前方也不加 emoji;
+日志默认显示,不再通过点击“XX条日志”显示和隐藏;因此也可去掉“XX条日志”的提示;日志面板放到“工具调用”面板下方,最终结果上方。
+
+---
+
+1. 将实时状态移动到流式思考过程上方;
+2. 我确定调用了TODO工具,但 TODO List 现在无法看到。
+
+---
+
+1. 工具调用,不要“仅显示最近 5 条”,全部显示;
+2. 执行日志面板,同样增加展开收起功能,且运行完成后默认收起;
+3. 另外,TODO List 现在还是无法查看,我能看到智能体节点多次调用了 todo 工具,但是无法查看 TODO List。对照 TODO 的实现,仔细检查。
+
+---
+
+1. TODO List 修改为 “待办清单”,后面加(X / X),前面不加 emoji,样式与“工具调用”等一致,同样增加展开收起功能,且运行完成后默认收起;
+2. “思考过程”后的(XX字),字体与其他面板()中的内容不一致,统一一下;
+3. “待办清单”面板放到“工具调用”上方。
+
+---
+
+“思考过程”后的(XX),字体与其他()中字体还是不一致。先将“字”加回来吧,恢复(XX字),然后检查字体问题。
+
+---
+
+将工作流界面的“运行结果”中的内容,迁移到“运行记录”页面,按照所有节点运行完成后的默认状态显示。(面板均为收起状态)
+
+---
+
+1. 将“运行结果”和“运行记录”中,输出节点的_workingDirFiles显示修改为“工作空间文件列表”,_runId显示修改为“运行ID”。
+2. 实现 clarifyWaitExecutor 独立池。
+
+---
+
+把“工作空间文件列表”和“运行ID”独立出来显示吧,与“下载工作空间”放到一起,出现时机也保持一致。
+
+---
+
+移除输出节点输出的_workingDirFiles和_runId字段吧,这两个字段作为“workflow-run”的属性存储起来,最终用于展示。
+
+---
+
+部分情况下,假如“智能操作”节点出现clarify选项,“用户输入”节点也会显示这些选项。这不对,移除。
+
+---
+
+由于 hermes-agent 项目是别人的项目,我们对它进行了修改,所以我需要将对它的改动记录为一个文件,放到 docs 下,你来决定以什么方式存储,便于后续该项目 git pull 后应用更改。
+
+---
+
+如果使用 patch 的方式,该文件发生修改后,行号变化,该 patch 还会不会生效?如果会影响,就换一种方式记录变更。
+
+---
+
+尝试在 hermes-agent 新版本上应用变更。
+另外,告诉我,自从我上次pull后,到这次 pull 期间,hermes-agent 进行了哪些升级。
+
+---
+
+如果 hermes 已原生支持了永久不超时,那么适配 hermes 的能力,看我们的bridge等实现是否需要修改;
+另外,还是告诉我,自从我上次pull后,到这次 pull 期间,hermes-agent 进行了哪些升级。
+
+---
+
+将 hermes 最新改动输出到 docs/reference 中,然后处理 patch 废弃标记。
+
+---
+
+我要实现一个 agent 前端问答页面。用户输入问题后,(选择模型,若不选择,使用默认),根据用户问题进行回答/技能执行/智能体执行等操作,过程同样需要显示思考过程、工具调用等内容(与运行结果类似)。开展设计,输出到 docs/design 下。
+
+---
+
+Askfor 界面,用户自定义输入区域,提示信息靠最左,但输入光标在提示信息第一个字后。在提示信息左侧加padding,把提示信息移动到输入光标处。