external-workflow-api-spec.md 21 KB

工作流外部调用接口规范(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)://<host>:<port>/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,响应体:

    { "code": "UNAUTHORIZED", "message": "API Key 缺失或无效" }
    

2.4 统一响应包络

非 SSE 接口统一返回:

{
  "code": "OK",
  "message": "",
  "data": { ... }
}

错误时:

{
  "code": "<ERROR_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: <key>
Content-Type: application/json  |  multipart/form-data

两种调用形态

形态 A:仅 JSON 变量(无文件)

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 变量 + 文件 / 目录

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 但不自动启动,需调用 /startfalse:自动启动并直接进入 SSE 流(等价于现有 /run 行为)
failStrategy JSON string 整体默认失败策略,覆盖节点级配置:abort(默认)| skip
ttlHours JSON number 工作空间保留时间,默认 720(30 天);超期清理
files multipart binary[] 二进制文件,可重复
X-Relative-Path part header string 该文件在工作目录中的相对路径;缺失时使用 filename;含 / 时自动创建目录结构(目录上传场景)

目录上传约定: 客户端在前端使用 <input webkitdirectory> 时浏览器原生提供 webkitRelativePath,通过该 header 透传到后端即可保留目录结构。后端会做 ZIP slip 防护(dest.normalize().startsWith(runDir) 校验)。

响应(async=true)

{
  "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 创建的运行需要显式启动。

请求

POST /api/v1/workflows/12/runs/a3f9b2c1/start
X-API-Key: demo-key

无请求体。

响应

{
  "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: <key>
Accept: text/event-stream

响应Content-Type: text/event-stream,按 SSE 协议推送事件,事件名与 payload 详见 §5。

断线重连

支持通过 Last-Event-ID 头部续传:

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: <key>

响应

{
  "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),用于回放或归档:

{
  "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: <key>

响应

HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename*=UTF-8''workflow-12-run-a3f9b2c1.zip

<zip binary>
  • 应用 .agentignore 规则过滤(默认排除 .hermes/*.log*.tmp.DS_StoreThumbs.db
  • 404 时返回标准错误包络,code = RUN_NOT_FOUND

3.6 列出可调用工作流

GET /api/v1/workflows?page=1&size=20&search=
X-API-Key: <key>

响应

{
  "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" }
        ]
      }
    ]
  }
}

inputsoutputs 由服务端扫描 graphDatauserInputoutput 节点的字段定义自动推导,便于第三方系统展示表单。


5. SSE 事件规范

所有事件格式:

event: <事件名>
id: <单调递增整数>
data: <JSON 对象>

5.1 事件类型

事件名 触发时机 是否终止流
run_started 执行引擎启动
node_status 节点状态变更
node_stream 节点流式增量(thinking / tool_call / tool_result)
workflow_complete 工作流成功完成
workflow_error 工作流出错

5.2 run_started

{
  "runId": "a3f9b2c1",
  "workflowId": 12,
  "timestamp": "2026-07-01T08:30:05Z"
}

5.3 node_status

{
  "runId": "a3f9b2c1",
  "type": "node_status",
  "nodeId": "node-2",
  "nodeType": "agent",
  "label": "研究助手",
  "status": "RUNNING",
  "timestamp": "2026-07-01T08:30:06Z"
}

状态变为 SUCCESS 时额外携带:

{
  "...": "...",
  "status": "SUCCESS",
  "output": { "research": "..." },
  "completedAt": "2026-07-01T08:30:45Z"
}

状态变为 FAILED 时携带 error 字段。

5.4 node_stream(思考过程)

{
  "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

{
  "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

{
  "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 输入

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 + 文件 + 目录

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 启动 + 订阅

# 启动
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 查询结果 + 下载

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,便于后续按版本演进。