# 外部工作流 API 参考 当需要从其他应用或新的集成层调用 agent-management 工作流时,按本参考实现服务调用。 本文聚焦接口契约。前端如何展示运行过程(面板结构、折叠策略、交互逻辑)见 `run-history-ui.md`;可直接复用的 JS/Vue 参考实现见 `js-implementation.md`。 ## 契约概要 - 基础路径:`http(s)://:/api/v1` - 默认后端端口:`2438` - 鉴权:每个 `/api/v1/**` 请求需携带 `X-API-Key: ` 请求头(**仅支持请求头,不支持 query 参数**) - CORS:后端对 `/api/**` 全开放(任意 Origin、`allowedHeaders: *`),浏览器可跨域直连 - JSON 成功响应包: ```json { "code": "OK", "message": "", "data": {} } ``` - 错误响应包: ```json { "code": "VALIDATION_FAILED", "message": "可读错误信息", "data": null } ``` - 主要错误码:`UNAUTHORIZED`、`VALIDATION_FAILED`、`WORKFLOW_NOT_FOUND`、`RUN_NOT_FOUND`、`GRAPH_INVALID`、`RUN_ALREADY_STARTED`、`INTERNAL_ERROR` - 判断成功用 `resp.code === 'OK'`(字符串,非数字 0) ## 工作流标识 外部 API 的所有 `{workflowName}` 路径段同时接受两种值,**优先使用 `name`**: - **`name`(推荐)**:kebab-case 字符串,正则 `^[a-z0-9]+(-[a-z0-9]+)*$`,最大 64 字符,全局唯一。例:`military-analyze-agent`、`report-pipeline`。 - **`id`(兼容兜底)**:纯数字字符串。当传入值不是 `name` 且为纯数字时,按内部主键 `id` 查找。仅用于兼容历史调用方,新集成不要依赖。 解析顺序:先 `findByName(nameOrId)`,未命中且为纯数字时回退 `findById(Long.valueOf(nameOrId))`。 响应中的标识字段约定: | 字段 | 类型 | 含义 | |---|---|---| | `workflowId` | number | 内部数字主键,仅作为元数据返回,**不用于 URL 路径** | | `workflowName` | string | kebab-case 唯一标识,所有 links、downloadUrl、logsUrl 都用它拼接 | | `displayName` | string | 中文展示名,仅用于 UI 展示 | 工作流列表响应还会返回 `description`、`inputs`、`outputs`(从 `userInput` / `output` 节点自动提取的字段定义)。 ## 端点 | 方法 | 路径 | 用途 | |---|---|---| | `GET` | `/api/v1/workflows` | 列出当前 API key 可访问的工作流 | | `POST` | `/api/v1/workflows/{workflowName}/runs` | 创建一次运行,可携带变量与可选文件(JSON 或 multipart) | | `POST` | `/api/v1/workflows/{workflowName}/runs/{runId}/start` | 启动一个已创建的异步运行 | | `GET` | `/api/v1/workflows/{workflowName}/runs/{runId}/stream` | 订阅 SSE 节点/运行事件(支持 `Last-Event-ID` 续传) | | `GET` | `/api/v1/workflows/{workflowName}/runs/{runId}` | 查询运行状态、输出、节点和工作空间信息 | | `GET` | `/api/v1/workflows/{workflowName}/runs/{runId}/nodes/{nodeId}/logs` | 查询某节点的持久化日志 | | `GET` | `/api/v1/workflows/{workflowName}/runs/{runId}/workspace` | 下载工作空间 zip | ## 推荐的异步流程 1. `POST /workflows/{workflowName}/runs`,带 `async: true`。`{workflowName}` 推荐使用 kebab-case `name`。 2. 读取 `data.runId` 和 `data.links`。响应中的 `workflowId`(数字主键)和 `workflowName`(kebab-case)可分别作为元数据保存。 3. `POST /workflows/{workflowName}/runs/{runId}/start`。 4. 打开 `GET /workflows/{workflowName}/runs/{runId}/stream`,请求头 `Accept: text/event-stream`。 5. 收到 `workflow_complete` 或 `workflow_error` 后,关闭流或标记为终态。 6. 查询 `GET /workflows/{workflowName}/runs/{runId}` 获取持久化输出与节点摘要。 7. 将 `GET /workflows/{workflowName}/runs/{runId}/workspace` 作为二进制下载入口提供给用户。 仅当期望"单次阻塞请求直接返回 SSE"时才使用 `async: false`(此时创建即启动,响应直接是 StartRunResponse 结构)。 ## 创建运行 纯 JSON 请求: ```http POST /api/v1/workflows/military-analyze-agent/runs X-API-Key: demo-key Content-Type: application/json { "variables": { "topic": "大语言模型在企业中的应用", "maxTokens": 2048, "options": { "lang": "zh", "verbose": false } }, "async": true, "ttlHours": 720 } ``` Multipart 请求: ```http POST /api/v1/workflows/military-analyze-agent/runs X-API-Key: demo-key Content-Type: multipart/form-data payload: {"variables":{"topic":"汇总报告"},"async":true} files: report.md files: inputs/2024/data.csv ``` 字段说明: - `variables`:自定义工作流输入变量;对象;可选。 - `async`:默认 `true`。为 `true` 时仅创建;为 `false` 时立即启动并以 SSE 形式返回。 - `ttlHours`:工作空间保留小时数;默认 `720`。 - `files`:可重复的 multipart 二进制 part;可选。上传后自动注入 `variables.uploadedFiles`(相对路径数组)。 - `X-Relative-Path`:multipart part 的可选 header,用于保留目录路径。若客户端技术栈不支持,则退化为仅使用文件名并明确记录该限制。 `async: true` 的响应: ```json { "code": "OK", "data": { "runId": "a3f9b2c1", "workflowId": 12, "workflowName": "military-analyze-agent", "displayName": "军事分析助手", "status": "CREATED", "createdAt": "2026-07-01T08:30:00Z", "links": { "start": "/api/v1/workflows/military-analyze-agent/runs/a3f9b2c1/start", "stream": "/api/v1/workflows/military-analyze-agent/runs/a3f9b2c1/stream", "result": "/api/v1/workflows/military-analyze-agent/runs/a3f9b2c1", "workspace": "/api/v1/workflows/military-analyze-agent/runs/a3f9b2c1/workspace" } } } ``` ## SSE 流 打开连接(路径段使用 `workflowName`,即 kebab-case `name`): ```http GET /api/v1/workflows/{workflowName}/runs/{runId}/stream X-API-Key: Accept: text/event-stream ``` 断线重连时携带: ```http Last-Event-ID: 42 ``` 每条事件遵循标准 SSE 格式(`id` 为 run 内单调递增的序号): ```text event: node_stream id: 43 data: {"runId":"a3f9b2c1","type":"node_stream","nodeId":"node-2","visitUuid":"5c1f...","kind":"thinking","content":"首先我需要搜索相关资料...","toolName":null} ``` ### 事件类型(6 种) | type | 触发时机 | 关键字段 | |---|---|---| | `node_status` | 节点状态变更(启动/完成/失败/跳过) | `nodeId`、`visitUuid`、`status`、`output`、`error`、`logs`、`contextSnapshot`;RUNNING 时另携带 `nodeType`、`nodeLabel`(v1.2) | | `node_stream` | 节点执行中的流式增量 | `nodeId`、`visitUuid`、`kind`、`content`、`toolName` | | `visit_recorded` | 一次节点访问(Visit)已持久化 | `visit`(完整 Visit 快照对象) | | `planner_decision` | AGENTIC 模式下 AI 决定下一步访问哪个节点 | `decision`(含 nextNodeId 与 reason) | | `workflow_complete` | 终态成功 | `output`、`success: true`、`workingDirFiles` | | `workflow_error` | 终态失败 | `error`、`success: false` | 注意:**没有 `run_started` 事件**,运行启动信号由 `start` 接口的 HTTP 响应承担。 ### `node_status` 详解 - `status` 取值:`RUNNING` / `SUCCESS` / `FAILED` / `SKIPPED`(大写枚举)。 - 节点启动时推 `RUNNING`,除 `nodeId` + `visitUuid` 外还携带 `nodeType` 与 `nodeLabel`(v1.2)——无画布数据的集成方应在此时直接建卡片并使用这两个字段识别节点类型/显示名,避免显示"未知"(终态事件不携带)。 - 节点结束时推最终状态,同时携带: - `output`:节点输出 envelope,结构为 `{status: 200, message, data}`(`status` 是数字:200 成功 / 400 失败),业务字段在 `data` 内(解包规则见 `run-history-ui.md`)。 - `logs`:节点执行日志数组,元素结构 `{type, message, detail, timestamp}`,可用于回放思考过程、工具调用、待办清单(回放场景优先从 `visit_recorded` 的 `visit.logsJson` 取)。 - `contextSnapshot`:节点完成后的完整变量快照(debug 用途,可忽略)。 ### `node_stream` 详解(8 种 kind) | kind | 语义 | `content` 格式 | `toolName` | 前端处理 | |---|---|---|---|---| | `thinking` | LLM 思考增量 | 纯文本片段 | null | 追加到思考过程面板(markdown 渲染) | | `status` | 实时状态文案(如"压缩上下文中") | 纯文本 | null | 覆盖式显示,节点完成后隐藏 | | `step` | 每轮 LLM 调用进度 | JSON 字符串 `{"iteration":N,"prev_tools":[...]}` | null | 追加到执行日志 | | `tool_progress` | 工具细粒度进度 | JSON 字符串 `{"event_type":"...","name":"...","preview":...}` | 工具名 | 更新对应工具卡片的运行中状态 | | `tool_call` | 工具调用开始 | 参数 JSON(截断到 500 字符) | 工具名 | 新建工具调用卡片 | | `tool_result` | 工具调用结束 | 结果摘要(截断到 500 字符) | 工具名 | 关闭对应卡片,展示结果 | | `todo_update` | 待办清单更新 | JSON 字符串(TodoWrite 工具完整输出) | `"todo"` | 覆盖式更新待办面板 | | `clarify_request` | AskFor:请求用户确认 | JSON 字符串 `{"clarify_id":"...","question":"...","choices":[...]}` | null | 渲染 AskFor 面板(提交/恢复见下方"AskFor(用户确认)") | `tool_call` / `tool_result` 的 `content` 均被截断到 500 字符,完整内容通过 `node_status.logs` 或节点日志接口获取。 ### visitUuid 路由(重要) 每个 `node_status` / `node_stream` 事件都携带 `visitUuid`,标识**一次节点访问**: - DAG 模式:节点只执行一次,`visitUuid` 与 `nodeId` 一一对应。 - AGENTIC 模式(智能体工作流):同一节点可被多次访问(最多 50 次),每次访问独立生成 `visitUuid`。 **前端必须按 `visitUuid` 聚合事件、渲染卡片,不能按 `nodeId` 合并**——否则同节点重访时思考过程、工具调用会混在一起。`visit_recorded` 事件携带的 `visit.sortOrder` 决定卡片排列顺序;`visit.iterSeq` 表示同节点第几次访问;`visit.fromNodeId` 表示直接来源节点。 ### 客户端行为约定 - 以 `visitUuid` 为主键维护每个 Visit 卡片的状态。 - `node_status RUNNING` 创建卡片;终态更新卡片状态并触发折叠。 - 将 `node_stream.kind === "thinking"` 的 `content` 追加到该 Visit 的思考缓冲区。 - `tool_call` / `tool_result` / `tool_progress` 按到达顺序维护工具调用列表。 - 把 `workflow_complete` 与 `workflow_error` 视为终态,停止一切重连。 - 如需支持重连,持久化最近一条 SSE 的 `id`。 ### 断线重连机制 服务端保障: - 每个 run 缓存最近 **5000 条**事件(超出丢弃最旧)。 - 运行结束后缓存保留 **25 小时**,期间重连可完整回放。 - SSE 连接超时 24 小时(兜底值,真实长等待由重连机制覆盖)。 - 每 30 秒发送一次 `:keep-alive` 心跳注释帧(不触发业务逻辑)。 - 重连时携带 `Last-Event-ID` 请求头,服务端补发该 id 之后的全部事件。 - 运行结束超过 25 小时(或服务重启导致缓存丢失)后再连 stream:服务端从数据库重建一条终止事件(`workflow_complete` 或 `workflow_error`),客户端据此直接进入终态。 客户端建议(完整实现见 `js-implementation.md`): - SSE 断开后指数退避重连(1s/2s/4s/8s/16s,5 次)。 - 5 次失败后降级轮询 `GET /runs/{runId}`(30s/次),依据返回的 `status` 判断终态。 - SSE 恢复后立即停止轮询。 - **浏览器直连注意**:`EventSource` 原生 API 不支持自定义请求头,无法携带 `X-API-Key`。两个选择: 1. 用 `fetch` + `ReadableStream` 手动解析 SSE 流(参考实现即此方案,另需自行实现重连与 `Last-Event-ID` 传递); 2. 业务方后端反代 `/agent-api/*` → `/api/v1/*` 并注入密钥,前端走同源 `EventSource`。 ### AskFor(用户确认) 工作流中的智能体节点可能触发 `clarify_request`(AskFor),此时整个工作流暂停等待用户选择选项或输入自定义答案。外部 API 已开放对应端点(v1.2): #### 提交答案 ```http POST /api/v1/runs/{runId}/resume X-API-Key: demo-key Content-Type: application/json { "clarifyId": "clar-5678", "answer": "v2.0" } ``` 响应 data:`{ "accepted": true, "runId": "...", "clarifyId": "..." }`。`accepted=false` 表示答案已落库但 Bridge 唤醒失败(或平台未启用 Hermes Bridge);错误场景直接返回 HTTP 错误码: | HTTP | code | 场景 | |---|---|---| | 400 | `VALIDATION_FAILED` | runId/clarifyId 格式非法或缺请求体 | | 404 | `RUN_NOT_FOUND` | runId 不存在,或 Key 无该 run 所属工作流权限(不泄露存在性) | | 404 | `CLARIFY_NOT_FOUND` | clarifyId 不存在、不属于该 run,或运行未启动 | | 409 | `CLARIFY_ALREADY_ANSWERED` | 同一 clarifyId 重复提交——**按幂等约定视为成功** | | 410 | `RUN_ALREADY_FINISHED` | 运行已 SUCCESS/FAILED | #### 状态恢复(断线重连) ```http GET /api/v1/runs/{runId}/state X-API-Key: demo-key ``` 响应 data:`{ runId, status, currentPause }`,`status` 为 `CREATED`/`RUNNING`/`SUCCESS`/`FAILED`(大写);`currentPause` 为当前未答暂停点(无则 null),含 `clarifyId`、`nodeId`、`question`、`choices`(**已解析为数组**,区别于内部 API 的 JSON 字符串)、`createdAt`(ISO_LOCAL_DATE_TIME)。 恢复流程:先查 `/state` 恢复 AskFor 面板,再以 `Last-Event-ID` 请求头续传 SSE 补齐节点过程数据。注意两端点路径**不带 `workflows/{name}` 段**(runId 全局唯一),页面刷新后 workflowName 丢失也不影响调用。 ## 查询结果 ```http GET /api/v1/workflows/military-analyze-agent/runs/a3f9b2c1 X-API-Key: demo-key ``` 预期的 data 形态: ```json { "runId": "a3f9b2c1", "workflowId": 12, "workflowName": "military-analyze-agent", "displayName": "军事分析助手", "status": "SUCCESS", "startedAt": "2026-07-01T08:30:05Z", "completedAt": "2026-07-01T08:31:12Z", "error": null, "outputs": { "status": 200, "message": "工作流执行成功", "data": { "summary": "本报告探讨了..." } }, "nodes": [ { "nodeId": "node-2", "nodeType": "agent", "label": "研究助手", "status": "SUCCESS", "output": { "status": 200, "message": "调用成功", "data": { "research": "..." } }, "error": null, "logsCount": 124, "logsUrl": "/api/v1/workflows/military-analyze-agent/runs/a3f9b2c1/nodes/node-2/logs" } ], "workspace": { "fileCount": 12, "totalBytes": 456789, "downloadUrl": "/api/v1/workflows/military-analyze-agent/runs/a3f9b2c1/workspace" } } ``` 状态取值:`CREATED`、`RUNNING`、`SUCCESS`、`FAILED`。 注意:`outputs` 与 `nodes[].output` 均为 envelope 结构(服务端不解包),业务字段取自 `data`(解包规则见 `run-history-ui.md`)。AGENTIC 模式的运行结果中 `nodes` 可能为空数组(该模式按 Visit 持久化,不生成节点级记录);完整过程数据以 SSE 重放或 `visit_recorded` 事件为准。 ## 日志与工作空间 节点日志: ```http GET /api/v1/workflows/{workflowName}/runs/{runId}/nodes/{nodeId}/logs ``` 日志元素结构 `{type, message, detail, timestamp}`,`type` 取值 `THINKING`、`TOOL_CALL`、`TOOL_RESULT`、`STATUS`、`STEP`、`TOOL_PROGRESS`、`TODO_UPDATE`、`CLARIFY_REQUEST`、`ERROR`。把思考过程抽到独立展示区,非思考日志放在可折叠的日志列表中。 工作空间下载: ```http GET /api/v1/workflows/{workflowName}/runs/{runId}/workspace ``` 响应是二进制 zip 内容,不要走 JSON 响应拦截器。 ## 超时设置建议 - REST 接口(创建/启动/查询):客户端超时 ≤ 30 秒。 - SSE 流:连接可能持续数分钟到数小时,`fetch` 直连方案不要给 read 超时;依赖心跳帧(30s)+ 重连机制保证活性。 - 运行本身有 24 小时兜底超时(AskFor 等待场景用户可能长时间离开)。 ## 安全与集成注意事项 - 默认不要在前端代码中暴露 `X-API-Key`。优先使用后端代理,由服务端注入密钥。 - base URL 与 API key 应存放在环境/配置中,不要硬编码到源码。 - API Key 的访问范围(白名单)在 agent-management 后端 `ExternalApiProperties` 中配置,同时支持 `workflowNames`(推荐,按 kebab-case `name` 精确匹配)和 `workflowIds`(兼容,按数字主键匹配);两者皆空表示不限制。新集成建议使用 `workflowNames` 与 URL 路径保持一致。 - 如实现代理上传,需校验路径,防止 `../` 路径穿越。 - 代理 SSE 时关闭缓冲(`X-Accel-Buffering: no`),保留事件名、id 和 data 负载。 - 未经用户同意,不要悄悄把 SSE 降级为轮询。 - 使用 axios/fetch 等封装时,对 `text/event-stream` 和工作空间 zip 端点绕过 JSON 响应包解析。