Files
cmroubao/docs/api.md
T

423 lines
11 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 超时判断操作失败,必须查询资源最终状态。
通用错误:
```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"}
],
"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,
"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` 阈值、模型和提示词版本记录格式。
- 外部管理后台的服务账号认证方式和调用频率。