3
api
ila edited this page 2026-08-07 16:47:48 +08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

同步来源:docs/api.md · commit afc651f75a3a

API 合约

MVP 使用 /api/v1、UTF-8 JSON over HTTPS。文件上传使用 multipart/form-data。 Web 页面可以直接调用同一 usecase,但不能形成不同的业务规则。

通用约定

  • 所有 ID 为 UUID 字符串。
  • 时间为 UTC ISO 8601,例如 2026-07-25T08:30:00Z。
  • 金额使用十进制定点字符串,例如 "199.00",币种固定 CNY;不使用浮点数。
  • 写接口在合约标注处接收 Idempotency-Key 请求头。
  • App 的任务执行接口同时需要用户 Bearer Token、设备身份和 X-Claim-Token。
  • 分页使用 limit 和不透明 cursor;MVP limit 最大 100。
  • 客户端不得根据 HTTP 超时判断操作失败,必须查询资源最终状态。

管理 Web/API 使用 ADMIN 服务端会话;App 执行接口使用 BUYER + 设备 Bearer token。 两种身份不能互换。HTTP 明文只允许 loopback 开发监听,非 loopback 服务必须配置 certificate/private key 并直接启用 TLS。

通用错误:

{
  "error": {
    "code": "TASK_STATE_CONFLICT",
    "message": "任务当前状态不允许此操作",
    "retryable": false,
    "details": {}
  },
  "request_id": "f2cf02e9-57e0-4fca-817b-c85228acfc81"
}

HTTP 语义:

  • 400 请求格式或业务校验失败
  • 401 未建立有效身份
  • 403 身份有效但无权限、设备禁用或 claim 不属于当前设备
  • 404 资源不存在,外部响应不泄露其他人的资源存在性
  • 409 幂等冲突、状态冲突或设备已有活跃任务
  • 413 文件过大
  • 415 不支持的媒体类型
  • 422 结构可解析但字段不满足 schema
  • 429 频率限制
  • 503 依赖暂不可用

服务健康

GET /healthz

不需要业务身份,只检查进程和 SQLite 连接,不返回版本、路径、DSN、连接池统计或内部 错误。响应带 Cache-Control: no-store,默认不返回 CORS header。

数据库可用:

{"status":"ok"}

返回 200。数据库不可用时返回 503:

{"status":"unavailable"}

健康检查失败不能终止进程;未知路由和不允许的方法分别使用稳定 404/405 JSON。

认证

管理 Web 会话

POST /login 接受表单账号密码,成功后设置 HttpOnly、Secure、SameSite=Lax 会话 Cookie。loopback HTTP 开发时不设置 Secure,非 loopback 服务必须直接启用 TLS。管理会话固定30天绝对有效期,不滚动续期;既有会话不静默延期,部署后需重新登录 才取得新期限。POST /logout 撤销服务端会话并清除 Cookie。Web 会话不能调用设备执行 接口。

未登录页面请求以 303 跳转 /login?next=...;next 只允许 /tasks 及其本站 子路径。未授权管理 API 返回 401 ADMIN_SESSION_REQUIRED。Cookie 认证的管理 API 写请求除原 Content-Type/幂等要求外,还必须携带与 CSRF Cookie 匹配的 X-CSRF-Token。

POST /login 在通过表单和 CSRF 校验、执行 bcrypt 前,按服务端观察到的来源地址 限流。5 分钟内最多 10 次;成功登录清零。超限返回 429、Retry-After 和通用 中文提示,不暴露账号是否存在。

POST /api/v1/auth/token

采购 App 建立用户和设备联合会话。

{
  "username": "buyer01",
  "password": "local-input-only",
  "device_id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8",
  "device_token": "local-input-only",
  "app_version": "0.1.0",
  "android_version": "待设备填写"
}

成功:

{
  "access_token": "opaque-token-returned-once",
  "token_type": "Bearer",
  "expires_in": 2592000,
  "user": {
    "id": "d950db90-cf58-4f21-86fd-067e1a91a3a6",
    "username": "buyer01",
    "role": "BUYER"
  },
  "device": {
    "id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8",
    "enabled": true
  }
}

密码和设备 token 不得出现在响应、日志或 execution event 中。

T-204 固定使用 256 bit 随机 opaque access token,数据库只保存 SHA-256;T-266 将新签发 token 的绝对有效期调整为固定30天(2592000 秒),不滚动续期。既有 token 保留原 expires_at,部署后重新登录才取得30天期限。用户必须为有效 BUYER,设备 必须预授权且启用;未绑定设备在首次成功登录时原子绑定当前采购员,已绑定其他采购员 时拒绝。不提供设备自助登记、refresh 或 App logout;Android 安全存储接入属于 T-206。

App token 登录与管理登录使用独立限流 scope,同样为每个来源地址 5 分钟最多 10 次。 超限返回 429、Retry-After 和稳定错误码 AUTH_RATE_LIMITED,retryable=true。

资产

POST /api/v1/assets

管理会话上传任务参考图片;App 也可用设备会话上传执行截图。请求必须带 Idempotency-Key。

表单字段:

  • file:必填,MVP 允许可解码 JPEG/PNG/WebP;请求最多 20 MiB、最长边最多 10000 px、总像素最多 25 MP。
  • purpose:TASK_REFERENCE 或 EXECUTION_EVIDENCE。
  • task_id:证据图片必填;参考图创建时为空。

成功:

{
  "id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3",
  "media_type": "image/jpeg",
  "size_bytes": 183245,
  "sha256": "hex-value",
  "created_at": "2026-07-25T08:30:00Z"
}

T-203 成功返回 201。使用相同 Idempotency-Key 和相同图片内容重试时返回同一 资产;同 key 不同内容返回 409。

TASK_REFERENCE 在后端统一白底合成、缩放到最长边不超过 2048 px,并以质量 90 编码为匿名 JPEG。响应的 media_type、size_bytes 和 sha256 都描述规范化结果, 不描述原始上传文件。T-203 尚不接受 EXECUTION_EVIDENCE。

GET /api/v1/assets/{asset_id}/content

