任务与批次
1. 提交任务
POST /v1/jobs
Authorization: Bearer zqa_live_xxx
Idempotency-Key: order-20261109-001
Content-Type: application/json{
"batchId": "sprint-42",
"items": [
{
"kind": "ai",
"projectId": "proj_order",
"prompt": "检查 src 下所有 TODO 并生成 reports/todo.md",
"mode": "auto",
"priority": 10,
"maxAttempts": 4,
"externalId": "TASK-1024",
"metadata": { "owner": "team-a" },
"callbackUrl": "https://team.cd-zq.com/api/zqagent/webhook"
}
]
}字段
| 字段 | 必填 | 说明 |
|---|---|---|
kind | ✓ | ai(跑一轮 agent)或 script(跑一条命令) |
projectId | ✓ | 目标工作区,从 GET /v1/projects 取 |
nodeId | 不填则按 projectId 反查;跨节点重名时必须填 | |
prompt | kind=ai | 任务描述(≤8000 字符) |
command | kind=script | 命令(≤8000 字符) |
timeoutSeconds | 仅 script,1–300,默认 60 | |
mode | auto(默认)/ discuss / plan / interactive。无人值守请用 auto | |
priority | -100–100,数值大者先跑 | |
maxAttempts | 1–4,默认 4 | |
externalId | 你系统里的任务 id,原样回传(≤128) | |
metadata | 任意 JSON,原样回传(≤2KB) | |
callbackUrl | 覆盖 Key 的默认回调地址 |
响应
202 Accepted
{
"data": {
"batchId": "sprint-42",
"accepted": 1,
"items": [
{ "id": "job_9f2c", "externalId": "TASK-1024", "status": "queued",
"nodeId": "node_be", "projectId": "proj_order" }
]
},
"requestId": "req_..."
}注意:202 只代表已受理。真正的完成信号来自 webhook 或轮询详情。
整批原子性
任一条不合法(缺 prompt、projectId 不存在、节点离线……)→ 整批 400,一条都不入队。 这样你不需要处理"一半成功"的中间态。
幂等
同一 Idempotency-Key 在 24 小时内重复提交,返回首次响应(HTTP 200 而非 202),不会重复建任务。
为什么必须用:agent 会真实改文件、跑命令。网络抖动时如果你的系统自动重试, 没有幂等键就可能执行两遍。
建议:直接用业务单号(order-20261109-001)。
批次 id
不传 batchId 时服务端自动生成(batch_xxxx)。 同一次提交的所有任务共享它,之后可用 GET /v1/batches/{batchId} 一次拿到整批进度。
2. 查询任务
列表
GET /v1/jobs?status=failed&projectId=proj_order&limit=20| 参数 | 说明 |
|---|---|
status | queued / running / done / failed / cancelled |
nodeId / projectId | 按节点/项目过滤 |
batchId | 按批次过滤 |
externalId | 按你系统里的任务 id 精确定位(最常用) |
limit | 默认 50,上限 200 |
按时间倒序返回,nextCursor 为上一页最后一条的 queuedAt。
推荐用法:用 externalId 精确定位单条,而不是翻页找。
详情
GET /v1/jobs/job_9f2c{
"data": {
"id": "job_9f2c",
"externalId": "TASK-1024",
"kind": "ai",
"status": "failed",
"nodeId": "node_be",
"nodeLabel": "后端构建机",
"projectId": "proj_order",
"batchId": "sprint-42",
"title": "检查 src 下所有 TODO 并生成 reports/todo.md",
"attempt": 2,
"maxAttempts": 4,
"priority": 10,
"queuedAt": "2026-11-09T10:05:00Z",
"startedAt": "2026-11-09T10:05:12Z",
"finishedAt": "2026-11-09T10:12:03Z",
"durationMs": 411000,
"waitMs": 12000,
"nextRetryAt": "2026-11-09T10:14:00Z",
"sessionId": "sess_x1",
"result": {
"summary": "已生成 reports/todo.md,共 23 项",
"producedFiles": ["reports/todo.md"],
"usage": { "inputTokens": 21000, "outputTokens": 3400 },
"iterations": 12,
"toolCalls": 18
},
"failure": {
"type": "llm_error",
"message": "本轮未正常完成(error):RateLimitError",
"lastError": "429 rate limited"
},
"blocked": null,
"progress": {
"toolCalls": 18,
"tokensIn": 21000,
"tokensOut": 3400,
"iterations": 12,
"lastActivity": "调用工具 write_file",
"updatedAt": "2026-11-09T10:11:58Z"
},
"metadata": { "owner": "team-a" }
},
"requestId": "req_..."
}关键字段
| 字段 | 用途 |
|---|---|
attempt / maxAttempts | 第几次尝试;attempt < maxAttempts 说明还会重试 |
nextRetryAt | 已安排自动重试时,下次开始时间 |
failure.type | 机器可读的失败分类,见下节 |
failure.lastError | 自动重试时上一轮的原因(message 是当前轮的) |
blocked | 排队中且目标节点离线时出现,见下节 |
sessionId | 在控制台点进去可看完整执行过程(工具调用、改动文件) |
result.producedFiles | 本轮改动的文件(工作区相对路径) |
progress | 运行进度,节点心跳(10s)上报;仅 running 时更新,终态后保留最后一份 |
3. 失败类型(重试策略)
failure.type | 含义 | 自动重试 |
|---|---|---|
node_lost | 执行节点失联(租约到期) | ✅ |
llm_error | 模型侧错误;仅 retriable=true(429/5xx/网络)才重试 | ✅ 条件 |
engine_error | 引擎/框架异常 | ❌ |
timeout | 节点侧整轮超时(AI 25 分钟上限) | ❌ |
max_iterations | 达到最大迭代次数 | ❌ |
project_not_found | 目标项目在该节点上不存在 | ❌ |
rejected | 因未决审批等原因被拒绝提交 | ❌ |
cancelled | 被取消 | ❌ |
script_failed | 脚本退出码非 0 | ❌ |
为什么 timeout / max_iterations 不重试:同样的输入与同样的上限, 重跑必然再撞一次,只是白烧额度。需要你调整 prompt 或拆小任务。
重试次数:共 maxAttempts 次(默认 4,含首次)。退避 30s → 2min → 10min。
⚠️ 副作用提醒:node_lost 重试时,上一次可能已经改了一半文件。 每次重试都是新会话(不继承上下文),但工作区改动是累积的。 需要严格回滚时,可在项目上开启变更追踪,用 sessionId 查每轮 diff。
3.1 实时跟进:事件流(SSE)
轮询是最简单的方式,但长任务(几分钟到几十分钟)用轮询既费请求也慢半拍。 GET /v1/jobs/{id}/events 直接推:
curl -N https://hub.example.com/v1/jobs/job_9f2c/events \
-H "Authorization: Bearer $ZQA_API_KEY": ping
id: 1
event: job.state
data: {"id":"job_9f2c","status":"running","progress":{"toolCalls":3,...}}| event | 何时发 |
|---|---|
job.state | 连接建立时先发一次当前状态(可能在跑,不能等变化) |
job.progress | 仍在 running,但进度/尝试次数变了 |
job.completed | status = done(随后服务端关闭连接) |
job.failed | status = failed 且没有待重试(随后关闭) |
job.cancelled | 已取消(随后关闭) |
job.deleted | 作业记录已不存在(随后关闭) |
- 每 15 秒发一次注释行
: ping保活(反代常在 60 秒空闲后断流)。 - 断线重连带
Last-Event-ID(或?since=N),从上次的id之后继续编号。 - 不是逐 token 流:Hub 到节点没有入站通道,这里推的是"状态 + 进度摘要"。 要看逐条工具调用,用
sessionId到控制台看。
建议的接入方式
| 场景 | 方式 |
|---|---|
| 任务管理系统(要跨重启、要审计) | webhook 为主,GET /v1/jobs/{id} 兜底对账 |
| 前台等结果、要实时进度条 | SSE |
| 脚本 / 一次性任务 | GET /v1/jobs/{id} 轮询,3–5 秒一次 |
4. 取消与重试
POST /v1/jobs/job_9f2c/cancel
POST /v1/jobs/job_9f2c/retry- 取消:排队中立即生效;运行中只改状态,节点手里那一轮无法远程打断(结果会被丢弃)。
- 已结束的取消返回
409 job_finished。 - 重试仅对
failed/cancelled生效,返回409 job_not_retryable否则。 - 重试会清零自动重试配额(你主动干预时信任你)。
5. 批次汇总
GET /v1/batches/sprint-42{
"data": {
"batchId": "sprint-42",
"total": 100,
"counts": { "queued": 60, "running": 3, "done": 34, "failed": 2, "cancelled": 1 },
"progress": 0.37,
"estimatedRemainingMs": 5400000,
"byProject": [
{ "projectId": "proj_order", "total": 40, "done": 20, "failed": 1,
"cancelled": 0, "queued": 19, "running": 1, "avgDurationMs": 310000 },
{ "projectId": "proj_web", "total": 60, "done": 14, "failed": 1,
"cancelled": 1, "queued": 41, "running": 2, "avgDurationMs": 240000 }
],
"failures": [
{ "jobId": "job_9f2c", "externalId": "TASK-17",
"failureType": "llm_error", "message": "429 rate limited", "attempt": 2 }
]
},
"requestId": "req_..."
}progress= 已到终态(done + failed + cancelled)/ 总数estimatedRemainingMs取最慢项目的剩余时间(跨项目并行,串行只发生在项目内)failures最多返回 100 条
整批全部到终态时,会额外推送一次 batch.completed webhook(见 webhooks.md)。
整批取消:POST /v1/batches/sprint-42/cancel → { "cancelled": 37 }。
6. 常见坑
6.1 projectId 跨节点可能重名
projectId 是节点本地的租户 id。两个节点上可能都存在 proj_order, 但指向不同的目录。此时不传 nodeId 会返回:
{ "error": { "code": "ambiguous_project",
"message": "项目 proj_order 存在于多个节点(node_a, node_be),请显式指定 nodeId" } }建议:建项目时不要手填 id,让服务端生成(或统一加前缀 be- / fe-)。
6.2 指定节点的任务不会改派
节点离线时提交会被拒绝(409 node_offline)。已经提交的任务如果目标节点 中途离线,会一直排队,详情里出现:
"blocked": { "reason": "node_offline", "nodeId": "node_be", "lastSeen": "2026-11-09T08:30:00Z" }GET /v1/overview 的 load.stalledQueued 会随之增长。
处理方式:节点恢复后自动开始跑;或取消后重新提交到别的节点。 (刻意不做自动改派:前端构建这类任务必须在指定机器上跑。)
6.3 提交成功不等于执行成功
Hub 侧的校验(项目存在、节点在线)是尽力而为: 它基于节点最近一次上报的快照(约 5 秒延迟,网关不可达时 30 秒后才判离线)。 最终以节点执行时的结果为准。
