407 lines
10 KiB
Markdown
407 lines
10 KiB
Markdown
# 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 超时判断操作失败,必须查询资源最终状态。
|
||
|
||
通用错误:
|
||
|
||
```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` 依赖暂不可用
|
||
|
||
## 认证
|
||
|
||
### 管理 Web 会话
|
||
|
||
`POST /login` 接受表单账号密码,成功后设置 `HttpOnly`、`Secure`、`SameSite=Lax`
|
||
会话 Cookie。`POST /logout` 清除会话。Web 会话不能调用设备执行接口。
|
||
|
||
### `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-or-jwt-token",
|
||
"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 中。
|
||
|
||
## 资产
|
||
|
||
### `POST /api/v1/assets`
|
||
|
||
管理会话上传任务参考图片;App 也可用设备会话上传执行截图。请求必须带
|
||
`Idempotency-Key`。
|
||
|
||
表单字段:
|
||
|
||
- `file`:必填,MVP 允许 JPEG/PNG/WebP;实际大小上限在配置中固定。
|
||
- `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"
|
||
}
|
||
```
|
||
|
||
### `GET /api/v1/assets/{asset_id}/content`
|
||
|
||
受鉴权的文件流。只能读取用户有权查看的任务资产;不返回服务端文件路径。
|
||
|
||
## 采购任务
|
||
|
||
### `POST /api/v1/tasks`
|
||
|
||
采购管理员或授权的外部管理系统创建任务。必须带 `Idempotency-Key`。
|
||
|
||
```json
|
||
{
|
||
"source_ref": "external-admin-task-10001",
|
||
"title": "黑色双肩包",
|
||
"description": "容量约20L,外观接近参考图",
|
||
"image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3",
|
||
"quantity": 2,
|
||
"max_budget": "200.00"
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- `title` 或 `description` 至少一个非空,`image_asset_id` 必填。
|
||
- `quantity` 是正整数。
|
||
- `max_budget` 可为空,否则为大于零、最多两位小数的 CNY 金额。
|
||
- 同一调用方的 `source_ref` 如填写必须唯一。
|
||
|
||
成功返回 `201`:
|
||
|
||
```json
|
||
{
|
||
"id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
|
||
"status": "PENDING",
|
||
"title": "黑色双肩包",
|
||
"quantity": 2,
|
||
"max_budget": "200.00",
|
||
"created_at": "2026-07-25T08:30:00Z"
|
||
}
|
||
```
|
||
|
||
### `GET /api/v1/tasks`
|
||
|
||
管理端列表。可选参数:`status`、`created_from`、`created_to`、`limit`、`cursor`。
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
|
||
"title": "黑色双肩包",
|
||
"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`:有权限的摘要。
|
||
|
||
### `POST /api/v1/tasks/{task_id}/cancel`
|
||
|
||
管理端取消任务。`PENDING` 可立即取消;执行中只设置取消请求,App 在安全检查点确认
|
||
后进入 `CANCELED`。终态返回 `409`。
|
||
|
||
```json
|
||
{
|
||
"reason": "需求已撤销"
|
||
}
|
||
```
|
||
|
||
## 设备与领取
|
||
|
||
### `POST /api/v1/devices/heartbeat`
|
||
|
||
App 空闲或运行时上报设备状态;运行时任务续租使用任务专用 heartbeat。
|
||
|
||
```json
|
||
{
|
||
"device_id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8",
|
||
"app_version": "0.1.0",
|
||
"pdd_version": "device-observed-value",
|
||
"readiness": {
|
||
"accessibility_enabled": true,
|
||
"pdd_installed": true,
|
||
"active_task_id": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### `POST /api/v1/tasks/claim-next`
|
||
|
||
由用户点击触发,在单个数据库事务中领取下一条任务。必须带 `Idempotency-Key`。
|
||
|
||
```json
|
||
{
|
||
"device_id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8"
|
||
}
|
||
```
|
||
|
||
有任务时:
|
||
|
||
```json
|
||
{
|
||
"task": {
|
||
"id": "37c9c715-9b51-4ed5-984e-66dad2710c71",
|
||
"status": "CLAIMED",
|
||
"title": "黑色双肩包",
|
||
"description": "容量约20L,外观接近参考图",
|
||
"image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3",
|
||
"quantity": 2,
|
||
"max_budget": "200.00"
|
||
},
|
||
"claim_token": "returned-once",
|
||
"claim_expires_at": "2026-07-25T08:40:00Z"
|
||
}
|
||
```
|
||
|
||
没有任务返回 `204`。设备未就绪或已有活跃任务返回 `409`。幂等重放返回同一领取
|
||
结果;claim token 的安全重放形式在实现前通过测试固定。
|
||
|
||
### `POST /api/v1/tasks/{task_id}/start`
|
||
|
||
把当前设备持有的 `CLAIMED` 任务改为 `RUNNING` 并创建 execution。请求带
|
||
`X-Claim-Token` 和 `Idempotency-Key`。
|
||
|
||
### `POST /api/v1/tasks/{task_id}/heartbeat`
|
||
|
||
运行时续租并返回是否请求取消:
|
||
|
||
```json
|
||
{
|
||
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
|
||
"step": "SCAN_RESULTS",
|
||
"client_time": "2026-07-25T08:33:30Z"
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"claim_expires_at": "2026-07-25T08:43:30Z",
|
||
"cancel_requested": false
|
||
}
|
||
```
|
||
|
||
### `POST /api/v1/tasks/{task_id}/release`
|
||
|
||
只允许尚未开始的 `CLAIMED` 任务释放回 `PENDING`。运行中使用取消/失败流程。
|
||
|
||
## 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"}
|
||
],
|
||
"quantity": 2,
|
||
"max_budget": "200.00",
|
||
"confidence": 0.86,
|
||
"warnings": []
|
||
}
|
||
```
|
||
|
||
### `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,
|
||
"decision": "REVIEW",
|
||
"score": 0.82,
|
||
"matched": ["颜色接近", "价格未超预算"],
|
||
"missing_or_uncertain": ["容量无法从当前页面确认"],
|
||
"rejection_reasons": [],
|
||
"confidence": 0.78
|
||
}
|
||
```
|
||
|
||
`decision` 只允许 `REVIEW`、`REJECT`、`MANUAL_REQUIRED`。模型没有“提交订单”权限。
|
||
|
||
## 执行事件与结果
|
||
|
||
### `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` 表达。
|
||
|
||
### `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"]
|
||
}
|
||
```
|
||
|
||
后端只接受文档登记的错误码族和合法状态迁移。
|
||
|
||
## 待实现前固定
|
||
|
||
- 上传大小、像素和保留期限的具体数值。
|
||
- access token、管理会话和 claim lease 的最终时长。
|
||
- claim token 幂等重放与轮换细节。
|
||
- VLM `confidence` 阈值、模型和提示词版本记录格式。
|
||
- 外部管理后台的服务账号认证方式和调用频率。
|