受鉴权的文件流。只能读取用户有权查看的任务资产;不返回服务端文件路径。

Go ERP 会话与货运信息

顺运宝仅由 Go API 进程直连;浏览器不复用 ERP 凭证,也不接收 Cookie、JWT、用户资料或 原始响应。货运同步只接收以下最小化规范结构:

{
  "schema_version": 1,
  "query": {"mode":"ORDER_NUMBER"},
  "orders": [{
    "external_stock_id": "99001122",
    "source_code": "masked-at-log-boundary",
    "platform_order_no": "optional",
    "shop_name": "来源店铺",
    "source_created_at": "2026-07-28T08:00:00+08:00",
    "order_status": "0",
    "purchase_status": "0",
    "is_canceled": false,
    "items": [{
      "external_item_id": "880011",
      "title": "商品标题",
      "product_spec": "灰色,2XL",
      "sku": "灰色,2XL",
      "quantity": 2,
      "product_thumb_ref": "190000000",
      "original_unit_price_minor": 12950,
      "original_currency": "TWD",
      "purchase_status": "0"
    }]
  }]
}

不得返回 receiver、receiverTel、receiverAddr、Cookie、JWT、ERP 用户资料或完整原始 对象。多商品必须全部保留;缺失详情返回协议错误,不允许部分成功。

商品 product_spec 和采购使用的 sku 均来自 ERP productSpec,不得静默回退 sku/variationSku。productPrice 严格转换为 original_unit_price_minor;缺失价格为 null,币种固定为 TWD。product_thumb_ref 来自商品 productThumb,存在时必须是 规范化正整数,不能使用货运单 ID、商品明细 ID 或完整 URL。

货运详情中的每个商品还返回:

{
  "image_status": "READY",
  "image_error_code": null,
  "image_url": "/api/v1/freight-items/{item_id}/image"
}

image_status 为 NONE/PENDING/READY/MISSING/FAILED。只有 READY 返回同源 image_url;其他状态返回 null,失败时 image_error_code 是不包含上游正文的稳定错误码。 元数据同步先提交,图片在剩余请求预算内 best-effort 缓存,因此图片失败不改变货运同步的 SUCCEEDED 终态。后续新同步会重试非 READY 图片,相同引用的 READY 图片不会重复下载。

GET /api/v1/freight-items/{item_id}/image

Admin 会话鉴权的本地货运商品图片流。仅当商品仍在当前货运版本中、当前 productThumb 与已缓存引用一致且状态为 READY 时返回 200 image/jpeg;不存在、未完成、引用已变化或 无权访问统一返回 404。响应包含 Content-Length、ETag、 X-Content-Type-Options: nosniff 和 Cache-Control: private, no-store,不返回 ERP URL、 查询参数或服务端文件路径。

后端只使用配置的 CMROUBAO_SHUNYUNBAO_URL origin 和固定 /api/p/file?id=<productThumb> 请求图片,复用 ERP Cookie jar 并禁止重定向。下载内容经过 既有大小、尺寸、像素和解码校验,统一白底规范化为最长边不超过 2048 px 的 JPEG。

Go ERP 会话(T-230、T-236)

/erp、/erp/captcha、/erp/login 和 /api/v1/erp-session* 不再暴露。创建货运同步 前,单一 API 进程在未认证时以同一 Cookie jar 获取验证码、调用受控 OCR、登录并校验用户。 验证码图片、OCR 文字、Cookie、JWT、账号、密码和原始响应都不返回浏览器/API,也不写 SQLite。 登录成功后从 data.user 严格读取 id/username,并请求 GET /am/user/get?id=<user.id>;返回身份必须一致。只有 HTTP 401/403 或 ERP code -2 被视为 会话失效,其他 status=false 返回 ERP_RESPONSE_INVALID。

ERP 登录明确返回 status=false、msg=图片验证码不正确 时,首次失败后最多额外重试 6 次, 单次认证最多登录 7 次;每次都重新获取验证码图片并重新调用 OCR。认证总预算为 20 秒,调用方 deadline 更短时以调用方为准。账号密码错误、其他登录拒绝、OCR 异常、网络错误和协议错误均不 重试;次数耗尽返回 ERP_LOGIN_REJECTED。

ERP 配置来源:

  • CMROUBAO_SHUNYUNBAO_URL:默认 https://www.shunyunbaoerp.com;只接受无路径、 userinfo、query 或 fragment 的 HTTPS origin。
  • CMROUBAO_SHUNYUNBAO_USERNAME 与 CMROUBAO_SHUNYUNBAO_PASSWORD:必须同时设置; 为空时连接页明确显示未配置,服务进程仍可用于其他本地功能。
  • cmd/api 只把标准工作目录 backend-api/.env 中的上述三个值作为回退。系统环境变量 优先;文件缺失不报错。该解析器不修改全局环境,不加载数据库、HTTP、TLS 或 authctl 密码,也不支持变量展开、命令执行或任意路径。
  • CMROUBAO_OCR_API_URL 为可选 OCR POST endpoint;只允许 HTTPS,或本机 loopback HTTP, 不允许 userinfo、query、fragment 或重定向。请求使用 multipart/form-data 的 file 字段; OCR 不可达、超时、非成功、过大或响应无有效文本时,货运创建返回 503 OCR_SERVICE_INVALID 且不会创建同步记录。预检的稳定错误还包括 422 ERP_NOT_CONFIGURED、 422 ERP_LOGIN_REJECTED、502 ERP_RESPONSE_INVALID 和 503 ERP_UNAVAILABLE;SSR 导入页 用相同 code 显示可关闭弹窗并保留表单。所有 message 都是固定匿名文本,不含 ERP/OCR 原始 响应、验证码、Cookie、账号或密码。
  • CMROUBAO_ERP_DEBUG_LOG 默认 false,仅接受 true 或 false。临时设为 true 后, API 启动窗口记录 ERP request 的 method/path,以及 response 的 status、content type、长度和 最多 4 KiB 的递归脱敏 JSON 摘要;不记录请求 body、完整 URL query、header、Cookie、验证码、 OCR 文本、账号、密码、token、订单标识、收件信息、非 JSON body 或图片。诊断完成后必须设回 false 并重启 API。Windows 可使用根目录 start-backend.bat --erp-debug 仅为本次 API 进程覆盖开启;也可与 --migrate 组合。该模式额外在 ERP 登录前输出单行 erp_ocr_result value="..." length=...,值只限本次有效 OCR 结果;OCR 失败或无效时只输出 class=failed 或 class=invalid,不输出原文。每次登录另输出 erp_login_attempt attempt=<n> max_attempts=7 result=<固定分类>,不包含凭证或 ERP 原始响应。

