版本:v1.0 适用范围:第三方系统通过 HTTP 调用平台工作流,提交自定义输入、订阅节点级思考过程流、获取最终输出变量与工作空间产物。 文档基于现有
/api/workflows/{id}/run(SSE 同步流)与/api/workflows/{id}/runs/{runId}/download能力抽象出对外发布接口规范。
| 目标 | 说明 |
|---|---|
| 异步分离 | 提交运行与订阅流分离,客户端可延迟订阅、断线重连、批量并发 |
| 输入灵活 | 支持纯 JSON 变量、二进制文件、目录结构(保留层级路径)三种输入 |
| 全过程可观测 | 节点状态、思考流(thinking)、工具调用、错误事件全部以 SSE 事件推送 |
| 产物可获取 | 最终输出变量、工作目录 zip 均可通过稳定 URL 拉取 |
| 可认证 | 通过 X-API-Key 头部识别调用方,可绑定工作流白名单 |
http(s)://<host>:<port>/api/v1
2438(与平台后端一致,见 application.yml: server.port)/api/v1 开头,与平台内部 /api/workflows、/api/run-history 隔离,便于独立鉴权| 场景 | Content-Type |
|---|---|
| 创建运行(仅 JSON 变量) | application/json |
| 创建运行(含文件/目录上传) | multipart/form-data |
| 订阅运行流(SSE) | 响应 text/event-stream |
| 查询运行结果 | 响应 application/json |
| 下载工作空间 | 响应 application/zip |
所有 /api/v1/** 接口必须携带 API Key:
X-API-Key: <预共享密钥>
application.yml 中通过 external.api-keys 列表配置,可配置多组workflowIds 白名单(不填表示可调用所有工作流)缺失或不匹配 → HTTP 401 Unauthorized,响应体:
{ "code": "UNAUTHORIZED", "message": "API Key 缺失或无效" }
非 SSE 接口统一返回:
{
"code": "OK",
"message": "",
"data": { ... }
}
错误时:
{
"code": "<ERROR_CODE>",
"message": "<可读错误信息>",
"data": null
}
SSE 接口(/stream)的认证与业务错误通过事件 payload(见 §5)传递,不使用 HTTP 状态码包络。
| 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 | 服务器内部错误(堆栈已脱敏) |
| 名称 | 类型 | 说明 |
|---|---|---|
workflowId |
number | 工作流主键(路径参数) |
runId |
string | 每次运行的唯一标识,8 位十六进制(如 a3f9b2c1),由服务端生成,贯穿工作目录、SSE 事件、下载 URL |
| # | 方法 | 路径 | 用途 |
|---|---|---|---|
| 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 改为「提交 → 启动 → 订阅」三段式,以满足「思考过程通过另一个接口获取」的诉求,并支持客户端重连。
请求
POST /api/v1/workflows/{workflowId}/runs
X-API-Key: <key>
Content-Type: application/json | multipart/form-data
两种调用形态
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
}
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;含 / 时自动创建目录结构(目录上传场景) |
目录上传约定: 客户端在前端使用
<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 的输出。
仅当 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 条事件用于断线重连补发。
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 后关闭SseEmitter(1_800_000L) 一致)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 条结果..." }
]
}
}
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_Store、Thumbs.db)code = RUN_NOT_FOUNDGET /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" }
]
}
]
}
}
inputs 与 outputs 由服务端扫描 graphData 中 userInput 与 output 节点的字段定义自动推导,便于第三方系统展示表单。
所有事件格式:
event: <事件名>
id: <单调递增整数>
data: <JSON 对象>
| 事件名 | 触发时机 | 是否终止流 |
|---|---|---|
run_started |
执行引擎启动 | 否 |
node_status |
节点状态变更 | 否 |
node_stream |
节点流式增量(thinking / tool_call / tool_result) | 否 |
workflow_complete |
工作流成功完成 | 是 |
workflow_error |
工作流出错 | 是 |
run_started{
"runId": "a3f9b2c1",
"workflowId": 12,
"timestamp": "2026-07-01T08:30:05Z"
}
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 字段。
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 |
工具调用返回(结果摘要) | 工具名 |
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"
}
workflow_error{
"runId": "a3f9b2c1",
"type": "workflow_error",
"success": false,
"error": "节点 node-3 执行失败:LLM 调用超时",
"failedNodeId": "node-3",
"completedAt": "2026-07-01T08:31:00Z"
}
客户端 服务端
│ │
│── 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 ───────────────────│
客户端在事件 id=42 处断开,重连:
│── GET /runs/{runId}/stream ───────────────────────▶│
│ Last-Event-ID: 42 │
│◀── event: node_status (重发 id=43..) ──────────────│
适合简单集成,一次请求直接返回 SSE 流:
│── POST /runs (async=false) ───────────────────────▶│
│◀── event: run_started ────────────────────────────│
│◀── event: node_stream ... ────────────────────────│
│◀── event: workflow_complete ──────────────────────│
| 项目 | 限制 | 说明 |
|---|---|---|
| 单次执行超时 | 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 |
服务端在 /runs 创建时或 /start 启动时会校验 graphData:
userInput 类型节点output 类型节点userInput 到任一 output 必须存在有向路径,且路径上至少包含 1 个中间节点(直接 input → output 边不满足)不满足时返回 GRAPH_INVALID(HTTP 422),错误信息指明具体原因。
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
}'
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。
# 启动
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
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
| 外部接口 | 内部接口 | 备注 |
|---|---|---|
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,便于后续按版本演进。