NanoCloud 私有化材料

开放接口文档

飞书 Wiki 同步子文档,revision 5。

开放接口文档

1. 使用约定

1.1 地址、鉴权与公共请求头

export BASE_URL='https://api.'
export OPEN_TOKEN=[REDACTED]'

除 POST /open/auth/v1/token 外,其余 20 个接口都必须携带:

Authorization: Bearer 

OPEN_TOKEN 是 nco_ 开头、总长 47 的不透明令牌,不是 JWT,只能调用 /open/*。
JSON 写接口还需携带 Content-Type: application/json。当前只有创建任务接口强制要求
Idempotency-Key。

dev 环境示例

后文每个接口末尾的「dev 环境示例」都在 dev 集群实际执行过,响应是 2026-09-16 修复后的复验结果,令牌已打码。先在终端设置变量:

export BASE_URL='https://api.nanocloud-cluster.x2robot.cn'
export TENANT_ID=10100000
export APP_KEY=[REDACTED]
export OPEN_USER='ncadmin@nanocloud'
# SK 和 ncadmin 口令向环境负责人索取,只在终端读入,不要写进文档、脚本或截图
read -rs APP_SECRET
read -rs OPEN_PASSWORD

取令牌见 2.1,成功后令牌保存在 OPEN_TOKEN。后续示例从上一步响应里取 ID 存进变量,建议按以下顺序执行:

  1. 2.1 取令牌;
  2. 5.1、5.2、6.1 查采集员、标签和设备,得到 COLLECTOR_ID、COLLECTION_TAG_ID、PROJECT_TAG_ID、DEVICE_ID;
  3. 4.1 创建技能,得到 TASK_ID;
  4. 3.2 创建定制任务,得到 JOB_ID,再依次执行 3.5、3.7、3.9、3.10;
  5. 驳回、关闭、自由采集和恢复采集各用独立任务,见 3.8、3.11 和 3.2。

dev 环境的限制:

  • AK nanocloud 只允许代表 ncadmin@nanocloud,其它账号取令牌返回 30003。审核和分发都用同一个令牌;ncadmin 在审核组里,可以审核自己创建的任务。「非审核人审核返回 60003」这类需要两个身份的用例,dev 上暂时测不了。
  • 示例响应里的 job-ddky…、tsk-ddky… 是实测时新建的,状态已经改变,不要照抄,按步骤自己创建。
  • 示例创建的数据名称都带 qa-demo- 前缀;自己测试时也建议加统一前缀,便于识别和清理。

1.2 统一响应

成功:

{"code":0,"data":{}}

失败:

{"code":10001,"message":"required field is missing"}
HTTPcode含义
4001xxxx参数缺失、枚举非法、字段组合不合法
4012xxxxAK/SK、用户口令或 OPEN_TOKEN 无效
4033xxxx代表关系、租户或业务权限不满足
4044xxxx任务、技能、字典对象或路由不存在
40545001HTTP 方法不允许
4095xxxx幂等冲突、同键请求在途或任务状态冲突
4226xxxx审核组、质检组、技能、标签或上传位置前置条件不满足
5027xxxx上游异常、超时或不可达
4298xxxx轮询限流;当前代码未接通
50090001本服务内部错误

如果响应包含 data.upstream,它仅供排障,不得作为测试断言或客户端分支条件。

1.3 JSON 类型与分页

  • expires_in、tenant_id、total、各计数字段是 JSON 数字。
  • deadline、created_at、updated_at、last_connected_at 是 Unix 秒级时间戳的字符串。
  • 所有列表接口都接受 page、page_size;page_size > 100 返回 HTTP 400 / 10006。
  • 未传或传 0 时,由下游使用默认分页值;测试不要把默认页大小写成固定断言。

2. 鉴权接口

2.1 获取 OPEN_TOKEN

POST /open/auth/v1/token,匿名接口。

请求体:

参数类型必填说明
app_keystring是调用系统 AK;无效或停用返回 20001
app_secretstring是调用系统 SK 明文;服务只与 bcrypt 摘要比对
tenant_idint32否目标租户;缺省使用 AK 的默认租户,仍无法确定时返回 10008
usernamestring是被代表的平台账号,须在 AK 的允许列表中
passwordstring是被代表账号口令;只用于即时登录,不持久化、不回显

请求示例:

curl -skS -X POST "$BASE_URL/open/auth/v1/token" \
  -H 'Content-Type: application/json' \
  -d '{
    "app_key":"qa-client",
    "app_secret":"",
    "tenant_id":10100000,
    "username":"system_integrator",
    "password":""
  }'

成功响应 data:

字段类型说明
access_tokenstring新签发的 OPEN_TOKEN
token_typestring固定为 bearer
expires_inint32实际剩余秒数;客户端必须读取,不得硬编码
tenant_idint32本令牌绑定的租户
usernamestring本令牌代表的账号
{
  "code":0,
  "data":{
    "access_token":"nco_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "token_type":"bearer",
    "expires_in":257400,
    "tenant_id":10100000,
    "username":"system_integrator"
  }
}

重点反例:空 AK/SK、空用户名、空口令均为 10001;错误 AK/SK 为 20001;错误账号口令为
20006;AK 无权代表指定账号为 30003。

dev 环境示例(2026-09-16 修复后复验)

RESP=$(curl -sS -X POST "$BASE_URL/open/auth/v1/token" \
  -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg ak "$APP_KEY" --arg sk "$APP_SECRET" --argjson tid "$TENANT_ID" \
        --arg user "$OPEN_USER" --arg pw "$OPEN_PASSWORD" \
        '{app_key:[REDACTED],app_secret:[REDACTED],tenant_id:$tid,username:$user,password:[REDACTED]
# 打印时遮住令牌,避免出现在屏幕和截图里
echo "$RESP" | jq 'if .code == 0 then .data.access_token = "nco_***" else . end'
export OPEN_TOKEN=[REDACTED] "$RESP" | jq -r '.data.access_token // empty')
{
  "code": 0,
  "data": {
    "access_token": "nco_***",
    "token_type": "bearer",
    "expires_in": 257399,
    "tenant_id": 10100000,
    "username": "ncadmin@nanocloud"
  }
}

反例实测(前 4 条改取令牌的请求体,后 2 条调用 GET /open/collect/v1/operators):

场景请求差异HTTP响应
请求体为空{}400{"code":10001,"message":"app_key and app_secret are required"}
SK 错误app_secret 填错401{"code":20001,"message":"app_key or app_secret is invalid or revoked; do not retry"}
账号口令错误password 填错401{"code":20006,"message":"username or password of the represented account is incorrect","data":{"upstream":{"message":"账号不存在或登录凭证错误","reason":"UNAUTHORIZED"}}}
账号不在允许列表username 换成 nctest0@nanocloud403{"code":30003,"message":"app_key \"nanocloud\" is not allowed to represent \"nctest0@nanocloud\" in tenant 10100000"}
业务接口不带令牌去掉 Authorization 请求头401{"code":20005,"message":"missing Authorization header; expected: Authorization: Bearer "}
令牌不存在Bearer nco_ 后接 43 个 A401{"code":20002,"message":"token has expired or was never issued; obtain a new one and replay the request once"}

3. 任务接口

3.1 任务类型、状态和公共返回

job_type:

名称JSON 值说明
CUSTOM_COLLECTION"CUSTOM_COLLECTION"定制采集,必须有技能,需审核和分发
RECOVERY_COLLECTION"RECOVERY_COLLECTION"恢复采集,必须有技能,需审核和分发
FREE_COLLECTION"FREE_COLLECTION"自由采集,禁止技能和多样性,无审核和分发

status:

名称数值说明
PENDING_REVIEW5待审核
REJECTED6已驳回,可重提
PENDING_EXECUTION9已分发、待设备执行
PUBLISHED10采集中
PENDING_DISPATCH15审核通过、待分发
PAUSED20已停止,通过 dispatch 恢复
CLOSED90已关闭,终态

写操作通常返回 JobBrief:

字段类型说明
job_idstring任务唯一 ID
job_namestring任务名;部分动作响应可能为空
statusenum当前对外状态
stagestring编排阶段,如 JOB_CREATED、REVIEW_APPROVED、RESUBMITTED、DISPATCHED、STOPPED、CLOSED
next_actionsstring[]当前建议动作;调用方优先据此展示按钮

3.2 创建任务

POST /open/collect/v1/jobs

请求头:

参数必填说明
Authorization是集成账号的 Bearer
Idempotency-Key是建议 UUID;同 AK + 同 key + 同 body 返回同一 job_id
Content-Type是application/json

请求体:

参数类型必填规则
job_typeenum是三种任务类型之一
task_idstring条件必填定制/恢复必填且技能应启用;自由采集禁止传
device_typestring是设备型号,大小写不敏感;可从设备列表获得
target_countint32是Episode 目标数,必须大于 0
label_tag_idsstring[]是必须恰好 2 个:collection、project 两维各一个
exp_total_valid_cntint32否自由采集有效数阈值;0 表示不启用自动停止
diversity_idstring否定制/恢复可传;自由采集禁止传
scene_tag_idsstring[]否仅自由采集生效
job_namestring否任务展示名
priorityint32否优先级,当前交由上游处理
deadlinestring否Unix 秒级时间戳字符串;非法字符串当前会被转为 0 交给上游
qc_requirementstring否质检要求
descriptionstring否任务描述
notesstring否备注
recommend_scoreint32否推荐分,当前交由上游处理

定制任务示例:

IDEM_KEY="$(uuidgen)"
curl -skS -X POST "$BASE_URL/open/collect/v1/jobs" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H "Idempotency-Key: $IDEM_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "job_type":"CUSTOM_COLLECTION",
    "task_id":"",
    "device_type":"",
    "target_count":10,
    "label_tag_ids":["",""],
    "job_name":"open-api-qa-custom",
    "description":"Open API QA"
  }'

自由任务示例:

curl -skS -X POST "$BASE_URL/open/collect/v1/jobs" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d '{
    "job_type":"FREE_COLLECTION",
    "device_type":"RD001",
    "target_count":10,
    "exp_total_valid_cnt":8,
    "label_tag_ids":["",""],
    "scene_tag_ids":[""],
    "job_name":"open-api-qa-free"
  }'

响应示例:

{"code":0,"data":{"job_id":"","job_name":"open-api-qa-custom","status":"PENDING_REVIEW","stage":"JOB_CREATED","next_actions":["approve","reject","close"]}}

当前实现注意:服务只本地校验标签数量,不校验两个 ID 的实际维度、租户和启用状态;设备型号与任务类型的
双向兼容规则也未在本层校验。自由采集虽写入自动停止观察记录,但没有 reconciler,不能断言达到阈值后自动停止。

dev 环境示例(2026-09-16 修复后复验)

定制任务。TASK_ID 来自 4.1,两个标签 ID 来自 5.2;dev 上定制和恢复采集用 EX001、CX001 这类非自由采集型号:

IDEM_KEY=$(uuidgen)
BODY=$(jq -nc --arg task "$TASK_ID" --arg c "$COLLECTION_TAG_ID" --arg p "$PROJECT_TAG_ID" \
  --arg name "qa-demo-custom-$(date +%Y%m%d%H%M)" \
  '{job_type:"CUSTOM_COLLECTION",task_id:$task,device_type:"EX001",target_count:2,
    label_tag_ids:[$c,$p],job_name:$name,description:"qa-demo 定制任务主链路"}')
RESP=$(curl -sS -X POST "$BASE_URL/open/collect/v1/jobs" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H "Idempotency-Key: $IDEM_KEY" \
  -H 'Content-Type: application/json' -d "$BODY")
echo "$RESP" | jq .
JOB_ID=$(echo "$RESP" | jq -r '.data.job_id')
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "qa-demo-custom-202609160020",
    "status": "PENDING_REVIEW",
    "stage": "JOB_CREATED",
    "next_actions": ["approve", "reject", "close"]
  }
}

用同一个 IDEM_KEY 和同一个 BODY 再发一次,返回同一个 job_id 和完整任务名,不会新建任务:

{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "qa-demo-custom-202609160020",
    "status": "PENDING_REVIEW",
    "stage": "JOB_CREATED",
    "next_actions": ["approve", "reject", "close"]
  }
}

同一个 IDEM_KEY 改 description 再发,返回 HTTP 409:

{
  "code": 50001,
  "message": "Idempotency-Key reused with a different request body"
}

请求指纹已覆盖 job_name、description、notes 和 target_count;只要这些字段变化,复用同一幂等键就返回 50001。

自由采集。不传 task_id,型号用 RD001 这类自由采集专属型号;建单后直接是 PENDING_EXECUTION:

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg c "$COLLECTION_TAG_ID" --arg p "$PROJECT_TAG_ID" --arg name "qa-demo-free-$(date +%Y%m%d%H%M)" \
        '{job_type:"FREE_COLLECTION",device_type:"RD001",target_count:2,exp_total_valid_cnt:2,
          label_tag_ids:[$c,$p],job_name:$name}')" | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6rcv4sfn4wz",
    "job_name": "qa-demo-free-202609160020",
    "status": "PENDING_EXECUTION",
    "stage": "JOB_CREATED",
    "next_actions": ["stop", "close"]
  }
}

恢复采集。请求体与定制任务相同,只把 job_type 换成 RECOVERY_COLLECTION(示例型号 CX001):

{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6ruk6vywkr6",
    "job_name": "qa-demo-recovery-202609160020",
    "status": "PENDING_REVIEW",
    "stage": "JOB_CREATED",
    "next_actions": ["approve", "reject", "close"]
  }
}

反例实测:

场景HTTP响应
不带 Idempotency-Key400{"code":10001,"message":"Idempotency-Key header is required"}
Idempotency-Key 超过 128 字符400{"code":10009,"message":"Idempotency-Key must be at most 128 characters"}
换幂等键但复用同名 job_name400{"code":10009,"message":"upstream '/core/api/job/v1/approve-create' rejected the request: job_name already exists: qa-demo-custom-202609160020","data":{"upstream":{"message":"error: code = 400 reason = INVALID_PARAMS message = job_name already exists: qa-demo-custom-202609160020 metadata = map[] cause = ","reason":"JOB_INVALID_PARAMS"}}}
label_tag_ids 只传一个400{"code":10004,"message":"label_tag_ids is required: exactly one collection-category and one project-category tag"}
定制任务用自由采集型号 RD001400{"code":10005,"message":"device_type RD001 is only supported by FREE_COLLECTION"}
标签传分类节点 collection_category、project_category 的 ID422{"code":60008,"message":"label tag tag-ddewsefzqwxdr7gf is not an enabled leaf tag available to this tenant; pick tag_id from GET /open/collect/v1/label-tags"}

型号与标签在调用上游前本地校验;其它上游 400 业务拒绝统一返回 400 / 10009,不再伪装成 502。

3.3 查询任务详情

GET /open/collect/v1/jobs/{job_id}

参数位置类型必填说明
job_idpathstring是创建任务返回的 ID
curl -skS "$BASE_URL/open/collect/v1/jobs/" \
  -H "Authorization: Bearer $OPEN_TOKEN"

data 字段:

字段类型说明
job_id、job_namestringID 与名称
job_typeenum对外任务类型
statusenum对外状态;业务逻辑使用它而不是 status_code
status_codeint32上游内部状态,仅排障使用
next_actionsstring[]当前可执行动作
completedbool完成判定;不要仅靠 status 判断
progress.thresholdint32完成阈值
progress.threshold_fieldstring定制/恢复为 target_count,自由为 exp_total_valid_cnt
progress.collected_countint32当前实现等于 valid_count
progress.uploaded_countint32当前固定为 0
progress.valid_countint32有效 Episode 数
device_type、task_idstring设备型号、技能 ID
label_tag_idsstring[]任务绑定的采集维与项目维标签;列表摘要可能为空,详情会补齐
deadline、descriptionstring建单配置中的截止时间与描述;未设置时为空字符串
created_at、updated_atstringUnix 秒级时间戳字符串
{"code":0,"data":{"job_id":"","job_name":"open-api-qa-custom","job_type":"CUSTOM_COLLECTION","status":"PENDING_REVIEW","status_code":5,"next_actions":["approve","reject","close"],"completed":false,"progress":{"threshold":10,"threshold_field":"target_count","collected_count":0,"uploaded_count":0,"valid_count":0},"device_type":"","task_id":"","label_tag_ids":["",""],"deadline":"","description":"QA 定制任务","created_at":"1789344000","updated_at":"1789344000"}}

dev 环境示例(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/jobs/$JOB_ID" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "qa-demo-custom-202609160020",
    "job_type": "CUSTOM_COLLECTION",
    "status": "PENDING_REVIEW",
    "status_code": 5,
    "next_actions": ["approve", "reject", "close"],
    "completed": false,
    "progress": {
      "threshold": 2,
      "threshold_field": "target_count",
      "collected_count": 0,
      "uploaded_count": 0,
      "valid_count": 0
    },
    "device_type": "EX001",
    "task_id": "tsk-ddkybulkf4tiiv4r",
    "label_tag_ids": ["tag-ddewsegd2ms6myxs", "tag-ddewsegdbtjz5uyc"],
    "deadline": "",
    "description": "qa-demo 定制任务主链路",
    "created_at": "1789489232",
    "updated_at": "1789489233"
  }
}

device_type、description、deadline 和 label_tag_ids 已从上游任务配置与标签关系补齐。

查询不存在的任务 job-notexist000000,返回 HTTP 404,应为 404 / 40001:

{
  "code": 40001,
  "message": "upstream '/core/api/job/v1/info' rejected the request: job not found: job-notexist000000",
  "data": {
    "upstream": {"message": "job not found: job-notexist000000", "reason": "JOB_NOT_FOUND"}
  }
}

3.4 查询任务列表

GET /open/collect/v1/jobs

参数位置类型必填说明
keywordquerystring否按任务名模糊匹配
statusqueryenum否按对外状态过滤,例如 PENDING_REVIEW
pagequeryint32否页码
page_sizequeryint32否每页数量,上限 100
curl -skS "$BASE_URL/open/collect/v1/jobs?keyword=open-api-qa&status=PENDING_REVIEW&page=1&page_size=20" \
  -H "Authorization: Bearer $OPEN_TOKEN"
{"code":0,"data":{"items":[{"job_id":"","job_name":"open-api-qa-custom","job_type":"CUSTOM_COLLECTION","status":"PENDING_REVIEW","status_code":5,"next_actions":["approve","reject","close"],"completed":false,"progress":{"threshold":10,"threshold_field":"target_count","collected_count":0,"uploaded_count":0,"valid_count":0},"device_type":"","task_id":"","created_at":"1789344000","updated_at":"1789344000"}],"total":1}}

已关闭任务不出现在列表中,但仍可按 ID 查询详情。

dev 环境示例(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/jobs?keyword=qa-demo&page=1&page_size=20" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq .
{
  "code": 0,
  "data": {
    "items": [
      {
        "job_id": "job-ddkyv6osv7fhx7qm",
        "job_name": "qa-demo-custom-202609160020",
        "job_type": "CUSTOM_COLLECTION",
        "status": "PENDING_REVIEW",
        "status_code": 5,
        "next_actions": ["approve", "reject", "close"],
        "completed": false,
        "progress": {
          "threshold": 2,
          "threshold_field": "target_count",
          "collected_count": 0,
          "uploaded_count": 0,
          "valid_count": 0
        },
        "device_type": "EX001",
        "task_id": "tsk-ddkybulkf4tiiv4r",
        "label_tag_ids": [],
        "deadline": "",
        "description": "",
        "created_at": "1789489232",
        "updated_at": "1789489233"
      }
    ],
    "total": 1
  }
}

3.5 查询审核流

GET /open/collect/v1/jobs/{job_id}/review-flow

参数位置类型必填说明
job_idpathstring是任务 ID
curl -skS "$BASE_URL/open/collect/v1/jobs//review-flow" \
  -H "Authorization: Bearer $OPEN_TOKEN"
响应字段类型说明
strategystring当前为 ANY,任一审核节点通过即整轮通过
nodes[].reviewerstring审核账号名
nodes[].resultstringPENDING、APPROVED 或 REJECTED
nodes[].commentstring审核意见
{"code":0,"data":{"strategy":"ANY","nodes":[{"reviewer":"system_reviewer","result":"PENDING","comment":""}]}}

内部 flow_id、node_id 不对外暴露,审核接口不需要这两个参数。

dev 环境示例(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/jobs/$JOB_ID/review-flow" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq .
{
  "code": 0,
  "data": {
    "strategy": "ANY",
    "nodes": [
      {"reviewer": "ncadmin@nanocloud", "result": "PENDING", "comment": ""},
      {"reviewer": "ncqc@nanocloud", "result": "PENDING", "comment": ""},
      {"reviewer": "nctest1@nanocloud", "result": "PENDING", "comment": ""},
      {"reviewer": "我是数采员", "result": "PENDING", "comment": ""}
    ]
  }
}

dev 审核组有 4 名成员,所以有 4 个审核节点,任一节点通过即整轮通过。reviewer 是展示字段,可能是账号名也可能是姓名;服务审核时使用 JWT 的 uid 与上游 reviewer_id 匹配,不依赖展示文本。

3.6 重新提交审核

POST /open/collect/v1/jobs/{job_id}/resubmit

参数位置类型必填说明
job_idpathstring是只允许 REJECTED 状态;PAUSED 改用 dispatch 恢复
reasonbodystring否重提原因,转为上游审核备注
curl -skS -X POST "$BASE_URL/open/collect/v1/jobs//resubmit" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reason":"修正测试数据后重提"}'
{"code":0,"data":{"job_id":"","status":"PENDING_REVIEW","stage":"RESUBMITTED","next_actions":["approve","reject","close"]}}

不允许的状态返回 HTTP 409 / 50002。

dev 环境示例(2026-09-16 修复后复验)

resubmit 只用于驳回任务。对 3.10 停止后的任务(PAUSED)调用会被拒,并提示改用 3.9 的 dispatch 恢复:

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs/$JOB_ID/resubmit" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reason":"qa-demo 验证 PAUSED 拒绝 resubmit"}' | jq .

返回 HTTP 409:

{
  "code": 50002,
  "message": "job is PAUSED(20); resume it with dispatch instead of resubmit"
}

对 3.8 驳回后的任务调用同一接口,返回 PENDING_REVIEW,并原样沿用上一轮审核流的策略与 4 个节点:

{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6qmrxgg5uls",
    "job_name": "",
    "status": "PENDING_REVIEW",
    "stage": "RESUBMITTED",
    "next_actions": ["approve", "reject", "close"]
  }
}
{
  "code": 0,
  "data": {
    "strategy": "ANY",
    "nodes": [
      {"reviewer": "ncqc@nanocloud", "result": "PENDING", "comment": ""},
      {"reviewer": "ncadmin@nanocloud", "result": "PENDING", "comment": ""},
      {"reviewer": "nctest1@nanocloud", "result": "PENDING", "comment": ""},
      {"reviewer": "我是数采员", "result": "PENDING", "comment": ""}
    ]
  }
}

3.7 审核通过

POST /open/collect/v1/jobs/{job_id}/approve

参数位置类型必填说明
job_idpathstring是待审核任务 ID
commentbodystring否审核意见

必须使用实际审核节点中账号的 OPEN_TOKEN:

curl -skS -X POST "$BASE_URL/open/collect/v1/jobs//approve" \
  -H "Authorization: Bearer $REVIEWER_OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"comment":"QA 审核通过"}'
{"code":0,"data":{"job_id":"","status":"PENDING_DISPATCH","stage":"REVIEW_APPROVED","next_actions":["dispatch","stop","close"]}}

非实际审核人返回 HTTP 422 / 60003,缺审核组或组内无人分别为 60001、60002。

dev 环境示例(2026-09-16 修复后复验)

dev 上建单人和审核人都是 ncadmin@nanocloud,直接用同一个 OPEN_TOKEN:

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs/$JOB_ID/approve" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"comment":"qa-demo 审核通过"}' | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "",
    "status": "PENDING_DISPATCH",
    "stage": "REVIEW_APPROVED",
    "next_actions": ["dispatch", "stop", "close"]
  }
}

响应会回填请求路径中的 job_id,状态进入 PENDING_DISPATCH。

3.8 驳回任务

POST /open/collect/v1/jobs/{job_id}/reject

参数与审核通过相同,也必须使用实际审核人的令牌。

curl -skS -X POST "$BASE_URL/open/collect/v1/jobs//reject" \
  -H "Authorization: Bearer $REVIEWER_OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"comment":"缺少 QA 必要说明"}'
{"code":0,"data":{"job_id":"","status":"REJECTED","stage":"REJECTED","next_actions":["resubmit"]}}

REJECTED 下只允许 resubmit;关闭接口会返回 409 / 50002。

dev 环境示例(2026-09-16 修复后复验)

另建一个定制任务(同 3.2,job_name 换一个),把新的任务 ID 存进 JOB_ID 后驳回:

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs/$JOB_ID/reject" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"comment":"qa-demo 缺少必要说明"}' | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6qmrxgg5uls",
    "job_name": "",
    "status": "REJECTED",
    "stage": "REJECTED",
    "next_actions": ["resubmit"]
  }
}

驳回后唯一建议动作是 resubmit;关闭会返回 409 / 50002,见 3.11。

3.9 分发任务

POST /open/collect/v1/jobs/{job_id}/dispatch

参数位置类型必填说明
job_idpathstring是通常应为 PENDING_DISPATCH
start_datebodystring是YYYY-MM-DD;当前只校验非空,具体格式交上游
collector_idsbodystring[]是采集员 ID,来自采集员列表
device_idsbodystring[]是设备 ID,来自设备列表
qc_inspector_idsbodystring[]否质检员 ID;空数组时由平台自动指派并排除采集员
curl -skS -X POST "$BASE_URL/open/collect/v1/jobs//dispatch" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "start_date":"2026-09-15",
    "collector_ids":[""],
    "device_ids":[""],
    "qc_inspector_ids":[]
  }'
{"code":0,"data":{"job_id":"","status":"PENDING_EXECUTION","stage":"DISPATCHED","next_actions":["stop","close"]}}

缺采集员或设备数组返回 10001。自动指派质检员时,缺组、组内无人、排除采集员后无候选分别返回
60004、60005、60006。upload_env 不是接口参数,由部署配置决定。

dev 环境示例(2026-09-16 修复后复验)

采集员用 5.1 里的 nctest0@nanocloud(opr-ddchzvxizsyejiy7),设备用 6.1 里型号为 EX001 的 dev-ddcrr72ambcqhwr2;qc_inspector_ids 为空时由平台自动指派质检员:

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs/$JOB_ID/dispatch" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg day "$(date +%F)" --arg o "$COLLECTOR_ID" --arg d "$DEVICE_ID" \
        '{start_date:$day,collector_ids:[$o],device_ids:[$d],qc_inspector_ids:[]}')" | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "",
    "status": "PENDING_EXECUTION",
    "stage": "DISPATCHED",
    "next_actions": ["stop", "close"]
  }
}

分发后查询详情(3.3),status_code 为 9:

{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "qa-demo-custom-202609160020",
    "job_type": "CUSTOM_COLLECTION",
    "status": "PENDING_EXECUTION",
    "status_code": 9,
    "next_actions": ["stop", "close"],
    "completed": false,
    "progress": {
      "threshold": 2,
      "threshold_field": "target_count",
      "collected_count": 0,
      "uploaded_count": 0,
      "valid_count": 0
    },
    "device_type": "EX001",
    "task_id": "tsk-ddkybulkf4tiiv4r",
    "label_tag_ids": ["tag-ddewsegd2ms6myxs", "tag-ddewsegdbtjz5uyc"],
    "deadline": "",
    "description": "qa-demo 定制任务主链路",
    "created_at": "1789489232",
    "updated_at": "1789489233"
  }
}

3.10 停止任务

POST /open/collect/v1/jobs/{job_id}/stop

参数位置类型必填说明
job_idpathstring是任务 ID
reasonbodystring否停止原因
curl -skS -X POST "$BASE_URL/open/collect/v1/jobs//stop" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reason":"QA 停止验证"}'
{"code":0,"data":{"job_id":"","status":"PAUSED","stage":"STOPPED","next_actions":["dispatch"]}}

停止与关闭互斥:停止后不能关闭,也不能重新提交审核;调用 dispatch 走平台重启路径恢复。定制/恢复采集会在同一次调用中重新分发,自由采集直接回到 PENDING_EXECUTION。

dev 环境示例(2026-09-16 修复后复验)

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs/$JOB_ID/stop" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reason":"qa-demo 停止验证"}' | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "",
    "status": "PAUSED",
    "stage": "STOPPED",
    "next_actions": ["dispatch"]
  }
}

停止响应会回填 job_id 和 PAUSED,next_actions 只给出 dispatch。沿用 3.9 的分发请求体恢复定制任务后,同一次调用完成重启和重新分发:

{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6osv7fhx7qm",
    "job_name": "",
    "status": "PENDING_EXECUTION",
    "stage": "DISPATCHED",
    "next_actions": ["stop", "close"]
  }
}

自由采集任务不需要采集员或设备,传空对象即可恢复为 PENDING_EXECUTION:

{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6rcv4sfn4wz",
    "job_name": "",
    "status": "PENDING_EXECUTION",
    "stage": "RESTARTED",
    "next_actions": ["stop", "close"]
  }
}

3.11 关闭任务

POST /open/collect/v1/jobs/{job_id}/close

参数位置类型必填说明
job_idpathstring是只允许状态 5、9、15
reasonbodystring否proto 接受,但当前 service 不向上游传递
curl -skS -X POST "$BASE_URL/open/collect/v1/jobs//close" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reason":"QA 撤单验证"}'
{"code":0,"data":{"job_id":"","status":"CLOSED","stage":"CLOSED","next_actions":[]}}

关闭是终态;关闭后列表中不可见,仍可按 ID 查详情。不允许的状态返回 50002。

dev 环境示例(2026-09-16 修复后复验)

关闭一个待审核(PENDING_REVIEW)的任务:

curl -sS -X POST "$BASE_URL/open/collect/v1/jobs/$JOB_ID/close" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' -d '{}' | jq .
{
  "code": 0,
  "data": {
    "job_id": "job-ddkyv6ruk6vywkr6",
    "job_name": "",
    "status": "CLOSED",
    "stage": "CLOSED",
    "next_actions": []
  }
}

关闭响应会回填 job_id 和 CLOSED。

对停止后(PAUSED)的任务关闭,返回 HTTP 409:

{
  "code": 50002,
  "message": "close is only allowed in PENDING_REVIEW(5)/PENDING_DISPATCH(15)/PENDING_EXECUTION(9); current=20"
}

对驳回后(REJECTED)的任务关闭,返回 HTTP 409:

{
  "code": 50002,
  "message": "close is only allowed in PENDING_REVIEW(5)/PENDING_DISPATCH(15)/PENDING_EXECUTION(9); current=6"
}

4. 技能接口

技能没有物理删除;停用后不能用于新建任务,历史任务不受影响。技能以 task_id 寻址,不能把名称当业务键。

4.1 创建技能

POST /open/collect/v1/skills

参数位置类型必填说明
namebodystring否proto 中存在,但当前 service 忽略
aliasbodystring是租户内唯一;上游按相同 alias 幂等
summarybodystring是技能摘要
instructionbodystring是技能指令正文
curl -skS -X POST "$BASE_URL/open/collect/v1/skills" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"qa_pick","alias":"qa-pick-20260914","summary":"QA 拾取技能","instruction":"识别目标并完成拾取"}'
{"code":0,"data":{"task_id":"","name":"","alias":"qa-pick-20260916","summary":"QA 拾取技能","instruction":"识别目标并完成拾取","status":"ACTIVE","created_at":"","updated_at":""}}

创建、更新、停用和启用响应都返回完整技能字段。技能接口不要求 Idempotency-Key。

dev 环境示例(2026-09-16 修复后复验)

RESP=$(curl -sS -X POST "$BASE_URL/open/collect/v1/skills" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg alias "qa-demo-skill-$(date +%Y%m%d%H%M)" \
        '{alias:$alias,summary:"qa-demo 拾取技能",instruction:"识别桌面上的目标物体并完成拾取"}')")
echo "$RESP" | jq .
TASK_ID=$(echo "$RESP" | jq -r '.data.task_id')
{
  "code": 0,
  "data": {
    "task_id": "tsk-ddkyv6nwjggtpimv",
    "name": "tsk-ddkyv6nwjggtpimv",
    "alias": "qa-demo-skill-202609160020",
    "summary": "qa-demo 拾取技能",
    "instruction": "识别桌面上的目标物体并完成拾取",
    "status": "ACTIVE",
    "created_at": "1789489232",
    "updated_at": "1789489232"
  }
}

创建、更新、启用、停用和详情接口都返回完整技能字段;上游状态 0 / 90 分别映射为 ACTIVE / INACTIVE。

4.2 更新技能

POST /open/collect/v1/skills/{task_id}

参数位置类型必填说明
task_idpathstring是技能 ID
namebodystring否当前 service 忽略
aliasbodystring否新别名
summarybodystring否新摘要
instructionbodystring否新指令
curl -skS -X POST "$BASE_URL/open/collect/v1/skills/" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"alias":"qa-pick-20260914-v2","summary":"QA 拾取技能 v2","instruction":"先识别,再拾取并回报结果"}'
{"code":0,"data":{"task_id":"","name":"","alias":"qa-pick-20260914-v2","summary":"","instruction":"","status":"ACTIVE","created_at":"0","updated_at":"0"}}

更新响应与创建响应一样是稀疏结果;应再调用详情接口核对可读字段。

dev 环境示例(2026-09-16 修复后复验)

curl -sS -X POST "$BASE_URL/open/collect/v1/skills/$TASK_ID" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg alias "qa-demo-skill-$(date +%Y%m%d%H%M)-v2" \
        '{alias:$alias,summary:"qa-demo 拾取技能 v2",instruction:"先识别目标,再拾取并回报结果"}')" | jq .
{
  "code": 0,
  "data": {
    "task_id": "tsk-ddkyv6nwjggtpimv",
    "name": "tsk-ddkyv6nwjggtpimv",
    "alias": "qa-demo-skill-202609160020-v2",
    "summary": "qa-demo 拾取技能 v2",
    "instruction": "先识别目标,再拾取并回报结果",
    "status": "ACTIVE",
    "created_at": "1789489232",
    "updated_at": "1789489232"
  }
}

4.3 停用技能

POST /open/collect/v1/skills/{task_id}/disable

参数位置类型必填说明
task_idpathstring是技能 ID
curl -skS -X POST "$BASE_URL/open/collect/v1/skills//disable" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' -d '{}'
{"code":0,"data":{"task_id":"","name":"","alias":"qa-pick-20260914-v2","summary":"","instruction":"","status":"INACTIVE","created_at":"0","updated_at":"0"}}

dev 环境示例(2026-09-16 修复后复验)

curl -sS -X POST "$BASE_URL/open/collect/v1/skills/$TASK_ID/disable" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' -d '{}' | jq .
{
  "code": 0,
  "data": {
    "task_id": "tsk-ddkyv6nwjggtpimv",
    "name": "tsk-ddkyv6nwjggtpimv",
    "alias": "qa-demo-skill-202609160020-v2",
    "summary": "qa-demo 拾取技能 v2",
    "instruction": "先识别目标,再拾取并回报结果",
    "status": "INACTIVE",
    "created_at": "1789489232",
    "updated_at": "1789489232"
  }
}

技能仍被未关闭的任务引用时停用,返回 HTTP 409:

{
  "code": 50002,
  "message": "upstream '/core/api/task/v1/disable' rejected the request: task is being used by 3 job(s), cannot disable",
  "data": {
    "upstream": {
      "message": "failed to disable task: error: code = 36013 reason = TASK_IN_USE message = task is being used by 3 job(s), cannot disable metadata = map[] cause = ",
      "reason": "TASK_UPDATE_FAILED"
    }
  }
}

TASK_IN_USE 被翻译为 409 / 50002;重复停用同样返回状态冲突,不再报 502。

4.4 启用技能

POST /open/collect/v1/skills/{task_id}/enable

参数只有 path 中的 task_id。

curl -skS -X POST "$BASE_URL/open/collect/v1/skills//enable" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' -d '{}'
{"code":0,"data":{"task_id":"","name":"","alias":"qa-pick-20260914-v2","summary":"","instruction":"","status":"ACTIVE","created_at":"0","updated_at":"0"}}

dev 环境示例(2026-09-16 修复后复验)

curl -sS -X POST "$BASE_URL/open/collect/v1/skills/$TASK_ID/enable" \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H 'Content-Type: application/json' -d '{}' | jq .
{
  "code": 0,
  "data": {
    "task_id": "tsk-ddkyv6nwjggtpimv",
    "name": "tsk-ddkyv6nwjggtpimv",
    "alias": "qa-demo-skill-202609160020-v2",
    "summary": "qa-demo 拾取技能 v2",
    "instruction": "先识别目标,再拾取并回报结果",
    "status": "ACTIVE",
    "created_at": "1789489232",
    "updated_at": "1789489232"
  }
}

4.5 查询技能详情

GET /open/collect/v1/skills/{task_id}

参数位置类型必填说明
task_idpathstring是技能 ID
curl -skS "$BASE_URL/open/collect/v1/skills/" \
  -H "Authorization: Bearer $OPEN_TOKEN"
响应字段类型当前说明
task_idstring技能唯一 ID
namestring上游技能名;当前创建路径默认等于 task_id
aliasstring技能别名
summarystring技能摘要
instructionstring技能指令
statusstringACTIVE 或 INACTIVE
created_atstringUnix 秒级时间戳字符串
updated_atstringUnix 秒级字符串
{"code":0,"data":{"task_id":"","name":"","alias":"qa-pick-20260916-v2","summary":"QA 拾取技能 v2","instruction":"先识别,再拾取并回报结果","status":"ACTIVE","created_at":"1789343900","updated_at":"1789344000"}}

dev 环境示例(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/skills/$TASK_ID" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq .
{
  "code": 0,
  "data": {
    "task_id": "tsk-ddkyv6nwjggtpimv",
    "name": "tsk-ddkyv6nwjggtpimv",
    "alias": "qa-demo-skill-202609160020-v2",
    "summary": "qa-demo 拾取技能 v2",
    "instruction": "先识别目标,再拾取并回报结果",
    "status": "ACTIVE",
    "created_at": "1789489232",
    "updated_at": "1789489232"
  }
}

4.6 查询技能列表

GET /open/collect/v1/skills

参数位置类型必填当前说明
keywordquerystring否proto 已声明,当前 service 忽略
active_onlyquerybool否proto 已声明,当前 service 忽略
pagequeryint32否页码
page_sizequeryint32否上限 100
curl -skS "$BASE_URL/open/collect/v1/skills?keyword=qa&active_only=true&page=1&page_size=20" \
  -H "Authorization: Bearer $OPEN_TOKEN"
{"code":0,"data":{"items":[{"task_id":"","name":"","alias":"qa-pick-20260914-v2","summary":"QA 拾取技能 v2","instruction":"先识别,再拾取并回报结果","status":"ACTIVE","created_at":"0","updated_at":"1789344000"}],"total":1}}

测试过滤参数时,应预期目前不会生效并记录缺陷,不要把返回全量误判成测试数据污染。

dev 环境示例(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/skills?page=1&page_size=5" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq .

响应中 items 实际有 5 条(total 为 18),这里保留前 2 条:

{
  "code": 0,
  "data": {
    "items": [
      {
        "task_id": "tsk-ddkyv6nwjggtpimv",
        "name": "tsk-ddkyv6nwjggtpimv",
        "alias": "qa-demo-skill-202609160020-v2",
        "summary": "qa-demo 拾取技能 v2",
        "instruction": "先识别目标,再拾取并回报结果",
        "status": "ACTIVE",
        "created_at": "1789489232",
        "updated_at": "1789489232"
      },
      {
        "task_id": "tsk-ddkybulkf4tiiv4r",
        "name": "tsk-ddkybulkf4tiiv4r",
        "alias": "qa-demo-skill-202609152114-v2",
        "summary": "qa-demo 拾取技能 v2",
        "instruction": "先识别目标,再拾取并回报结果",
        "status": "ACTIVE",
        "created_at": "1789478063",
        "updated_at": "1789478064"
      }
    ],
    "total": 18
  }
}

5. 字典接口

5.1 查询采集员

GET /open/collect/v1/operators

参数位置类型必填当前说明
keywordquerystring否proto 已声明,当前 service 忽略
pagequeryint32否页码
page_sizequeryint32否上限 100
curl -skS "$BASE_URL/open/collect/v1/operators?page=1&page_size=20" \
  -H "Authorization: Bearer $OPEN_TOKEN"
响应字段类型说明
items[].operator_idstring分发时填入 collector_ids
items[].usernamestring展示名
items[].is_onlinebool当前心跳在线状态
items[].connected_controller_idstring当前连接上位机,可能为空
items[].connected_device_idstring当前连接设备,可能为空
items[].last_connected_atstringUnix 秒级字符串,可能为 0
totalint32总数
{"code":0,"data":{"items":[{"operator_id":"","username":"collector_qa","is_online":false,"connected_controller_id":"","connected_device_id":"","last_connected_at":"0"}],"total":1}}

连接字段是 Redis 心跳瞬时态,不能用来反查任务分配关系。

dev 环境示例(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/operators?page=1&page_size=20" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq .
# 分发时用 nctest0@nanocloud 作为采集员
COLLECTOR_ID=$(curl -sS "$BASE_URL/open/collect/v1/operators?page=1&page_size=20" \
  -H "Authorization: Bearer $OPEN_TOKEN" | jq -r '.data.items[] | select(.username=="nctest0@nanocloud") | .operator_id')
{
  "code": 0,
  "data": {
    "items": [
      {
        "operator_id": "opr-ddchzvxi645pdzam",
        "username": "ncadmin@nanocloud",
        "is_online": false,
        "connected_controller_id": "",
        "connected_device_id": "",
        "last_connected_at": "0"
      },
      {
        "operator_id": "opr-ddchzvxizsyejiy7",
        "username": "nctest0@nanocloud",
        "is_online": false,
        "connected_controller_id": "",
        "connected_device_id": "",
        "last_connected_at": "0"
      },
      {
        "operator_id": "opr-ddchzvxitdzrdfrs",
        "username": "nctest1@nanocloud",
        "is_online": false,
        "connected_controller_id": "",
        "connected_device_id": "",
        "last_connected_at": "0"
      },
      {
        "operator_id": "opr-ddchzvxil67lfrzy",
        "username": "ncqc@nanocloud",
        "is_online": false,
        "connected_controller_id": "",
        "connected_device_id": "",
        "last_connected_at": "0"
      }
    ],
    "total": 4
  }
}

page_size=101 返回 HTTP 400:`{"code":10006,"message":"page_size must be ","tag_code":"normal_collection","tag_name":"常规采集","status":"ACTIVE"}],"total":1}}


建单必须从两次查询中各选择一个启用标签;空维度为 `10001`,非法维度为 `10002`。

**dev 环境示例**(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/label-tags?dimension=collection&page=1&page_size=100"
-H "Authorization: Bearer $OPEN_TOKEN" | jq . curl -sS "$BASE_URL/open/collect/v1/label-tags?dimension=project&page=1&page_size=100"
-H "Authorization: Bearer $OPEN_TOKEN" | jq .


`dimension=collection`:

{ "code": 0, "data": { "items": [ {"tag_id": "tag-ddewsegdbtjz5uyc", "tag_code": "normal_collection", "tag_name": "正式任务", "status": "ACTIVE"}, {"tag_id": "tag-ddewsege5rjy7adm", "tag_code": "test_task", "tag_name": "测试任务", "status": "ACTIVE"} ], "total": 2 } }


`dimension=project`:

{ "code": 0, "data": { "items": [ { "tag_id": "tag-ddewsegd2ms6myxs", "tag_code": "pretrain_large_scale", "tag_name": "预训练大规模采集", "status": "ACTIVE" } ], "total": 1 } }


建单时两维各取一个 `tag_id`:

COLLECTION_TAG_ID=$(curl -sS "$BASE_URL/open/collect/v1/label-tags?dimension=collection"
-H "Authorization: Bearer $OPEN_TOKEN" | jq -r '.data.items[] | select(.tag_code=="normal_collection") | .tag_id') PROJECT_TAG_ID=$(curl -sS "$BASE_URL/open/collect/v1/label-tags?dimension=project"
-H "Authorization: Bearer $OPEN_TOKEN" | jq -r '.data.items[0].tag_id')


不传 `dimension` 返回 HTTP 400:`{"code":10001,"message":"dimension is required: collection or project"}`。

dev 租户项目维只开通了 `pretrain_large_scale`。nano-open-api 修复前本接口只返回分类节点`collection_category`、`project_category`,拿它们建单会被拒,见 3.2 反例。

### 5.3 查询上传位置

`GET /open/collect/v1/upload-envs`

无 path、query 或 body 参数。

curl -skS "$BASE_URL/open/collect/v1/upload-envs"
-H "Authorization: Bearer $OPEN_TOKEN"


| 响应字段 | 类型 | 说明 |
|-|-|-|
| `options[].value` | string | 上传位置值,私有化环境通常为 `s3` |
| `options[].enabled` | bool | 上游是否启用 |
| `options[].is_default` | bool | 当前 service 固定返回 `false` |

{"code":0,"data":{"options":[{"value":"s3","enabled":true,"is_default":false}]}}


本接口只用于排障核对。分发接口不接收 `upload_env`,实际默认值由部署配置决定。

**dev 环境示例**(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/collect/v1/upload-envs"
-H "Authorization: Bearer $OPEN_TOKEN" | jq .

{ "code": 0, "data": { "options": [ {"value": "s3", "enabled": true, "is_default": false} ] } }


## 6. 设备接口

### 6.1 查询设备

`GET /open/device/v1/devices`

| 参数 | 位置 | 类型 | 必填 | 当前说明 |
|-|-|-|-|-|
| `keyword` | query | string | 否 | proto 已声明,当前 service 忽略 |
| `device_model` | query | string | 否 | proto 已声明,当前 service 忽略 |
| `page` | query | int32 | 否 | 页码 |
| `page_size` | query | int32 | 否 | 上限 100 |

curl -skS "$BASE_URL/open/device/v1/devices?device_model=RD001&page=1&page_size=20"
-H "Authorization: Bearer $OPEN_TOKEN"


| 响应字段 | 类型 | 说明 |
|-|-|-|
| `items[].device_id` | string | 分发时写入 `device_ids` |
| `items[].device_sn` | string | 设备序列号 |
| `items[].device_model` | string | 建单时可作为 `device_type` |
| `items[].status` | string | `ACTIVE`、`DISABLED`、`MAINTENANCE` 或 `UNKNOWN` |
| `total` | int32 | 总数 |

{"code":0,"data":{"items":[{"device_id":"","device_sn":"RD001-QA-001","device_model":"RD001","status":"ACTIVE"}],"total":1}}


当前过滤参数不生效。自由采集仅允许  
`RD001/RD001_EGO/RD001_VIO/RM003_VIO/RM003_VR/RM003_VR_WIRELESS/RP001_VR`;  
定制和恢复应使用这 7 个型号之外的支持型号。该双向排除当前主要由上游兜底。

**dev 环境示例**(2026-09-16 修复后复验)

curl -sS "$BASE_URL/open/device/v1/devices?page=1&page_size=20"
-H "Authorization: Bearer $OPEN_TOKEN" | jq . DEVICE_ID=$(curl -sS "$BASE_URL/open/device/v1/devices?page=1&page_size=20"
-H "Authorization: Bearer $OPEN_TOKEN" | jq -r '[.data.items[] | select(.device_model=="EX001")][0].device_id')


响应中 `items` 共 10 条,这里保留 `EX001`、`RD001`、`DESKTOP` 各一条:

{ "code": 0, "data": { "items": [ { "device_id": "dev-ddcrr72ambcqhwr2", "device_sn": "XREX21ZD2604200102", "device_model": "EX001", "status": "UNKNOWN" }, { "device_id": "dev-ddch2xuidf4ewxg4", "device_sn": "XRRD00ZD0000000000", "device_model": "RD001", "status": "UNKNOWN" }, { "device_id": "dev-ddeso3j62e5yenw2", "device_sn": "XRZM11ZD2606260001", "device_model": "DESKTOP", "status": "UNKNOWN" } ], "total": 10 } }


dev 设备大多是 `UNKNOWN` 状态,3.9 分发给 `UNKNOWN` 状态的 `EX001` 设备实测成功。

## 7. 当前未注册的目标接口

`GET /open/openapi.json` 当前未注册;接口自描述以后续独立版本为准。

curl -skS "$BASE_URL/open/openapi.json"
-H "Authorization: Bearer $OPEN_TOKEN"


当前预期为 HTTP 404 / `40006`。不要用它做存活探测,也不要把它计入 21 个已实现端点。

**dev 环境示例**(2026-09-16 修复后复验)

返回 HTTP 404:`{"code":40006,"message":"no such endpoint; check the method and path against the interface reference"}`。错误文案只提示核对接口参考,不再指向未注册的 OpenAPI JSON 路径。

## 8. 多样性与场景标签

本节是增量接口说明,不修改前面已有的接口参数和调用示例。

### 8.1 查询场景标签

GET /open/collect/v1/label-tags?dimension=scene&page=1&page_size=100 Authorization: Bearer


返回当前租户可用于自由采集任务 `scene_tag_ids` 的启用叶子标签。场景标签不能代替任务原有的采集维和项目维标签。

SCENE_TAG_ID=$(curl -skS "$BASE_URL/open/collect/v1/label-tags?dimension=scene&page=1&page_size=100"
-H "Authorization: Bearer $OPEN_TOKEN"
| jq -er '.data.items[0].tag_id')


### 8.2 创建多样性

POST /open/collect/v1/diversities Authorization: Bearer Content-Type: application/json


请求体:

{ "name": "qa-open-api-diversity", "description": "QA template", "content": "{"scene":"kitchen"}" }


- `name`:必填;同一租户内唯一。
- `description`:选填。
- `content`:必填;必须是合法 JSON 对象或数组的文本形式,不能传 JSON 标量。

创建后模板类型固定为 `default`,状态固定为 `ACTIVE`。同名重放返回已有模板,不覆盖已经保存的 `description` 或 `content`。

DIVERSITY_ID=$(curl -skS -X POST "$BASE_URL/open/collect/v1/diversities"
-H "Authorization: Bearer $OPEN_TOKEN"
-H "Content-Type: application/json"
-d '{"name":"qa-open-api-diversity","description":"QA template","content":"{"scene":"kitchen"}"}'
| jq -er '.data.diversity_id')


### 8.3 查询多样性列表

GET /open/collect/v1/diversities?keyword=&page=1&page_size=100 Authorization: Bearer


列表只返回当前租户可用的 `ACTIVE` 模板。响应 `data` 包含 `items`、`total`、`page`、`page_size` 和 `has_more`。

### 8.4 查询多样性详情

GET /open/collect/v1/diversities/{diversity_id} Authorization: Bearer


详情字段包括 `diversity_id`、`name`、`diversity_type`、`description`、`content`、`example_images`、`status`、`created_at` 和 `updated_at`。模板不存在或不属于当前租户时返回 HTTP 404 / `40007`。

curl -skS "$BASE_URL/open/collect/v1/diversities/$DIVERSITY_ID"
-H "Authorization: Bearer $OPEN_TOKEN" | jq .


### 8.5 权限与能力边界

- 创建:需要 `collect:diversity:update` 或 `collect:job_manager:create`。
- 列表:接受 `collect:diversity:read` 或 `collect:job_manager:read`。
- 详情:需要 `collect:diversity:read`。
- 修改、停用、删除和示例图片上传当前不通过开放接口提供,继续由管理后台处理。
- 定制或恢复任务可以填写 `diversity_id`;自由采集任务禁止填写该字段,应使用 `dimension=scene` 返回的标签。

### 8.6 多样性 `content` 格式规范

创建多样性的外层请求是 JSON 对象,但 `content` 字段本身是字符串。它必须保存“只序列化一次”的非空 JSON 对象文本:先构造内层对象,再对内层对象执行一次 `JSON.stringify` 或 `json.dumps`。

以下写法不合法,并返回 HTTP 400 / `10009`:

- 数组:`"content":"[]"`
- 标量:`"content":"123"`
- 二次序列化字符串:`"content":"\"{\\\"mode\\\":\\\"tidy_up_items\\\"}\""`
- 不是合法 JSON 的文本

创建接口只校验 `content` 是非空 JSON 对象,不逐项验证对象里的业务字段。字段组合能否生成数据,取决于对应的 `mode`。

#### 推荐格式:`tidy_up_items`

当前对外示例统一使用 `tidy_up_items`。其他 `mode` 的字段并不完全相同;在对应格式单独发布前,应使用平台生成的模板,不要直接照搬本节字段。

| 内层字段 | 类型 | 建议 | 说明 |
|-|-|-|-|
| `mode` | string | 必填 | 模式判别字段,本示例固定为 `tidy_up_items` |
| `task` | string | 必填 | 任务描述 |
| `instructions` | string[] | 必填 | 按执行顺序排列的动作文本 |
| `data_quantity` | integer | 必填 | 生成样本数,使用正整数;生成器缺省值为 `1000` |
| `addition` | object | 可选 | 附加条件;`add_id` 为 `all` 或步骤下标列表,`add_info` 为权重对象列表 |
| `change_frequency` | object | 可选 | 动态维度名到正整数 N 的映射,表示该维度每 N 条样本重新选择一次 |
| `` | object | 可选 | 选项到非负数权重的映射;至少一个权重大于 0 |
| `version` | string | 可选 | 模板格式版本;不传时生成结果当前使用 `0` |

权重按相对大小抽样,不强制总和等于 `1`;为便于评审和人工核对,建议归一化为 `1`。

内层对象的可读形式:

{ "mode": "tidy_up_items", "task": "整理杯子和盘子", "instructions": [ "右臂拿起杯子", "右臂将杯子放到托盘" ], "data_quantity": 10, "addition": { "add_id": "all", "add_info": [ {"无干扰": 0.8, "有干扰": 0.2} ] }, "change_frequency": { "杯子位置": 5 }, "杯子位置": { "左侧": 0.5, "右侧": 0.5 }, "version": "1" }


完整外层请求示例:

{ "name": "qa-open-api-diversity-apifox-v1", "description": "tidy_up_items 多样性格式示例", "content": "{"mode":"tidy_up_items","task":"整理杯子和盘子","instructions":["右臂拿起杯子","右臂将杯子放到托盘"],"data_quantity":10,"addition":{"add_id":"all","add_info":[{"无干扰":0.8,"有干扰":0.2}]},"change_frequency":{"杯子位置":5},"杯子位置":{"左侧":0.5,"右侧":0.5},"version":"1"}" }


同一租户内 `name` 唯一。重复提交相同名称会返回已有模板,不会用新请求覆盖原有 `description` 或 `content`;需要验证新版格式时,请使用新的固定版本后缀名称。