会话不写 SQLite 或 Redis;服务重启后在下一次货运导入前重新经 OCR 建立会话。

POST /api/v1/freight-syncs

ADMIN 创建货运同步记录,必须带 Idempotency-Key:

{"mode":"ORDER_NUMBER","order_number":"完整单号"}

T-224 增加:

{
  "mode":"CREATED_RANGE",
  "created_from":"2026-07-22",
  "created_to":"2026-07-28"
}

手动范围最多 7 个自然日;也可以只提交 {"mode":"CREATED_RANGE","sync_to_now":true}。后者在没有水位时从当天开始,有水位时 从成功水位前回看 10 分钟对应的自然日开始,后端再按最多 7 天切窗。

ORDER_NUMBER 在同一请求内执行认证、ERP 查询、规范化和事务落库,总预算 55 秒。首次成功 返回 201 和最终 SUCCEEDED run,成功幂等重放返回 200,两者均设置 Location: /api/v1/freight-orders。同时只能执行一个完整单号同步,其他请求快速返回 409 FREIGHT_SYNC_BUSY;超时返回 504 FREIGHT_SYNC_TIMEOUT,并使用独立 cleanup context 把已创建 run 保存为 FAILED。Admin Web 成功后跳转 /freight?notice=import-succeeded, 此时列表已能读取货运头和全部商品明细。

CREATED_RANGE 和 sync_to_now 保持异步,响应 202 和 PENDING run,随后在后台执行。 订单号不进入 URL、事件 message 或访问日志;数据库只保存规范值及用于审计/检索的受控字段, 不保存 ERP 凭证、Cookie 或 JWT。HTTP Server WriteTimeout 为 70 秒,ERP 单次请求 timeout 仍为 30 秒。

GET /api/v1/freight-syncs/{sync_id}

返回 PENDING/RUNNING/SUCCEEDED/FAILED、查询模式、匿名查询摘要、开始/结束时间、 订单/商品计数和稳定错误码。失败不返回 ERP 原始 body 或个人信息。

GET /api/v1/freight-sync-watermark

返回当前 ADMIN creator 的顺运宝最近成功日期同步水位;尚未成功同步时返回 {"watermark":null}。只有完整批次落库成功才在同一事务推进水位;失败、空响应以外 的协议错误和较旧范围成功均不会覆盖较新的水位。

GET /api/v1/freight-orders

返回当前 ADMIN creator 最近更新的货运单并支持 limit(1..100)。响应不包含 收件人字段或 ERP 原文;列表筛选和 cursor 后置,不属于本次日期同步闭环。

GET /api/v1/freight-orders/{id}

T-222 返回货运头、全部当前商品明细和 revision/hash 状态。T-223 再增加采购需求和 已生成 task 引用。不存在和跨 creator 统一 404;响应 Cache-Control: no-store。

T-227 的 Go source 使用当前内存会话,按 listTotal -> list 分页 -> listByStock 查询; 完整单号由请求同步调用,日期范围由后台 worker 调用。每次先校验会话;列表最多 100 条、 每页 20 条,详情每批最多 100 个外部 stock ID。 顺运宝详情 created 已确认可能只有分钟精度;无时区的 yyyy-MM-dd HH:mm[:ss] 或 yyyy-MM-ddTHH:mm[:ss] 均严格按 Asia/Shanghai 解释并转 UTC, RFC3339 保留其显式时区。其他未知时间格式继续返回 ERP_RESPONSE_INVALID。 未配置、未登录、找不到货运单、响应协议错误和暂时不可用分别落为 ERP_NOT_CONFIGURED、ERP_SESSION_REQUIRED、ERP_FREIGHT_NOT_FOUND、 ERP_RESPONSE_INVALID 和 ERP_UNAVAILABLE,不返回 ERP 原始错误 body。

POST /api/v1/freight-items/{item_id}/procurement-request

T-223 创建或重放该商品当前 revision 的采购需求。ERP 数字状态不自动解释,ADMIN 必须显式确认仍需采购:

{"confirm_procurement_needed":true}

请求快照保留 title、product spec、SKU、quantity、来源 hash/revision 和当时的取消/ 采购状态。缺标题、SKU、正整数数量或来源明确取消时创建为 BLOCKED,不调用 VLM 补字段;字段合格但没有受控参考图时为 NEEDS_IMAGE。

PUT /api/v1/procurement-requests/{id}/reference-asset

绑定当前 ADMIN 已上传的 TASK_REFERENCE asset。asset 必须尚未属于其他任务/请求, 内容仍走既有解码、像素、规范化和 hash 校验。请求体:

{"image_asset_id":"2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3"}

绑定成功后进入 READY。来源 revision/hash 或货运取消标记已变化时返回 409 PROCUREMENT_SOURCE_CHANGED。

POST /api/v1/procurement-requests/{id}/purchase-task

必须带 Idempotency-Key,请求体是空 JSON 对象 {}。只有 READY request 可生成 任务;task、purchase_task_sources 和 request 的 TASK_CREATED 状态在同一事务内 提交。同一 request revision 即使使用不同幂等 key 重试也返回同一 task。来源后续 更新只显示 source_changed=true,不修改 PENDING、CLAIMED、RUNNING 或终态任务。

管理 Web 在 /freight/{id} 内提供对应的人工确认、参考图上传和生成任务操作;写操作 分别使用 /freight/items/{id}/procurement-request、 /freight/procurement-requests/{id}/reference 和 /freight/procurement-requests/{id}/purchase-task,均受 ADMIN session 与 CSRF 保护。

