20 KiB
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;MVPlimit最大 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结构可解析但字段不满足 schema429频率限制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
受鉴权的文件流。只能读取用户有权查看的任务资产;不返回服务端文件路径。
采购任务
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/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 -> 候选 -> 终态”顺序提交。所有写接口
重新校验 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",
"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}/candidates
批量保存当前 execution 实际检查的最多 5 个候选,必须带 Idempotency-Key:
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"task_content_sha256": "64-char-lowercase-hex",
"execution_mode": "AI_ASSISTED",
"search_query": "黑色 20L 双肩包",
"provenance": {
"provider_id": "device-configured-provider",
"model": "device-configured-model",
"prompt_version": "candidate-evaluation-v1",
"schema_version": 1
},
"candidates": [
{
"ordinal": 1,
"title": "页面可见标题",
"sku_text": "黑色 20L",
"price": "189.00",
"product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=example",
"image_url": "https://example.invalid/short-lived-image",
"evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"],
"evaluation": {
"decision": "REVIEW",
"score": 0.82,
"matched": ["颜色接近"],
"missing_or_uncertain": ["容量需人工确认"],
"rejection_reasons": [],
"confidence": 0.78
}
}
],
"recommendation": {
"candidate_ordinal": 1,
"policy_version": "local-recommendation-v1",
"reasons": ["当前证据下匹配分最高"]
}
}
MANUAL_FIRST 时 provenance 和 evaluation 为空,但候选观察、搜索词和人工结果仍
可提交。后端校验 ordinal 连续唯一、最多 5 个和任务内容哈希;不请求 product_url
或 image_url,主要证据必须是已鉴权 asset。
POST /api/v1/tasks/{task_id}/complete
人员完成确认后调用,必须带 Idempotency-Key:
{
"execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f",
"task_content_sha256": "64-char-lowercase-hex",
"execution_mode": "AI_ASSISTED",
"outcome": "CANDIDATE_ACCEPTED",
"operator_reason": "款式和预算符合验证要求",
"candidate": {
"ordinal": 1,
"title": "页面可见标题",
"price": "189.00",
"evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"]
},
"order_submitted": false
}
outcome 允许:
CANDIDATE_ACCEPTEDCANDIDATE_REJECTEDNO_MATCHMANUAL_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",
"error": {
"code": "PDD_RISK_CONTROL",
"message": "检测到平台风险提示,已停止自动化",
"step": "SCAN_RESULTS",
"retryable": false
},
"evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"]
}
后端只接受文档登记的错误码族和合法状态迁移。
待实现前固定
- 上传大小、像素和保留期限的具体数值。
- VLM
confidence阈值、模型和提示词版本记录格式。 - 外部管理后台的服务账号认证方式和调用频率。