Files
cmroubao/docs/api.md
T

730 lines
27 KiB
Markdown
Raw Blame History

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.
# 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。
通用错误:
```json
{
"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。
数据库可用:
```json
{"status":"ok"}
```
返回 `200`。数据库不可用时返回 `503`:
```json
{"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 建立用户和设备联合会话。
```json
{
"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": "待设备填写"
}
```
成功:
```json
{
"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`:证据图片必填;参考图创建时为空。
成功:
```json
{
"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`
受鉴权的文件流。只能读取用户有权查看的任务资产;不返回服务端文件路径。
## 采购任务
### `POST /api/v1/tasks`
采购管理员或授权的外部管理系统创建任务。必须带 `Idempotency-Key`。
```json
{
"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`:
```json
{
"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 同时编码这两个字段。
```json
{
"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`。
```json
{
"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`](tasks/T-215.md)。关键规则:
- 顶层和每个 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/devices/heartbeat`
App 空闲或运行时上报设备状态;运行时任务续租使用任务专用 heartbeat。
```json
{
"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。
```json
{}
```
有任务时:
```json
{
"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`。
```json
{
"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`
运行时续租并返回是否请求取消:
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"claim_generation": 1,
"step": "SCAN_RESULTS"
}
```
```json
{
"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 收到取消请求并在安全检查点停止后调用:
```json
{
"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`:
```json
{
"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 伪装成图片检索词:
```json
{
"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`:
```json
{
"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`:
```json
{
"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`
```json
{
"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` 阈值、模型和提示词版本记录格式。
- 外部管理后台的服务账号认证方式和调用频率。