采购任务

POST /api/v1/tasks

采购管理员或授权的外部管理系统创建任务。必须带 Idempotency-Key。

{
  "source_ref": "external-admin-task-10001",
  "title": "黑色双肩包",
  "sku": "BLACK-20L",
  "description": "容量约20L,外观接近参考图",
  "image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3",
  "quantity": 2,
  "max_budget": "200.00"
}

规则:

  • title、sku 和 image_asset_id 必填,description 可为空。
  • title 最多 120 个 Unicode 字符且不超过 2048 个 UTF-8 字节;sku 不超过 512 个 UTF-8 字节;description 不超过 8192 个 UTF-8 字节。
  • quantity 是正整数。
  • max_budget 可为空,否则为大于零、最多两位小数的 CNY 金额,表示当前任务全部 数量的最高商品总预算,不含尚无法确认的运费或优惠。
  • 同一调用方的 source_ref 如填写必须唯一。

成功返回 201:

{
  "id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
  "status": "PENDING",
  "title": "黑色双肩包",
  "sku": "BLACK-20L",
  "quantity": 2,
  "max_budget": "200.00",
  "created_at": "2026-07-25T08:30:00Z"
}

GET /api/v1/tasks

管理端列表。可选参数:q、status、created_from、created_to、limit、 cursor。q 匹配任务 ID、source_ref、标题或 SKU;默认 limit=20,最大 100。 排序固定为 created_at DESC, id DESC,不透明 cursor 同时编码这两个字段。

{
  "items": [
    {
      "id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
      "title": "黑色双肩包",
      "sku": "BLACK-20L",
      "status": "RUNNING",
      "quantity": 2,
      "device_name": "pdd-phone-01",
      "updated_at": "2026-07-25T08:35:00Z"
    }
  ],
  "next_cursor": null
}

GET /api/v1/tasks/{task_id}

管理会话返回完整任务、当前 execution、时间线、候选和资产元数据。App 只能读取 当前设备已领取的任务。响应必须同时包含:

  • original_requirement:不可变原始输入。
  • derived_requirement:模型输出,可能为空。
  • claim:管理端可见设备和到期时间;claim token 永不返回。
  • execution:step、outcome、错误、order_submitted。
  • events 和 assets:有权限的摘要。
  • retry:allowed、稳定 block_code 和按序保存的重试原因历史。

T-204 已实现的 TASK_CREATED、TASK_CANCELED 事件包含可空 actor_user_id;新管理操作写入真实 ADMIN 用户 ID,T-203 历史事件返回 null。

POST /api/v1/tasks/{task_id}/cancel

管理端取消任务。PENDING 可立即取消;执行中只设置取消请求,App 在安全检查点确认 后进入 CANCELED。终态返回 409。

{
  "reason": "需求已撤销"
}

PENDING/CLAIMED 立即进入 CANCELED。RUNNING/WAITING_CONFIRMATION 只记录 取消请求并保持原状态;重复请求不重复写事件。App 的任务 heartbeat 会返回 cancel_requested=true,只有 App 在安全检查点调用 cancel-ack 后才进入 CANCELED 并结束 execution。

POST /api/v1/tasks/{task_id}/retry

管理端将下单前失败或取消的原采购任务重新排入队列。必须带 Idempotency-Key:

{
  "reason_code": "AUTOMATION_FAILED",
  "reason_note": "图片搜索阶段中断"
}

reason_code 允许 AUTOMATION_FAILED、DEVICE_INTERRUPTED、 ADMIN_CANCELED、OTHER;OTHER 必须提供不超过 500 UTF-8 字节的 reason_note。

仅 FAILED/CANCELED 且不存在任何 order_submissions、历史 execution outcome 也没有 order_submitted=true 时允许。成功复用同一 task ID,将任务改为 PENDING 并增加 version;参考图、ERP 来源、候选、证据和旧 execution 不变,Roubao 再次领取后创建递增的新 execution attempt。响应:

{
  "task": {
    "id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
    "status": "PENDING"
  },
  "replayed": false
}

活跃或成功任务返回 409 TASK_RETRY_NOT_ALLOWED。只要任务存在提交围栏、待付款订单、 人工复核或其他下单信号,返回 409 TASK_RETRY_ORDER_UNCERTAIN,不提供绕过参数。 相同 key/body 返回同一任务且 replayed=true;同 key 不同 body 返回幂等冲突。

POST /api/v1/tasks/{task_id}/order-authorizations

T-215 起由有效 ADMIN 会话对当前 WAITING_CONFIRMATION execution 创建一次性待投递 授权。Cookie API 请求必须带 X-CSRF-Token 和 Idempotency-Key;请求体见 T-215。关键规则:

  • 顶层和每个 item 只引用 v7 candidate_key,不得用 ordinal 或 URL 选品。
  • items 恰好覆盖当前 execution 全部 observation,只有顶层 key 对应项为 ACCEPT;所有项必须包含 T-208 schema v1 合法理由。
  • execution_id、task content hash 和 expected task version 必须同时匹配;服务端 从任务/observation 读取 SKU、数量、候选规格、价格和身份指纹写入授权快照。
  • 首次成功返回 201 和 PENDING_DELIVERY 授权;同 key 同 body 重放返回相同 授权并带 replayed=true,同 key 不同 body 返回 409。
  • 未投递前改选必须引用当前 supersedes_authorization_id;旧授权变为 SUPERSEDED。已投递或不是最新授权时返回 409。

Admin GET /api/v1/tasks/{task_id} 同时返回当前及历史 order_authorizations。授权仅允许 T-216 设备命令领取,不代表浏览器可以直接操作 手机,也不授权付款。

POST /api/v1/tasks/{task_id}/commands/next

T-216 起由原 BUYER/设备主动拉取当前 execution 的下单命令。请求带 bearer、 X-Claim-Token,body 提交 device_id、execution_id 和 claim_generation。 服务端校验有效 claim/租约、WAITING_CONFIRMATION、未取消和同一 active authorization。没有命令返回 204;首次投递把授权置为 DELIVERED,之后对 DELIVERED/ACKNOWLEDGED 返回同一 command。历史授权保持 schema v2;T-278 后新授权 使用 schema v3。

