# 工作流外部调用接口规范(External Workflow API Specification) > 版本:v1.0 > 适用范围:第三方系统通过 HTTP 调用平台工作流,提交自定义输入、订阅节点级思考过程流、获取最终输出变量与工作空间产物。 > 文档基于现有 `/api/workflows/{id}/run`(SSE 同步流)与 `/api/workflows/{id}/runs/{runId}/download` 能力抽象出对外发布接口规范。 --- ## 1. 设计目标 | 目标 | 说明 | |---|---| | 异步分离 | 提交运行与订阅流分离,客户端可延迟订阅、断线重连、批量并发 | | 输入灵活 | 支持纯 JSON 变量、二进制文件、目录结构(保留层级路径)三种输入 | | 全过程可观测 | 节点状态、思考流(thinking)、工具调用、错误事件全部以 SSE 事件推送 | | 产物可获取 | 最终输出变量、工作目录 zip 均可通过稳定 URL 拉取 | | 可认证 | 通过 `X-API-Key` 头部识别调用方,可绑定工作流白名单 | --- ## 2. 通用约定 ### 2.1 基础路径 ``` http(s)://:/api/v1 ``` - 默认端口:`2438`(与平台后端一致,见 `application.yml: server.port`) - 所有路径以 `/api/v1` 开头,与平台内部 `/api/workflows`、`/api/run-history` 隔离,便于独立鉴权 ### 2.2 内容类型 | 场景 | Content-Type | |---|---| | 创建运行(仅 JSON 变量) | `application/json` | | 创建运行(含文件/目录上传) | `multipart/form-data` | | 订阅运行流(SSE) | 响应 `text/event-stream` | | 查询运行结果 | 响应 `application/json` | | 下载工作空间 | 响应 `application/zip` | ### 2.3 认证 所有 `/api/v1/**` 接口必须携带 API Key: ``` X-API-Key: <预共享密钥> ``` - API Key 在 `application.yml` 中通过 `external.api-keys` 列表配置,可配置多组 - 每条 Key 可选绑定 `workflowIds` 白名单(不填表示可调用所有工作流) - 缺失或不匹配 → HTTP `401 Unauthorized`,响应体: ```json { "code": "UNAUTHORIZED", "message": "API Key 缺失或无效" } ``` ### 2.4 统一响应包络 非 SSE 接口统一返回: ```json { "code": "OK", "message": "", "data": { ... } } ``` 错误时: ```json { "code": "", "message": "<可读错误信息>", "data": null } ``` SSE 接口(`/stream`)的认证与业务错误通过事件 payload(见 §5)传递,不使用 HTTP 状态码包络。 ### 2.5 错误码 | code | HTTP | 含义 | |---|---|---| | `OK` | 200 | 成功 | | `UNAUTHORIZED` | 401 | API Key 缺失或无权调用该工作流 | | `VALIDATION_FAILED` | 400 | 入参不合法(变量缺失、文件越界等) | | `WORKFLOW_NOT_FOUND` | 404 | 工作流 ID 不存在 | | `RUN_NOT_FOUND` | 404 | 运行 ID 不存在或已过期 | | `GRAPH_INVALID` | 422 | 工作流图结构未通过校验(无输入/输出节点、不连通、有环) | | `RUN_ALREADY_STARTED` | 409 | 该运行已启动,不可重复 start | | `INTERNAL_ERROR` | 500 | 服务器内部错误(堆栈已脱敏) | ### 2.6 标识符 | 名称 | 类型 | 说明 | |---|---|---| | `workflowId` | number | 工作流主键(路径参数) | | `runId` | string | 每次运行的唯一标识,8 位十六进制(如 `a3f9b2c1`),由服务端生成,贯穿工作目录、SSE 事件、下载 URL | --- ## 3. 接口总览 | # | 方法 | 路径 | 用途 | |---|---|---|---| | 3.1 | `POST` | `/api/v1/workflows/{workflowId}/runs` | 创建运行(提交输入与文件) | | 3.2 | `POST` | `/api/v1/workflows/{workflowId}/runs/{runId}/start` | 启动执行(与 3.1 配合的两阶段模式) | | 3.3 | `GET` | `/api/v1/workflows/{workflowId}/runs/{runId}/stream` | 订阅 SSE 流(思考过程、节点状态、完成事件) | | 3.4 | `GET` | `/api/v1/workflows/{workflowId}/runs/{runId}` | 查询运行结果(最终变量、状态) | | 3.5 | `GET` | `/api/v1/workflows/{workflowId}/runs/{runId}/workspace` | 下载工作空间 zip | | 3.6 | `GET` | `/api/v1/workflows` | 列出当前 API Key 可调用的工作流 | > **设计说明:** 现有 `/api/workflows/{id}/run` 是「SSE 同步阻塞」模式(一次请求边执行边返回)。对外 API 改为「**提交 → 启动 → 订阅**」三段式,以满足「思考过程通过另一个接口获取」的诉求,并支持客户端重连。 --- ## 4. 接口详细规范 ### 3.1 创建运行 **请求** ``` POST /api/v1/workflows/{workflowId}/runs X-API-Key: Content-Type: application/json | multipart/form-data ``` **两种调用形态** #### 形态 A:仅 JSON 变量(无文件) ```http POST /api/v1/workflows/12/runs X-API-Key: demo-key Content-Type: application/json { "variables": { "topic": "大语言模型在企业中的应用", "maxTokens": 2048, "options": { "lang": "zh", "verbose": false } }, "async": true } ``` #### 形态 B:JSON 变量 + 文件 / 目录 ```http POST /api/v1/workflows/12/runs X-API-Key: demo-key Content-Type: multipart/form-data; boundary=----boundary ------boundary Content-Disposition: form-data; name="payload" Content-Type: application/json { "variables": { "topic": "汇总报告" }, "async": true } ------boundary Content-Disposition: form-data; name="files"; filename="report.md" Content-Type: text/markdown <文件二进制内容> ------boundary Content-Disposition: form-data; name="files"; filename="data.csv" Content-Type: text/csv X-Relative-Path: inputs/2024/data.csv <文件二进制内容> ------boundary Content-Disposition: form-data; name="files"; filename="config.yaml" Content-Type: application/yaml X-Relative-Path: config/config.yaml <文件二进制内容> ------boundary-- ``` **关键字段说明** | 字段 | 位置 | 类型 | 必填 | 说明 | |---|---|---|---|---| | `variables` | JSON | object | 否 | 自定义输入变量,会合并到工作流上下文(`WorkflowContext.variables`) | | `async` | JSON | boolean | 否 | `true`(默认):返回 runId 但不自动启动,需调用 `/start`;`false`:自动启动并直接进入 SSE 流(等价于现有 `/run` 行为) | | `failStrategy` | JSON | string | 否 | 整体默认失败策略,覆盖节点级配置:`abort`(默认)\| `skip` | | `ttlHours` | JSON | number | 否 | 工作空间保留时间,默认 `720`(30 天);超期清理 | | `files` | multipart | binary[] | 否 | 二进制文件,可重复 | | `X-Relative-Path` | part header | string | 否 | 该文件在工作目录中的相对路径;缺失时使用 `filename`;含 `/` 时自动创建目录结构(目录上传场景) | > **目录上传约定:** 客户端在前端使用 `` 时浏览器原生提供 `webkitRelativePath`,通过该 header 透传到后端即可保留目录结构。后端会做 ZIP slip 防护(`dest.normalize().startsWith(runDir)` 校验)。 **响应(async=true)** ```json { "code": "OK", "data": { "runId": "a3f9b2c1", "workflowId": 12, "status": "CREATED", "createdAt": "2026-07-01T08:30:00Z", "links": { "start": "/api/v1/workflows/12/runs/a3f9b2c1/start", "stream": "/api/v1/workflows/12/runs/a3f9b2c1/stream", "result": "/api/v1/workflows/12/runs/a3f9b2c1", "workspace": "/api/v1/workflows/12/runs/a3f9b2c1/workspace" } } } ``` **响应(async=false,立即启动)** 直接以 SSE 流响应(Content-Type 切换为 `text/event-stream`),等价于 §3.3 的输出。 --- ### 3.2 启动运行 仅当 `async=true` 创建的运行需要显式启动。 **请求** ```http POST /api/v1/workflows/12/runs/a3f9b2c1/start X-API-Key: demo-key ``` 无请求体。 **响应** ```json { "code": "OK", "data": { "runId": "a3f9b2c1", "status": "RUNNING", "streamUrl": "/api/v1/workflows/12/runs/a3f9b2c1/stream" } } ``` > 启动后客户端应立即打开 `/stream`(§3.3)订阅事件;服务端会缓存最近 N 条事件用于断线重连补发。 --- ### 3.3 订阅运行流(SSE) ``` GET /api/v1/workflows/{workflowId}/runs/{runId}/stream X-API-Key: Accept: text/event-stream ``` **响应**:`Content-Type: text/event-stream`,按 SSE 协议推送事件,事件名与 payload 详见 §5。 **断线重连** 支持通过 `Last-Event-ID` 头部续传: ```http GET .../stream Last-Event-ID: 42 ``` 服务端为每个事件分配单调递增的 `id:`,客户端重连时携带最后收到的事件 id,服务端补发其后所有事件。事件缓存窗口默认 1000 条或运行结束后保留 1 小时。 **关闭时机** - 工作流成功完成:推送 `workflow_complete` 后关闭 - 工作流出错:推送 `workflow_error` 后关闭 - SSE 连接 30 分钟超时(与现有 `SseEmitter(1_800_000L)` 一致) - 客户端主动断开:服务端停止推送但不影响后台执行 --- ### 3.4 查询运行结果 ``` GET /api/v1/workflows/{workflowId}/runs/{runId} X-API-Key: ``` **响应** ```json { "code": "OK", "data": { "runId": "a3f9b2c1", "workflowId": 12, "status": "SUCCESS", "startedAt": "2026-07-01T08:30:05Z", "completedAt": "2026-07-01T08:31:12Z", "outputs": { "summary": "本报告探讨了...", "_workingDirFiles": ["report.md", "charts/fig1.png"], "_runId": "a3f9b2c1" }, "nodes": [ { "nodeId": "node-1", "nodeType": "userInput", "label": "用户输入", "status": "SUCCESS", "startedAt": "2026-07-01T08:30:05Z", "completedAt": "2026-07-01T08:30:05Z", "output": { "topic": "..." }, "logsCount": 0, "logsUrl": "/api/v1/workflows/12/runs/a3f9b2c1/nodes/node-1/logs" }, { "nodeId": "node-2", "nodeType": "agent", "label": "研究助手", "status": "SUCCESS", "output": { "research": "..." }, "logsCount": 124, "logsUrl": "/api/v1/workflows/12/runs/a3f9b2c1/nodes/node-2/logs" } ], "workspace": { "fileCount": 12, "totalBytes": 456789, "downloadUrl": "/api/v1/workflows/12/runs/a3f9b2c1/workspace" } } } ``` **status 取值** - `CREATED`:已创建未启动 - `RUNNING`:执行中 - `SUCCESS`:成功完成 - `FAILED`:执行失败(`error` 字段填充) **节点级思考过程日志查询** `logsUrl` 指向: ``` GET /api/v1/workflows/{workflowId}/runs/{runId}/nodes/{nodeId}/logs ``` 返回该节点执行期间产生的所有日志(`thinking` / `tool_call` / `tool_result` / `info` / `error`),用于回放或归档: ```json { "code": "OK", "data": { "nodeId": "node-2", "logs": [ { "type": "THINKING", "timestamp": "...", "message": "首先我需要..." }, { "type": "TOOL_CALL", "timestamp": "...", "message": "search_web", "detail": "{\"q\":\"...\"}" }, { "type": "TOOL_RESULT", "timestamp": "...", "message": "search_web", "detail": "返回 3 条结果..." } ] } } ``` --- ### 3.5 下载工作空间 ``` GET /api/v1/workflows/{workflowId}/runs/{runId}/workspace X-API-Key: ``` **响应** ``` HTTP/1.1 200 OK Content-Type: application/zip Content-Disposition: attachment; filename*=UTF-8''workflow-12-run-a3f9b2c1.zip ``` - 应用 `.agentignore` 规则过滤(默认排除 `.hermes/`、`*.log`、`*.tmp`、`.DS_Store`、`Thumbs.db`) - 404 时返回标准错误包络,`code = RUN_NOT_FOUND` --- ### 3.6 列出可调用工作流 ``` GET /api/v1/workflows?page=1&size=20&search= X-API-Key: ``` **响应** ```json { "code": "OK", "data": { "total": 1, "items": [ { "id": 12, "name": "研究报告生成", "description": "根据主题生成结构化研究报告", "inputs": [ { "name": "topic", "type": "string", "required": true, "description": "研究主题" }, { "name": "maxTokens", "type": "number", "required": false } ], "outputs": [ { "name": "summary", "type": "string" } ] } ] } } ``` `inputs` 与 `outputs` 由服务端扫描 `graphData` 中 `userInput` 与 `output` 节点的字段定义自动推导,便于第三方系统展示表单。 --- ## 5. SSE 事件规范 所有事件格式: ``` event: <事件名> id: <单调递增整数> data: ``` ### 5.1 事件类型 | 事件名 | 触发时机 | 是否终止流 | |---|---|---| | `run_started` | 执行引擎启动 | 否 | | `node_status` | 节点状态变更 | 否 | | `node_stream` | 节点流式增量(thinking / tool_call / tool_result) | 否 | | `workflow_complete` | 工作流成功完成 | 是 | | `workflow_error` | 工作流出错 | 是 | ### 5.2 `run_started` ```json { "runId": "a3f9b2c1", "workflowId": 12, "timestamp": "2026-07-01T08:30:05Z" } ``` ### 5.3 `node_status` ```json { "runId": "a3f9b2c1", "type": "node_status", "nodeId": "node-2", "nodeType": "agent", "label": "研究助手", "status": "RUNNING", "timestamp": "2026-07-01T08:30:06Z" } ``` 状态变为 `SUCCESS` 时额外携带: ```json { "...": "...", "status": "SUCCESS", "output": { "research": "..." }, "completedAt": "2026-07-01T08:30:45Z" } ``` 状态变为 `FAILED` 时携带 `error` 字段。 ### 5.4 `node_stream`(思考过程) ```json { "runId": "a3f9b2c1", "type": "node_stream", "nodeId": "node-2", "kind": "thinking", "content": "首先我需要搜索相关资料...", "toolName": null, "timestamp": "2026-07-01T08:30:07Z" } ``` | `kind` | 含义 | `toolName` | |---|---|---| | `thinking` | LLM / Agent 的思考片段 | null | | `tool_call` | 工具调用开始(参数摘要) | 工具名 | | `tool_result` | 工具调用返回(结果摘要) | 工具名 | ### 5.5 `workflow_complete` ```json { "runId": "a3f9b2c1", "type": "workflow_complete", "success": true, "output": { "summary": "...", "_workingDirFiles": ["report.md", "charts/fig1.png"], "_runId": "a3f9b2c1" }, "completedAt": "2026-07-01T08:31:12Z", "workspaceUrl": "/api/v1/workflows/12/runs/a3f9b2c1/workspace", "resultUrl": "/api/v1/workflows/12/runs/a3f9b2c1" } ``` ### 5.6 `workflow_error` ```json { "runId": "a3f9b2c1", "type": "workflow_error", "success": false, "error": "节点 node-3 执行失败:LLM 调用超时", "failedNodeId": "node-3", "completedAt": "2026-07-01T08:31:00Z" } ``` --- ## 6. 调用时序示例 ### 6.1 完整异步流程 ``` 客户端 服务端 │ │ │── POST /runs (variables + files) ─────────────────▶│ 创建运行 + 工作目录 │◀────────── 200 { runId, links } ──────────────────│ │ │ │── POST /runs/{runId}/start ───────────────────────▶│ 启动执行 │◀────────── 200 { status: RUNNING } ───────────────│ │ │ │── GET /runs/{runId}/stream ───────────────────────▶│ 订阅 SSE │◀── event: run_started ────────────────────────────│ │◀── event: node_status (node-1 RUNNING) ───────────│ │◀── event: node_status (node-1 SUCCESS) ───────────│ │◀── event: node_status (node-2 RUNNING) ───────────│ │◀── event: node_stream (node-2 thinking) ──────────│ │◀── event: node_stream (node-2 thinking) ──────────│ │◀── event: node_stream (node-2 tool_call) ─────────│ │◀── event: node_stream (node-2 tool_result) ───────│ │◀── event: node_status (node-2 SUCCESS) ───────────│ │◀── event: node_status (node-3 SUCCESS) ───────────│ │◀── event: workflow_complete ──────────────────────│ 流结束 │ │ │── GET /runs/{runId} ──────────────────────────────▶│ 查询结果 │◀────────── 200 { outputs, nodes[] } ──────────────│ │ │ │── GET /runs/{runId}/workspace ────────────────────▶│ 下载 zip │◀────────── 200 application/zip ───────────────────│ ``` ### 6.2 断线重连 ``` 客户端在事件 id=42 处断开,重连: │── GET /runs/{runId}/stream ───────────────────────▶│ │ Last-Event-ID: 42 │ │◀── event: node_status (重发 id=43..) ──────────────│ ``` ### 6.3 同步阻塞模式(async=false) 适合简单集成,一次请求直接返回 SSE 流: ``` │── POST /runs (async=false) ───────────────────────▶│ │◀── event: run_started ────────────────────────────│ │◀── event: node_stream ... ────────────────────────│ │◀── event: workflow_complete ──────────────────────│ ``` --- ## 7. 限制与约束 | 项目 | 限制 | 说明 | |---|---|---| | 单次执行超时 | 5 分钟(软) | 引擎层 `MAX_EXECUTION_SECONDS=300` | | SSE 连接超时 | 30 分钟 | 与现有 `SseEmitter` 一致 | | 单文件大小 | 100 MB | 超过建议改用对象存储预签名 URL | | 单次上传文件数 | 50 | multipart part 数上限 | | 总请求体大小 | 500 MB | Spring `spring.servlet.multipart.max-request-size` | | 工作目录保留 | 默认 30 天 | 通过创建运行时 `ttlHours` 调整 | | 并发运行 | 受 CPU 核数约束 | 引擎线程池 `fixedThreadPool(processors)` | | 工作流图校验 | 必须有 userInput + output 节点且通过中间节点连通 | 详见 §8 | --- ## 8. 工作流图结构校验 服务端在 `/runs` 创建时或 `/start` 启动时会校验 `graphData`: 1. 至少 1 个 `userInput` 类型节点 2. 至少 1 个 `output` 类型节点 3. 从任一 `userInput` 到任一 `output` 必须存在有向路径,且路径上至少包含 1 个中间节点(直接 `input → output` 边不满足) 4. DAG 无环 不满足时返回 `GRAPH_INVALID`(HTTP 422),错误信息指明具体原因。 --- ## 9. curl 调用示例 ### 9.1 纯 JSON 输入 ```bash curl -X POST https://host/api/v1/workflows/12/runs \ -H "X-API-Key: demo-key" \ -H "Content-Type: application/json" \ -d '{ "variables": { "topic": "AI 在医疗领域的应用" }, "async": true }' ``` ### 9.2 JSON + 文件 + 目录 ```bash curl -X POST https://host/api/v1/workflows/12/runs \ -H "X-API-Key: demo-key" \ -F 'payload={"variables":{"topic":"汇总"},"async":true};type=application/json' \ -F 'files=@report.md' \ -F 'files=@data.csv;type=text/csv' \ -H 'X-Relative-Path: inputs/2024/data.csv' \ -F 'files=@config/config.yaml;type=application/yaml' ``` > 注:curl 的 `X-Relative-Path` 是自定义 part header,后端在解析 multipart 时需读取该头部;如果客户端无法发送自定义 part header,可改为在 `payload.inputs.uploadedFiles` 中显式提供路径列表,并使用默认的 `filename`。 ### 9.3 启动 + 订阅 ```bash # 启动 curl -X POST https://host/api/v1/workflows/12/runs/a3f9b2c1/start \ -H "X-API-Key: demo-key" # 订阅流 curl -N -H "X-API-Key: demo-key" \ https://host/api/v1/workflows/12/runs/a3f9b2c1/stream ``` ### 9.4 查询结果 + 下载 ```bash curl -H "X-API-Key: demo-key" \ https://host/api/v1/workflows/12/runs/a3f9b2c1 curl -H "X-API-Key: demo-key" \ https://host/api/v1/workflows/12/runs/a3f9b2c1/workspace \ -o workspace.zip ``` --- ## 10. 与现有内部接口的映射关系 | 外部接口 | 内部接口 | 备注 | |---|---|---| | `POST /api/v1/.../runs` | (新增) | 复用 `WorkflowRunDirManager.createRunDir` + 文件写入逻辑(现有 `WorkflowController:165-192`) | | `POST /api/v1/.../runs/{runId}/start` | `WorkflowEngine.executeAsync` | 异步触发 | | `GET /api/v1/.../runs/{runId}/stream` | `SseEmitter` + `WorkflowRunEvent` | 复用现有事件工厂 | | `GET /api/v1/.../runs/{runId}` | `RunHistoryController:62` | 扩展为对外字段 | | `GET /api/v1/.../runs/{runId}/workspace` | `WorkflowController:205` | 复用 zip 打包 + `.agentignore` 过滤 | | 节点日志 | (新增) | 从 `WorkflowRunNode.logs` 反序列化 | 实现建议:新增 `ExternalApiController`(路径 `/api/v1/**`)与独立 `ExternalApiAuthFilter`(解析 `X-API-Key`),不污染现有 Controller,便于后续按版本演进。