Files
cmroubao/docs/api.md
T

574 lines
18 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/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_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"
}
```
当前设备自己有已过期 `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。
### `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-25T08:35:00Z"
},
"execution": {
"id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"current_step": "SCAN_RESULTS",
"order_submitted": false
},
"cancel_requested": false,
"server_time": "2026-07-25T08:33:30Z"
}
```
只接受 1 至 64 字节的大写 ASCII step。heartbeat 使用服务端 UTC 更新 execution、
设备最近在线时间、任务 version 和租约,不写高频任务事件;过期运行租约拒绝续租,
任务保持原非终态且绝不回到领取队列。
### `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、有效
运行租约和已存在的管理取消请求可以确认;成功把任务置为 `CANCELED`、结束
execution、清除 claim 秘密并追加带用户/设备 actor 的事件。同 key 重放不产生第二
个事件。
## AI 合约
### `POST /api/v1/tasks/{task_id}/ai/extract-requirements`
只允许当前 execution 调用。后端从任务读取原始图片和文字,客户端不能替换硬约束。
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f"
}
```
响应:
```json
{
"schema_version": 1,
"search_query": "黑色 20L 双肩包",
"category": "双肩包",
"attributes": [
{"name": "color", "value": "black", "source": "text"},
{"name": "capacity", "value": "about 20L", "source": "text"}
],
"sku": "BLACK-20L",
"quantity": 2,
"max_budget": "200.00",
"confidence": 0.86,
"warnings": [],
"manual_review": {
"required": false,
"reasons": []
},
"provenance": {
"provider_id": "configured-provider",
"model": "configured-model",
"prompt_version": "requirement-extraction-v1",
"reference_image_sha256": "64-char-lowercase-hex"
}
}
```
`sku`、`quantity` 和 `max_budget` 必须来自原始任务,不能采用模型返回值。第一层
`ProbeTask` 尚无预算字段,因此 T-103 输出 `max_budget: null` 并追加
`MAX_BUDGET_NOT_PROVIDED` 警告。属性 `source` 的 API 表示使用小写
`title/image/both`;供应商响应在 adapter 内规范化后才进入此合约。
### `POST /api/v1/tasks/{task_id}/ai/evaluate-candidate`
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"candidate_index": 1,
"screenshot_asset_id": "7b733922-f90f-4bc4-a9ad-3e8ec4769122",
"observed": {
"title": "页面可见标题",
"price": "189.00"
}
}
```
响应:
```json
{
"schema_version": 1,
"candidate_index": 1,
"decision": "REVIEW",
"score": 0.82,
"matched": ["颜色接近", "价格未超预算"],
"missing_or_uncertain": ["容量无法从当前页面确认"],
"rejection_reasons": [],
"confidence": 0.78
}
```
`candidate_index` 必须原样回显当前候选 ordinal;`decision` 只允许 `REVIEW`、
`REJECT`、`MANUAL_REQUIRED`。App 按 ordinal 串行评估,每个候选最多调用一次、整批
最多 5 次,并由本地确定性规则产生建议项。模型不能返回页面动作、建议 ordinal、
人工确认状态或订单授权,也没有“提交订单”权限;原任务未提供预算时,任何价格或
预算匹配声明都视为无效输出。
## 执行事件与结果
### `POST /api/v1/tasks/{task_id}/events`
批量追加事件,必须带 `Idempotency-Key`:
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"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}/complete`
人员完成确认后调用,必须带 `Idempotency-Key`:
```json
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"outcome": "CANDIDATE_ACCEPTED",
"operator_reason": "款式和预算符合验证要求",
"candidate": {
"title": "页面可见标题",
"price": "189.00",
"evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"]
},
"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",
"error": {
"code": "PDD_RISK_CONTROL",
"message": "检测到平台风险提示,已停止自动化",
"step": "SCAN_RESULTS",
"retryable": false
},
"evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"]
}
```
后端只接受文档登记的错误码族和合法状态迁移。
## 待实现前固定
- 上传大小、像素和保留期限的具体数值。
- VLM `confidence` 阈值、模型和提示词版本记录格式。
- 外部管理后台的服务账号认证方式和调用频率。