命令只含 task/execution/content hash、原始 SKU/数量、candidate key、观测标题/ 规格/价格、T-214 四个指纹、platform_product_id 和严格规范化的 canonical_product_url。schema v3 另含冻结的 authorized_total_price_cap_cents 和 price_cap_source=TASK_MAX_BUDGET|DEFAULT_LOW_PRICE_CAP;上限来自任务最高总预算, 任务未填价格时来自后台默认低价上限。observed_ordinal 仅是审计提示,不能替代商品 ID。 command_sha256 覆盖上述全部字段;投递重试不得变化。新授权只允许具备完整 goods_id/URL 身份的候选,历史无商品身份的授权不能生成自动下单命令。

POST /api/v1/tasks/{task_id}/commands/{command_id}/ack

App 严格校验并写入 Keystore-backed 加密状态后,使用 bearer、claim token、 Idempotency-Key 提交 execution、generation 和 command_sha256。成功把 DELIVERED -> ACKNOWLEDGED;相同 key/body 和同 command/hash 可安全重试,其他 command/hash/设备或无效租约返回冲突。ACK 只表示命令已可靠保存,不表示开始操作、 创建订单或付款。

POST /api/v1/tasks/{task_id}/order-dry-runs/start

T-217 App 在加密保存 dry-run 意图后调用。请求带 BUYER bearer、claim token、 Idempotency-Key,body 固定 device、execution、generation、command id/hash。 服务端要求有效租约、任务仍为 WAITING_CONFIRMATION、未取消且授权为 ACKNOWLEDGED;事务内创建 PREPARING dry-run、把授权置为 EXECUTING 并写开始 事件。相同请求重放返回同一记录。

POST /api/v1/tasks/{task_id}/order-dry-runs/{command_id}/ready

App 到达确认订单页并先加密保存 READY 后,提交当前 card/detail 指纹、规范化标题、 已选 SKU、数量、单价、商品总额和受控 evidence asset。服务端重新核对 command、 task/execution/claim、数量、授权冻结总额上限和 evidence 归属后,把同一 dry-run 置为 READY 并写 事件;相同请求可重放。该响应不表示订单已提交,且不授权付款。

POST /api/v1/tasks/{task_id}/order-dry-runs/{command_id}/fail

T-279 起,App 在 direct dry-run 无法严格确认商品身份、目标 SKU、当前价格或冻结预算时, 使用 BUYER bearer、claim token 和固定 Idempotency-Key 回传失败。body 包含 device、 execution、generation、command_sha256、failure_code 和受限失败说明;代码只接受 IDENTITY_MISMATCH、SKU_UNRESOLVED、PRICE_UNVERIFIED、BUDGET_EXCEEDED、 TRANSIENT_AUTOMATION。

服务端只允许同一有效 claim 下处于 EXECUTING 的授权和 PREPARING dry-run 失败, 事务内把授权置为 FAILED 并保存代码、说明和时间。相同 code/message 可安全重放, 不同失败内容返回幂等冲突。任务保留 WAITING_CONFIRMATION,Admin 可重新选择候选并创建 新授权;失败不会获得提交订单或支付资格。

POST /api/v1/tasks/{task_id}/order-submissions/start

T-218 App 在最终提交前调用。请求带 BUYER bearer、claim token、 Idempotency-Key,body 固定 device、execution、generation、authorization、 command id/hash、dry-run id/hash 和 App 重新核验的 SKU/数量/单价/总额。服务端要求 有效租约、未取消、authorization=EXECUTING、dry-run=READY 且所有快照完全一致。

成功事务内创建一个 order_submission、写 ORDER_SUBMISSION_FENCED 并使原授权 不可再次创建 submission。响应返回稳定 submission ID 和 fenced_at。同 key/body 重放返回相同记录;同 key 不同 body、第二个 submission 或非 READY 返回冲突。围栏 成功只表示 App 获得过一次点击机会;从此即使响应丢失或 App 退出也只能对账,不能 再次提交。

POST /api/v1/tasks/{task_id}/order-submissions/{submission_id}/reconcile

App 唯一识别待付款订单并先上传受控 evidence 后,提交订单编号、平台显示下单时间、 标题、SKU、数量、金额、状态和 evidence asset/hash。服务端核对原 device/task/ execution/generation、submission、期望快照、证据归属和幂等键;成功将 submission 置为 RECONCILED、authorization 置为 CONSUMED 并记录 order_submitted=true。订单编号不得进入 URL、日志、事件 message 或模型字段。 围栏创建后允许原 user/device/generation/claim token 在租约到期后完成该 submission 的对账,但任务必须仍未取消且 execution 未结束;该例外不适用于 start,也不产生 新的提交资格。

POST /api/v1/tasks/{task_id}/order-submissions/{submission_id}/manual-review

围栏后无法唯一回读订单时,App 用相同身份提交受限 reason code 和可选 evidence。 服务端将 submission 置为 MANUAL_REVIEW 并写审计事件,但不释放围栏、不允许生成 第二个 submission。后续只允许人员或同一设备重新对账,不提供“重试提交”接口。

三个接口都不接收支付方式、支付密码或付款结果,也不授权客户端点击付款控件。

Admin GET /api/v1/tasks/{task_id} 在 T-219 增加 order_submissions 数组。数组按 fenced_at、ID 升序;包含 submission/authorization/dry-run/execution 身份、状态、 预期标题/SKU/数量/单价/总额和围栏时间。RECONCILED 项另外返回订单号、平台下单 时间、PENDING_PAYMENT、对账 evidence asset/hash 和对账时间; MANUAL_REVIEW 返回受限 reason code。响应必须 Cache-Control: no-store,订单号 不进入任何资源 URL。无记录时返回 [],不能省略字段或返回 null。

设备与领取

POST /api/v1/devices/heartbeat

App 空闲或运行时上报设备状态;运行时任务续租使用任务专用 heartbeat。

