Files
cmroubao/docs/api.md
T

41 KiB
Raw Blame History

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。管理会话固定 8 小时绝对有效期;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": 3600,
  "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,固定 1 小时过期。用户必须为有效 BUYER,设备必须预授权且启用;未绑定设备在首次成功 登录时原子绑定当前采购员,已绑定其他采购员时拒绝。T-204 不提供设备自助登记、 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:有权限的摘要。

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}/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 返回同一 schema v1 command。

命令只含 task/execution/content hash、原始 SKU/数量、candidate key、观测标题/ 规格/价格和 T-214 四个指纹,不含第三方 URL。observed_ordinal 仅是重新定位提示。 command_sha256 固定 canonical payload,投递重试不得变化。

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-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。正式参考图检索使用固定审计值 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",
  "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",
      "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": {
        "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": {
    "candidate_ordinal": 1,
    "policy_version": "local-recommendation-v1",
    "reasons": ["当前证据下匹配分最高"]
  }
}

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、详情签名和规格证据哈希生成稳定的 execution-scoped candidate_key。Admin 任务详情在 observation 的 identity 中 返回该 key、四个指纹及 identity 版本;旧 v6 observation 可没有 identity。 product_url 和 image_url 只有设备实际取得可信 URL 时才提交,不能伪造;后端不 请求这些 URL,主要证据必须是已鉴权 asset。

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 阈值、模型和提示词版本记录格式。
  • 外部管理后台的服务账号认证方式和调用频率。