{
  "app_version": "0.1.0",
  "android_version": "16",
  "pdd_version": "device-observed-value",
  "readiness": {
    "accessibility_enabled": true,
    "pdd_installed": true,
    "active_task_id": null
  }
}

device_id 可以省略;若提供,必须与 Bearer token 中的设备一致。服务端时间是唯一 租约时钟。响应返回服务端认定的 active_task_id、client_state_matches、 readiness 上报时间和 server_time。heartbeat 超过配置 TTL 或任一就绪位为 false 时,claim 返回 409 DEVICE_NOT_READY。

POST /api/v1/tasks/claim-next

由用户点击触发,在单个 SQLite immediate transaction 中领取下一条任务。App 必须 在请求前生成并安全保存相互独立的 256 bit Raw URL X-Claim-Token 和 Idempotency-Key;网络结果不确定时复用同一组值。服务端只保存 token 的 SHA-256,响应和日志都不回显原 token 或 hash。

{}

有任务时:

{
  "task": {
    "id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
    "status": "CLAIMED",
    "title": "黑色双肩包",
    "sku": "BLACK-20L",
    "description": "容量约20L,外观接近参考图",
    "image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3",
    "reference_image_sha256": "64-char-lowercase-hex",
    "task_content_sha256": "64-char-lowercase-hex",
    "reference_image_url": "/api/v1/tasks/37c9c715-9b51-4ed5-984e-66dad2710c71/reference-image?claim_generation=1",
    "quantity": 2,
    "max_budget": "200.00",
    "currency": "CNY",
    "version": 2,
    "claim_generation": 1,
    "claim_issued_at": "2026-07-25T08:30:00Z",
    "claim_expires_at": "2026-07-25T08:40:00Z"
  },
  "replayed": false,
  "server_time": "2026-07-25T08:30:00Z"
}

task_content_sha256 是服务端对不可变任务内容生成的版本化摘要,客户端视为 opaque 并在候选和终态结果中原样回显;服务端拒绝与当前 execution 快照不一致的摘要。 App 下载参考图后必须独立校验 reference_image_sha256。

当前设备自己有已过期 CLAIMED 时优先回收该任务,避免设备唯一归属冲突;否则按 created_at ASC, id ASC 选择 PENDING 或已过期 CLAIMED。没有任务返回 204; 同 key 的无任务重放始终保持 204。同 key、同 token 的活跃 claim 重放返回同一 任务,即使状态已进入 RUNNING/WAITING_CONFIRMATION;请求不同返回 409 IDEMPOTENCY_CONFLICT;原 claim 已释放、取消或被其他领取回收时返回 409 CLAIM_REPLAY_EXPIRED。同一设备不能再领取第二条活跃任务。

GET /api/v1/tasks/{task_id}/reference-image

claim 响应给出的受保护参考图地址。请求使用 BUYER Bearer token、 X-Claim-Token 和 URL 中的 claim_generation;只有当前用户、当前设备、匹配 generation/token 且租约未过期的活跃任务可以读取。成功返回匿名规范化 JPEG, 设置 private, no-store、nosniff、长度和 ETag,不返回存储路径。

POST /api/v1/tasks/{task_id}/start

把当前设备持有的 CLAIMED 任务改为 RUNNING 并创建 execution。请求带 X-Claim-Token 和 Idempotency-Key。

{
  "claim_generation": 1,
  "expected_version": 2
}

同 key、同请求重放返回同一 execution;错误用户/设备/token 返回 403,过期租约、 状态或版本冲突返回 409。成功响应包含更新后的 task、execution、replayed 和 server_time。默认 running lease 为 30 分钟(允许配置 5 至 120 分钟), execution.execution_expires_at 是 App 可以离线继续自动化的上限,不是后台自动 重分配时间。

POST /api/v1/tasks/{task_id}/heartbeat

运行时续租并返回是否请求取消:

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "step": "SCAN_RESULTS"
}
{
  "task": {
    "id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
    "status": "RUNNING",
    "version": 4,
    "claim_generation": 1,
    "claim_expires_at": "2026-07-25T09:03:30Z"
  },
  "execution": {
    "id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
    "current_step": "SCAN_RESULTS",
    "order_submitted": false,
    "execution_expires_at": "2026-07-25T09:03:30Z"
  },
  "cancel_requested": false,
  "server_time": "2026-07-25T08:33:30Z"
}

只接受 1 至 64 字节的大写 ASCII step。heartbeat 使用服务端 UTC 更新 execution、 设备最近在线时间、任务 version 和 execution_expires_at,不写高频任务事件。 App 每 30 秒 best-effort 调用;网络失败时可执行到上一次服务端截止时间。截止时间 到达后 App 持久化 SAFE_STOPPED 并停止外部动作,任务保持原非终态且绝不回到 领取队列。原设备重连后可以用同一 execution/claim heartbeat 同步状态;后端不延长 已经过期的授权,且此时只接受 step=SAFE_STOPPED。若响应包含取消请求,App 可以 继续调用 cancel-ack;没有取消时仍保持安全停止,不自动恢复采购。

POST /api/v1/tasks/{task_id}/release

只允许尚未开始的 CLAIMED 任务释放回 PENDING。运行中使用取消/失败流程。 请求带 X-Claim-Token、Idempotency-Key,body 与 start 相同。成功后清除当前 用户、设备、token hash 和租约,但保留递增过的 claim_generation 供审计。

POST /api/v1/tasks/{task_id}/cancel-ack

App 收到取消请求并在安全检查点停止后调用:

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "expected_version": 5
}

请求带 X-Claim-Token 和 Idempotency-Key。只有匹配的未结束 execution、设备 归属和已存在的管理取消请求可以确认;App 已在安全检查点停止时,即使离线授权刚 过期也允许原设备补交确认。成功把任务置为 CANCELED、结束 execution、清除 claim 秘密并追加带用户/设备 actor 的事件。同 key 重放不产生第二个事件。

App 本地 AI 边界

MVP 后端不实现 /api/v1/tasks/{task_id}/ai/*,不保存 VLM 配置或 Key,也不代理 第三方模型。App 使用本机配置的 OpenAI 兼容 adapter 完成需求提取和候选评估;后台 任务 payload 不能携带或覆盖 provider、Base URL、model、prompt 或 API Key。

App 支持 MANUAL_FIRST 和 AI_ASSISTED。execution 结果必须记录实际模式; AI_ASSISTED 还要记录 provider ID、model、prompt/schema version、reference/ candidate evidence SHA-256 和结构化模型判断。结果不得包含 Key、Authorization、 完整 endpoint、订单号、店铺名或供应商原始响应正文。

sku、quantity 和 max_budget 始终来自原任务,模型不能覆盖。App 按 ordinal 串行评估,每个 execution 最多一次需求提取、最多 5 次候选评估,并由本地确定性规则 产生建议。模型不能返回页面动作、建议 ordinal、人工确认状态或订单授权;低置信度、 无效 schema、证据不足或预算不确定时转人工。

执行事件与结果

App 使用加密 outbox 按“事件 -> evidence asset -> 候选 -> 人工 review -> 终态” 顺序提交。所有写接口 重新校验 BUYER/device/task/execution/claim 和幂等键。原设备可以在 execution_expires_at 后补报授权内已经产生的结果;后端记录 received_after_execution_expiry=true,但这不允许 App 在过期后继续自动化。

POST /api/v1/tasks/{task_id}/events

批量追加事件,必须带 Idempotency-Key:

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "events": [
    {
      "event_id": "client-generated-uuid",
      "step": "SEARCH",
      "type": "STEP_COMPLETED",
      "message": "已进入搜索结果页",
      "occurred_at": "2026-07-25T08:34:00Z"
    }
  ]
}

message 不能包含凭证或完整个人敏感信息;同一 event_id 重放不重复插入。

POST /api/v1/tasks/{task_id}/evidence

以原始 image/jpeg、image/png 或 image/webp body 上传一张受控截图;请求必须带 Authorization: Bearer、X-Claim-Token、Idempotency-Key、X-Execution-ID 和 X-Claim-Generation。服务端规范化存为 JPEG 并返回 asset ID、SHA-256、尺寸及 received_after_execution_expiry,不接受图片 URL,也不从第三方下载图片。

POST /api/v1/tasks/{task_id}/candidates

保存当前 execution 已累计的 0..5 个原始曝光候选快照,必须带 Idempotency-Key。同一 execution 可以提交多个未完成快照,但候选只能按 ordinal 和 goods_id 单调追加;完成快照不可被旧请求覆盖。正式参考图检索使用固定审计值 PDD_IMAGE_SEARCH,不把标题或 SKU 伪装成图片检索词:

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "task_content_sha256": "64-char-lowercase-hex",
  "execution_mode": "AI_ASSISTED",
  "search_query": "PDD_IMAGE_SEARCH",
  "collection_complete": false,
  "search_round": 1,
  "provenance": {
    "provider_id": "device-configured-provider",
    "model": "device-configured-model",
    "prompt_version": "candidate-evaluation-v2",
    "schema_version": 2
  },
  "candidates": [
    {
      "ordinal": 1,
      "title": "页面可见标题",
      "sku_text": "BLACK-L",
      "price": "189.00",
      "sku_verification_status": "MATCH",
      "price_verification_status": "VERIFIED",
      "value_source": "LOCAL_VLM",
      "match_type": "EXACT_MATCH",
      "search_round": 1,
      "result_rank": 2,
      "goods_id": "190887498",
      "product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=190887498",
      "image_url": "",
      "card_signature": "64-char-lowercase-hex",
      "detail_signature": "64-char-lowercase-hex",
      "detail_evidence_sha256": "detail-asset-64-char-lowercase-hex",
      "specification_evidence_sha256": "specification-asset-64-char-lowercase-hex",
      "evidence_asset_ids": [
        "7b733922-f90f-4bc4-a9ad-3e8ec4769122",
        "4095ea37-eb4f-47c7-989b-adf060e45a32"
      ],
      "evaluation": {
        "decision": "REVIEW",
        "score": 0.82,
        "matched": ["颜色和尺码均有可见证据"],
        "missing_or_uncertain": [],
        "rejection_reasons": [],
        "confidence": 0.78,
        "hard_constraints": [
          {
            "kind": "COLOR",
            "expected": "BLACK",
            "status": "MATCH",
            "evidence": "候选页面显示黑色"
          },
          {
            "kind": "SIZE",
            "expected": "L",
            "status": "MATCH",
            "evidence": "候选页面显示 L 码"
          }
        ]
      }
    }
  ],
  "recommendation": null
}

collection_complete=false 表示 Roubao 仍在搜索,后台保存候选并保持任务 RUNNING;此时 candidates 至少包含一个候选且不能提交 recommendation。 collection_complete=true 才在同一事务中生成决策数据集;非空完成快照还会记录 CANDIDATES_READY 并将任务切换到 WAITING_CONFIRMATION。完成快照不要求凑满固定 数量:对于带 match_type 的已解析候选,接收 1..5 个且批次中每项都必须标注; 零候选完成快照仍表示搜索完成但无匹配。EXACT_MATCH 最多 5 个且只能来自第一轮 SKU 硬匹配;FALLBACK 最多 2 个且只能来自第二轮图片搜索默认排序,必须保留 result_rank 且不能作为自动推荐。累计快照可同时包含第一轮严格候选和第二轮保底候选, 但总数仍不得超过 5;后台拒绝未全部标注、超过上述上限或轮次归属不一致的快照。 新请求的 search_round 为 1..2;升级前已经写入数据库的第三轮记录只作为历史审计读取。

新 App 必须为每个候选提交规格/价格审计状态:sku_verification_status 为 MATCH|MISMATCH|UNVERIFIED,price_verification_status 为 VERIFIED|UNVERIFIED,value_source 为 DETERMINISTIC|LOCAL_VLM。SKU 或价格为空时 对应状态必须为 UNVERIFIED;第二轮 FALLBACK 的两项状态必须都是 UNVERIFIED 且 来源固定为 DETERMINISTIC。后端为旧候选读取默认 UNVERIFIED/DETERMINISTIC,Admin 同时展示值、状态和来源。LOCAL_VLM 不代表金额由模型生成:金额仍须由 App 从模型引用的 当前节点原文确定性解析。

MANUAL_FIRST 时 provenance 和 evaluation 为空,但候选观察、搜索审计值和人工 结果仍可提交。AI_ASSISTED 为每个原始 observation 保存 evaluation;颜色/尺码状态 可以是 MATCH/MISMATCH/UNKNOWN,模型拒绝项也必须保留。schema v2 的 recommendation 只能指向颜色和尺码均为 MATCH、分数和置信度均不低于 0.75 且没有拒绝原因的原始 ordinal;没有满足项时只省略 recommendation,不能删除原始 候选。最终快照到达时,后端在同一事务内写入 search run、observation、model evaluation 和 recommendation,并保留最新累计 JSON 审计副本。每个非空候选必须按 DETAIL、SPECIFICATION 顺序绑定两个不同的 evidence asset ID,且两个声明哈希 必须分别等于后端保存的实际 asset 哈希;卡片和详情语义签名也必须是小写 SHA-256。 后端用版本化的 execution、原 ordinal、goods_id、详情签名和规格证据哈希生成稳定的 execution-scoped candidate_key。Admin 任务详情在 observation 的 identity 中 返回该 key、四个指纹及 identity 版本;旧 v6 observation 可没有 identity。 goods_id 只能是 6-20 位非零开头数字,product_url 必须逐字等于 https://mobile.yangkeduo.com/goods.html?goods_id=<goods_id>;两者只能同时为空或 同时有效。后端不请求该 URL,主要图片证据必须是已鉴权 asset。无商品身份的观察可 留作审计,但 Admin 不允许选择它创建自动下单授权。

POST /api/v1/tasks/{task_id}/human-reviews

采购员确认候选后、提交终态前调用。请求使用设备 Bearer token、X-Claim-Token 和 Idempotency-Key:

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "task_content_sha256": "64-char-lowercase-hex",
  "reason_schema_version": 1,
  "outcome": "CANDIDATE_ACCEPTED",
  "selected_candidate_ordinal": 2,
  "primary_reason_code": "SELECTED_BEST_MATCH",
  "note": "",
  "supersedes_review_id": null,
  "items": [
    {
      "candidate_ordinal": 1,
      "label": "REJECT",
      "primary_reason_code": "NOT_BEST_MATCH",
      "reason_codes": ["NOT_BEST_MATCH"],
      "note": ""
    },
    {
      "candidate_ordinal": 2,
      "label": "ACCEPT",
      "primary_reason_code": "SKU_MATCH",
      "reason_codes": ["SKU_MATCH"],
      "note": ""
    }
  ]
}

非空 review 必须恰好覆盖本次所有 observation;接受时只有所选 ordinal 为 ACCEPT,其余全部为 REJECT。零候选只允许 NO_MATCH 或 MANUAL_REQUIRED 且 items 为空。理由使用 T-208 版本 1 allowlist;任务没有预算时 禁止价格类理由,OTHER 必须带 4-200 字备注。同一幂等键重放返回原 review; 修订必须通过 supersedes_review_id 引用当前最新版本,服务端追加版本并保留历史。

POST /api/v1/tasks/{task_id}/complete

人员完成确认后调用,必须带 Idempotency-Key:

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "task_content_sha256": "64-char-lowercase-hex",
  "execution_mode": "AI_ASSISTED",
  "outcome": "CANDIDATE_ACCEPTED",
  "operator_reason": "款式和预算符合验证要求",
  "candidate": {
    "ordinal": 1,
    "title": "页面可见标题",
    "sku_text": "BLACK-L",
    "price": "189.00",
    "product_url": "",
    "image_url": "",
    "card_signature": "64-char-lowercase-hex",
    "detail_signature": "64-char-lowercase-hex",
    "detail_evidence_sha256": "detail-asset-64-char-lowercase-hex",
    "specification_evidence_sha256": "specification-asset-64-char-lowercase-hex",
    "evidence_asset_ids": [
      "7b733922-f90f-4bc4-a9ad-3e8ec4769122",
      "4095ea37-eb4f-47c7-989b-adf060e45a32"
    ],
    "evaluation": null
  },
  "order_submitted": false
}

outcome 允许:

  • CANDIDATE_ACCEPTED
  • CANDIDATE_REJECTED
  • NO_MATCH
  • MANUAL_REQUIRED

后端对 MVP 强制 order_submitted=false,成功后任务进入 SUCCEEDED;这里的成功表示 验证工作流正常结束,业务结果由 outcome 表达。

T-207 第一版要求 operator_reason 为人员输入的简短审计说明,但不把它当作可训练 标签。T-208 在第一版链路跑通后扩展为版本化 human_review 合约,至少包含 reason_schema_version、可空选择候选、逐候选 ACCEPT/REJECT、主要理由码、 附加理由码和受限备注。改选必须同时提交原推荐项拒绝理由与替代项选择理由;全部 无匹配必须覆盖每个曝光候选。具体 endpoint 和 schema 在领取 T-208 时冻结。

模型评估理由、确定性推荐理由和 human_review 分开保存。服务端不接受客户端把 模型理由标记为已由人员确认;第三方商品/图片 URL 只作为受限观测字段,不触发后端 下载,主要证据仍通过鉴权 asset API 上传。

POST /api/v1/tasks/{task_id}/fail

{
  "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
  "claim_generation": 1,
  "error": {
    "code": "PDD_RISK_CONTROL",
    "message": "检测到平台风险提示,已停止自动化",
    "step": "SCAN_RESULTS",
    "retryable": false
  },
  "evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"]
}

后端只接受文档登记的错误码族和合法状态迁移。

待实现前固定

  • 上传大小、像素和保留期限的具体数值。
  • VLM confidence 阈值、模型和提示词版本记录格式。
  • 外部管理后台的服务账号认证方式和调用频率。