Files
goauto/docs/08-agent-api-contract.md
T

29 KiB
Raw Blame History

generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Android-Agent-API-Contract wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.- wiki_revision: 5da7f2250f851caab6d0d1713f869b619ed55eb2 synchronized_at: 2026-08-20T08:47:11Z

MVP 共享 API 契约

本文件是服务端、Web 和 Android 的共享契约事实来源。字段可以在实施工单中补充,但语义变化必须同步更新本文档。

通用约定

  • 正式环境仅允许 HTTPS。
  • 每台设备使用独立 Bearer Token。
  • 时间使用 RFC 3339 UTC,金额使用整数分。
  • 所有写接口携带 requestId 并保证幂等。
  • 错误响应包含稳定的 code、可读的 message 和 retryable。

管理端:PDD 商品

POST /api/admin/v1/pdd-products
PATCH /api/admin/v1/pdd-products/{productId}
GET /api/admin/v1/pdd-products
GET /api/admin/v1/pdd-products/{productId}

新增请求只提交 requestId 和 url。服务端规范化 URL、提取 goods_id;无法提取时返回 PDD_GOODS_ID_INVALID,已存在时返回 PDD_PRODUCT_EXISTS 和现有商品 ID。编辑请求完整提交 URL、标题、店铺、数字销量/评价、状态和 specs;人工保存覆盖当前值,但不改变已有任务快照。

specs 是维度数组。维度包含非空 name、role(color / size / other)和非空 values;规格值包含 name、selectable,只有颜色值允许包含非负整数分 priceCent。列表支持 keyword 搜索 goods_id、标题或店铺,并支持 status 筛选。商品状态为 pending、active、disabled。列表项另返回 collectionSelectable、可选的 collectionDisabledReason 和 activeCollectionTaskId,供当前页批量选择;服务端提交时仍必须重新校验。

Android 提交结果的契约不增加字段:服务端在保存任务结果的同一事务更新 PDD 商品最新档案。completed 全量覆盖,completed_partial 合并明确获得的数据,failed 不更新;人工 disabled 状态不会被采集自动改回 active。

管理端:虾皮商品

GET /api/admin/v1/shopee-products
POST /api/admin/v1/shopee-products
GET /api/admin/v1/shopee-products/{productId}
PATCH /api/admin/v1/shopee-products/{productId}
POST /api/admin/v1/shopee-products/{productId}/link-pdd
POST /api/admin/v1/shopee-products/{productId}/restore
POST /api/admin/v1/shopee-products/{productId}/specs/values
DELETE /api/admin/v1/shopee-products/{productId}/specs/values
PUT /api/admin/v1/shopee-products/{productId}/specs/mapping
DELETE /api/admin/v1/shopee-products/{productId}/specs/mapping
POST /api/admin/v1/shopee-products/{productId}/specs/mapping/confirm
POST /api/admin/v1/shopee-products/{productId}/specs/mapping/confirm-exact-matches
POST /api/admin/v1/shopee-products/batch-delete

创建与关联/映射相关写操作均提交 requestId 做幂等重放;重放请求返回相同结果并标记 replayed。创建请求提交 shopeeItemId、title、shopName,可选 pddProductId 和 specs;shopeeItemId 重复时返回 SHOPEE_ITEM_ID_EXISTS 和已存在商品 ID。link-pdd 校验 PDD 商品存在且非 disabled,否则分别返回 PDD_PRODUCT_NOT_FOUND 或 PDD_PRODUCT_DISABLED;关联成功后返回值包含 sharedByPddCount,表示当前共用同一 PDD 商品的虾皮商品数。

specs 结构同 PDD 商品的维度/规格值形状,但规格值额外携带 source(import / manual)与可选的 mapping(pddValue、source、status、confidence、reason)。新增规格值固定为 manual 来源;删除规格值仅允许 manual 来源,import 来源返回 SPEC_VALUE_NOT_MANUAL。设置映射时,exact_match 与 ai_match 来源一律写入 pending 状态,与请求体中的 status 无关;只有 manual 来源可以直接写入 confirmed。confirm-exact-matches 仅确认 source=exact_match 且状态为 pending 的映射,不影响 ai_match。

batch-delete 提交 requestId 和 ids(1~500 个),逐条校验引用后返回每条的 status(deleted / skipped)与 reason;引用检查覆盖 SYB 明细(#41)与采购任务(#33/#34),两张表落地前恒不阻塞删除。restore 恢复一条已软删除商品,恢复后原有 PDD 关联与规格映射保持不变。列表接口 status=deleted 筛选已删除商品,默认只返回存活商品。

管理端:SYB 商品明细

GET /api/admin/v1/syb-products
GET /api/admin/v1/syb-products/{productId}
POST /api/admin/v1/syb-products/import
GET /api/admin/v1/syb-products/sync-runs
GET /api/admin/v1/syb-products/sync-runs/{runId}
POST /api/admin/v1/syb-products/{productId}/reparse
POST /api/admin/v1/syb-products/reparse-batch
PATCH /api/admin/v1/syb-products/{productId}/correction

import 仅允许管理员调用。请求体提交 dateFrom、dateTo 后创建持久化后台任务并立即以 202 返回 runId 和 status=running;关闭弹窗、刷新或离开页面不影响任务。没有启用店铺时必须在读取凭据、登录、验证码 OCR 和任意 SYB 网络请求之前返回 422。任意时刻只能有一条 running 记录,内存锁与数据库唯一执行槽共同阻止单进程和跨进程重复导入;冲突时返回正在执行任务的日期范围。

sync-runs 列表支持 page、pageSize、status、dateFrom、dateTo,详情返回日期范围、状态(running / succeeded / failed / interrupted)、处理天数、货运单/明细/新增/覆盖数量、店铺准入与跳过数量、店铺筛选快照哈希、按店铺的 accepted / skipped 统计、操作人和起止时间。列表和详情对已登录角色只读开放。服务启动时遗留的 running 任务改为 interrupted;中途失败或中断已经写入的数据保留,重新导入仍按唯一键覆盖。

列表返回结构化字段(orderCode、shopeeItemId、productTitle、targetColor、targetSize、quantity、unitPriceCent、imageUrl、parseStatus、parseNote、manuallyConfirmed),不含原始 JSON;keyword 匹配货运单号、虾皮商品ID 或商品标题,parseStatus 筛选 success/uncertain/failed。详情额外返回 rawJson(原始 details[] 元素,未做任何改写)。

reparse 与 reparse-batch 只读取已保存的 rawJson 重新执行解析规则,不请求 SYB 接口;请求体 force 为 true 时才覆盖 manuallyConfirmed 的行,默认跳过并在批量结果中标记 skipped_manual。批量结果逐条返回 outcome(reparsed / skipped_manual / unchanged)与解析状态变化,ids 中任何一个不存在都会使整个请求返回错误(与虾皮商品批量删除的"部分成功"语义不同:ID 不存在通常是操作员选错了页面)。

correction 提交 targetColor、targetSize,覆盖解析结果并标记 manuallyConfirmed=true;非空的人工修正与解析成功同等可信,会合并进已关联的虾皮商品档案。

管理端:SYB 店铺

GET    /api/admin/v1/syb-shops
POST   /api/admin/v1/syb-shops
PATCH  /api/admin/v1/syb-shops/{shopId}/name
PATCH  /api/admin/v1/syb-shops/{shopId}/enabled
DELETE /api/admin/v1/syb-shops/{shopId}
POST   /api/admin/v1/syb-shops/discover

列表支持 keyword、enabledOnly、page 和 pageSize,返回店铺原名、规范化名称、启用状态及分页汇总。写操作由 go-admin/Casbin 仅授权管理员;其他角色只读。删除为软删除,不清理已经导入的数据。

discover 提交日期范围,从 SYB 真实货运单列表发现店铺,返回 shopName 和 status(new / exists);发现只读,不自动新增。店铺比较固定执行首尾去空白、全角/半角统一和忽略大小写。同步先完成原始列表完整性校验,再按一次性启用店铺快照过滤;明细响应中的店铺名为空或不在快照内时不得写入。

管理端:采集规则

POST /api/admin/v1/collection-rules
PATCH /api/admin/v1/collection-rules/{ruleId}
DELETE /api/admin/v1/collection-rules/{ruleId}
GET /api/admin/v1/collection-rules
GET /api/admin/v1/collection-rules/templates/pdd-product-detail-v1

规则只有 name 和 content 等当前值,创建成功后立即可用。删除为软删除;已删除规则不能创建新任务,已有任务继续使用任务内快照。

模板接口返回服务端内置的完整 v2 content,以及浏览器包名、别名数量/长度、超时、遍历上限和规格面板滑动动作的可编辑范围。管理端以安全表单修改这些参数,并显示只读 JSON 预览;模板中的采集器 ID、价格解析器、页面根证据、动作类型和语义目标不可在界面中改写。最终创建或更新请求仍由服务端按完整 v2 契约重新校验,不能依赖前端校验。

content 支持原有 schemaVersion: 1 线性步骤和 schemaVersion: 2 类型化采集器契约。v1 契约如下:

{
  "schemaVersion": 1,
  "packageName": "com.xunmeng.pinduoduo",
  "steps": [
    {
      "id": "extract-title",
      "page": "product-detail",
      "activityName": "com.xunmeng.pinduoduo.activity.NewPageActivity",
      "action": "extract",
      "selector": {"resourceId": "com.xunmeng.pinduoduo:id/title"},
      "field": "title",
      "timeoutMs": 5000
    }
  ]
}
  • action 只允许 wait、click、input、back、extract;除 back 外都必须有非空 selector。
  • selector 可组合 resourceId、text、contentDescription、className、clickable,执行操作前必须唯一命中;不唯一时以 RULE_AMBIGUOUS 失败,不猜测目标。
  • 点击步骤仍以唯一命中的节点作为语义证据;若文字节点本身不可点击,只允许沿其父链点击最近的 clickable 容器,不搜索兄弟节点或相近候选。父链不存在可点击容器时以 RULE_ACTION_FAILED 失败。
  • 每一步可以用 packageName 覆盖根级默认值;只允许拼多多、受支持浏览器和 Android 系统选择器包。
  • activityName 可声明该步骤允许执行的精确 Activity;商品详情页规则应同时校验 PDD 包名、目标 Activity 和唯一控件,避免登录页等同包页面误判为商品页。
  • input 必须有 value,extract 必须有 field;单步 timeoutMs 范围为 100~30000 毫秒。
  • optional: true 仅表示单步超时后继续,适合不同 ROM 可能不存在的浏览器/系统“打开”确认层;多匹配和动作执行失败仍立即失败。
  • 规则解析阶段拒绝支付、付款、提交订单等危险目标。完整控件树只在 Android 内存中用于匹配,不保存也不上传。
  • 浏览器和 Android 系统包中的 click 只允许精确目标 打开拼多多APP、打开拼多多 App 或 打开;其它点击在规则解析阶段拒绝。Android 先以浏览器打开服务端已规范化的 PDD URL,再由这些受控步骤进入拼多多。

v2 PDD 商品详情规则

v2 完整示例见 PDD 商品详情规则。规则声明:

  • ruleType: pddProductDetail。
  • navigation.steps:只负责浏览器和系统确认层;不能点击 PDD 页面。
  • pageEvidence:精确的 PDD 包名、Activity 和非空节点证据。
  • collector.collectorId: pddProductDetailV1:引用 Agent 中经过测试的类型化能力,不下发可执行代码。
  • hooks.afterSpecPanelOpen:固定阶段的安全动作;当前只允许对语义目标 specPanel 执行 swipe,方向为上下左右、单动作次数 1~5、等待 0~2000 毫秒,单阶段最多 8 个动作。
  • pageRecovery.transientSoldOut:商品详情页精确显示“商品已售罄”,或顶部区域出现固定兜底标题“相似商品”且缺少主商品规格/购买强证据时,允许对商品页执行一次有界恢复;默认下拉 2 次、间隔 1000 毫秒。推荐卡片自身的标题、价格和销量不属于主商品证据。fallbackTopText 缺省时 Agent 使用“相似商品”兼容旧快照。Agent 必须声明无障碍手势能力,且只把系统回调完成的下拉计为成功。规格面板内的 SKU 售罄不触发。恢复后仍异常时失败码为 PDD_GOODS_SOLD_OUT。
  • navigationRecovery.reopenBrowser:首次跳转未通过精确商品详情页证据时,Agent 重新显式打开任务 urlSnapshot 并重放安全导航步骤;最多恢复 1 次,重开后等待 500~5000 毫秒。登录、验证码、风控、无效链接和进入详情页后的采集失败不触发。
  • collector.dimensionAliases、timeoutsMs 和 limits:分别管理规格标题别名、超时和遍历/SKU 上限。

例如规格面板打开后向上滑动两次,只修改规则:

{"hooks":{"afterSpecPanelOpen":[{"action":"swipe","target":"specPanel","direction":"up","count":2,"settleMs":350}]}}

Agent 使用可扩展的类型化动作注册表,而不是任意脚本。采集规则不能创建订单;未来采购规则可以引用单独审核的创建订单能力,但任何规则都不能执行付款。

Android 的 pddProductDetailV1 采集器执行以下固定流程:

  1. 以规则中的包名、精确 Activity 和节点选择器验证商品详情页,并持续识别登录、验证码、风控和失效商品页面。
  2. 在商品页有限次纵向查找安全规格入口;规格面板必须同时具备规格维度和确认摘要或可滚动区域等强证据。
  3. 规格面板以有界稳定读取恢复到顶部;横向颜色容器和纵向面板容器必须由已识别规格节点的祖先关系锁定,不能只按屏幕中最大滚动区域猜测。
  4. 颜色先归左,再按视觉行执行左到右、右到左交替的蛇形遍历;每次点击都在最新无障碍树中重新定位唯一文字控件,确认选中后连续读取相同价格。
  5. 滚动优先调用目标容器的无障碍前进/后退动作;回退坐标手势时必须等待系统完成回调。视口签名包含规格文字和 bounds,连续稳定后才确认到边。
  6. 尺码仅通过有限次纵向滑动读取,不点击尺码;首次定位后保存容器和选项结构,标题滚出屏幕后仍可续页。颜色价格展开到该颜色下的可用尺码 SKU。
  7. 缺失颜色价格、尺码、第三规格维度或超过 SKU 上限时提交有界的 completed_partial;不猜测缺失值。采集期间离开商品页或出现验证码、登录、风控时明确失败。

无障碍节点只投影为 Agent 进程内的瞬时不可变模型,不序列化、不上传、不写入文件。完整状态机接入后 Agent 才上报 collector.pdd.product-detail.v1。

管理端:采集任务

POST /api/admin/v1/collection-tasks
POST /api/admin/v1/collection-tasks/batch
GET /api/admin/v1/collection-tasks
GET /api/admin/v1/collection-tasks/{taskId}
POST /api/admin/v1/collection-tasks/{taskId}/reset
DELETE /api/admin/v1/collection-tasks/{taskId}

创建请求:

{
  "requestId": "uuid",
  "pddProductId": "product-id",
  "ruleId": "rule-id",
  "deviceId": "device-id-or-null"
}

服务端在事务中复制当前 url、goods_id 和完整规则内容到任务。同一商品已有 pending 或 running 任务时返回 PDD_PRODUCT_TASK_ACTIVE。

批量创建请求:

{
  "requestId": "batch-uuid",
  "pddProductIds": [1, 2, 3],
  "ruleId": 2,
  "deviceId": null
}

pddProductIds 必须包含 1~100 个不重复的有效 ID。一个批次统一使用当前规则和可选设备;每个商品独立调用单任务创建事务并固化自己的 URL、goods_id、规则和设备快照。批次 requestId 与商品 ID 派生稳定的逐项创建标识,同一请求重放返回原任务,不重复创建。

HTTP 200 响应返回 successCount、failureCount 和按请求顺序排列的 items。成功项包含 pddProductId、success: true、taskId 和可选 replayed;失败项包含 success: false、稳定 code 和普通人可理解的 message。商品已停用返回 PDD_PRODUCT_DISABLED,活动任务冲突返回 PDD_PRODUCT_TASK_ACTIVE。逐项失败不回滚其它成功项;请求 JSON、批次 UUID、重复商品或数量上限无效时整体返回 INVALID_REQUEST。

重置只允许 completed、completed_partial 或 failed:

  • 保留 pddProductId、deviceId、urlSnapshot、goodsIdSnapshot、ruleId 和 ruleSnapshot。
  • 删除全部规格维度、规格值、SKU 和 SKU 值关联。
  • 清空标题、店铺、销量、评价、错误与执行时间。
  • 状态恢复为 pending。

running 禁止重置和删除。失败任务允许删除后重新创建。

reset 请求体和失败任务 DELETE 请求体均为 {"requestId":"uuid"}。只允许删除 failed;重置允许 completed、completed_partial、failed。二者均按 requestId 幂等。

Android:注册与心跳

POST /api/agent/v1/register
POST /api/agent/v1/heartbeat

客户端首次启动生成并持久化 installId,直接提交设备信息;服务端收到合法请求即创建或更新设备,不要求注册码、后台审核或人工确认。installId 在服务端唯一,硬件标识不能作为替代唯一键。响应返回 deviceId、Device Token 和心跳间隔,客户端安全保存 Token,服务端只保存其不可逆摘要。后续用 installId 保持同一安装实例的设备身份;心跳上报设备状态和 currentTaskId,服务端据此判断在线/空闲,但任务领取仍以数据库原子约束为准。

注册请求:

{
  "requestId": "uuid",
  "installId": "uuid",
  "name": "OPPO-PKG110",
  "manufacturer": "OPPO",
  "model": "PKG110",
  "androidVersion": "16",
  "agentVersion": "0.1.0",
  "pddVersion": "7.72.0",
  "capabilities": ["rule.schema.v2", "action.swipe.v1", "collector.pdd.product-detail.v1"]
}
  • 新 installId 立即创建设备并返回一次 deviceToken;响应带 Cache-Control: no-store。
  • 相同 requestId 重放只幂等返回原 deviceId,不再次返回 Token,也不覆盖设备信息。
  • 已存在 installId 的新请求必须携带该设备的 Bearer Token,认证成功后幂等更新设备信息,不轮换 Token。
  • 缺少或使用错误/已吊销 Token 返回 HTTP 409 和 DEVICE_INSTALL_ID_CONFLICT。
  • 生产模式只接受 TLS。只有服务部署在可信反向代理之后并显式设置 GOAUTO_TRUST_FORWARDED_PROTO=true 时,服务端才接受代理的 X-Forwarded-Proto: https;不得在服务直接暴露公网时开启。注册接口另有单实例、按直连来源 IP 的基础限流,网关仍须设置共享限流。
  • capabilities 最多 32 项,使用小写的版本化能力名。旧 Agent 可以不提交该字段并继续执行 v1;v2 任务只能由包含规则所需全部能力的设备领取。

管理员设备动作:

POST /api/admin/v1/devices/{deviceId}/disable
POST /api/admin/v1/devices/{deviceId}/token/revoke

两者都要求 go-admin 管理端认证与角色授权。吊销同时停用设备;重新启用和重新签发 Token 不在本工单范围。

心跳请求使用设备 Bearer Token:

{
  "requestId": "uuid",
  "currentTaskId": "task-id-or-null",
  "capabilities": ["rule.schema.v2", "action.swipe.v1"]
}

服务端校验 currentTaskId 必须与该设备唯一的 running 任务一致,不一致返回 HTTP 409 和 DEVICE_TASK_MISMATCH,不更新心跳。响应返回服务端确认的 currentTaskId、online、由运行任务派生的 busy、服务端时间和下一次心跳间隔。相同 requestId 重放不推进心跳时间。

默认心跳间隔 15 秒、离线阈值 45 秒、扫描间隔 15 秒。超过阈值后设备标为 offline;其 running 任务同时清除租约和活动 guard,转为 failed,错误码为 DEVICE_OFFLINE,且不自动重试或换机。设备重新发送合法心跳后可以恢复 online,但失败任务不会自动恢复。

Android:获取、领取和开始任务

GET  /api/agent/v1/tasks/next
POST /api/agent/v1/tasks/{taskId}/claim
POST /api/agent/v1/tasks/{taskId}/start

claim 和 start 请求体都为 {"requestId":"uuid"}。相同 requestId 重放返回首次成功后的当前任务载荷,不延长租约、不增加租约版本。next 没有可用任务时返回 HTTP 204。

领取规则:

  1. 指定设备的任务只能由指定设备领取。
  2. deviceId 为空的任务可由在线且没有活动任务的设备领取。
  3. claim 在一个数据库事务中设置设备和租约,竞争失败返回 TASK_ALREADY_CLAIMED。
  4. 设备已有活动任务时返回 DEVICE_BUSY。
  5. 指定设备和领取设备必须具备规则快照要求的全部能力;不兼容时返回 DEVICE_CAPABILITY_MISMATCH。未指定设备任务会跳过不兼容设备。

claim 保持任务为 pending,写入设备、两分钟领取租约和递增的 leaseVersion;start 只接受当前设备持有的有效租约,将状态原子改为 running、记录开始时间并续租。Android 进程还必须以本地互斥锁确保同一时刻只有一个任务进入执行器。

任务载荷包含:

{
  "taskId": "task-id",
  "pddProductId": "product-id",
  "urlSnapshot": "https://mobile.yangkeduo.com/goods.html?goods_id=123",
  "goodsIdSnapshot": "123",
  "ruleId": "rule-id",
  "ruleSnapshot": {},
  "timeoutSeconds": 120
}

规则已经软删除不影响载荷和执行。

Android:提交结果

POST /api/agent/v1/tasks/{taskId}/result
{
  "requestId": "uuid",
  "status": "completed_partial",
  "product": {
    "pddGoodsId": "123",
    "title": "商品标题",
    "shopName": "店铺名",
    "salesText": "已拼5872件",
    "reviewCount": 64
  },
  "dimensions": [
    {"key": "color", "name": "颜色", "values": ["黑色", "白色"]},
    {"key": "size", "name": "尺码", "values": ["M", "L"]}
  ],
  "colorPrices": [
    {"color": "黑色", "priceCent": 1000},
    {"color": "白色", "priceCent": 1200}
  ],
  "skus": [
    {"specs": {"color": "黑色", "size": "M"}, "priceCent": 1000, "available": true}
  ],
  "missing": ["白色/L"]
}

服务端验证任务当前设备和状态后,在一个事务中更新 collection_task 结果字段并重建结果子表。终态任务拒绝不同内容的再次提交;相同 requestId 幂等返回原响应。

Android:提交失败

POST /api/agent/v1/tasks/{taskId}/fail
错误码 含义 默认重试
DEVICE_OFFLINE 执行中连接中断 否
ACCESSIBILITY_NOT_READY 无障碍未绑定 否
PDD_LOGIN_REQUIRED PDD 登录失效 否
PDD_CAPTCHA_REQUIRED 验证码或人机验证 否
PDD_RISK_CONTROL 风控页面 否
PDD_LINK_INVALID 商品链接无效、商品不存在或已下架 否
PDD_GOODS_SOLD_OUT 商品页一次性下拉恢复后仍显示商品已售罄,或恢复动作失败 否
PDD_DETAIL_ENTRY_FAILED 一次性重开浏览器后仍未进入精确 PDD 商品详情页 否
RULE_NOT_MATCHED 找不到唯一控件或页面 否
RULE_AMBIGUOUS 规则同时匹配多个控件 否
TASK_ALREADY_CLAIMED 未指定任务已被其他设备领取 否
DEVICE_BUSY 设备已有活动任务 否
DEVICE_CAPABILITY_MISMATCH 设备缺少任务规则要求的版本化能力 否

采购任务共享契约(#33、#34)

本节固定采购域的数据和接口边界;purchase_task、purchase_task_attempt、规则校验及服务端 HTTP 状态机已落地,Admin 页面和 Android 执行动作分别由后续工单实现。任何实现都不得扩展为自动支付。

任务与快照

  • executionMode 只能是 rehearsal 或 live,创建后不可修改。
  • 正式 live 任务必须引用一条 syb_product;无副作用的 rehearsal 可以不引用 SYB。
  • 创建时固化 SYB、虾皮商品、PDD 商品、目标规格、映射规格、数量、价格区间、币种、URL、goods_id、规则和可选 PDD 账号引用。以后商品档案修改不会改写任务。
  • Android 无法可靠读取当前 PDD 账号,因此 pddAccountId 和 pddAccountRefSnapshot 均可空。已知账号时参与账号级串行,未知时不阻止任务。
  • 地址后缀由任务 ID 唯一确定为 _cg{taskId};#34 创建任务时必须在同一事务内回写快照。
  • 一个任务最多对应一个 PDD 订单;重新采购必须新建任务,旧任务和旧订单保留。

状态集合:

状态 含义 是否占用 SYB 活动槽
pending 待执行 是
spec_probe_pending 第一趟探测结束,待服务端固化规格并重新派发 是
running Agent 执行中 是
rehearsal_completed 演练安全结束,未改地址、未创建订单 否
order_submit_started 不可逆标记已落库,只能核单,禁止再次点击 是
order_created 已取得唯一 PDD 订单号和下单时间 是
order_result_unknown 无法确认是否下单,必须人工处理 是
failed / cancelled 终态 否

同一个 sybProductId 最多一个占用活动槽的任务;一个设备最多一个执行中的采购任务;已知的同一个 PDD 账号最多一个执行中的采购任务。终态历史不删除。

规则快照与能力

采购规则是 JSON 对象:

{
  "schemaVersion": 1,
  "ruleType": "pddPurchase",
  "requiredCapabilities": ["purchase.rehearsal.v1"],
  "actions": [
    {"type": "openProduct"},
    {"type": "verifyProduct"},
    {"type": "selectSpec"},
    {"type": "setQuantity"},
    {"type": "verifyUnitPrice"},
    {"type": "verifyOrderSummary"}
  ]
}

演练规则必须包含 purchase.rehearsal.v1,并且不能包含 updateShippingAddress、createOrder、readOrderResult。正式规则必须包含 purchase.live.v1;改地址和创建订单还分别要求 purchase.address-update.v1、purchase.order-create.v1。probeSpecs 要求 purchase.spec-probe.v1。任意模式下,pay、名称包含 payment 的动作以及未知动作一律拒绝;服务端不下发任意脚本。

管理端接口

方法 路径 幂等键 / 说明
POST /api/admin/v1/purchase-tasks requestId;单条创建
POST /api/admin/v1/purchase-tasks/{taskId}/authorize-repurchase 一次性授权;创建新任务后自动消耗
POST /api/admin/v1/purchase-tasks/{taskId}/payment-review paid 或 unpaid,管理员和采购员可操作
POST /api/admin/v1/purchase-tasks/{taskId}/writeback-candidate 人工选择回填候选;后选的已支付订单覆盖同一 SYB 明细的旧选择
POST /api/admin/v1/purchase-tasks/{taskId}/cancel 人工取消;执行中和已开始提交订单时禁止直接取消
POST /api/admin/v1/purchase-tasks/{taskId}/resolve-unknown 人工把结果未知解除为已创建订单或已取消
POST /api/admin/v1/purchase-tasks/{taskId}/spec-decision 固化第一趟规格探测决策;同一 attempt 不可改写

本工单只提供状态机写接口;列表、详情、批量创建等 Admin 查询与交互接口由 #35 按已确认原型补齐。

创建请求的价格保护使用整数分:referenceUnitPriceCent、minUnitPriceCent、maxUnitPriceCent 和 currency。执行时以 PDD App 实际单价校验;低于最小值或高于最大值均返回普通人可理解的价格越界错误,不考虑优惠券,不以订单总价替代单价判断。

Android 接口(由 #34 实现)

方法 路径 说明
GET /api/agent/v1/purchase-tasks/next 返回与设备能力兼容的指定任务或空闲任务
POST /api/agent/v1/purchase-tasks/{taskId}/claim requestId 原子领取,并建立设备/可选账号租约
POST /api/agent/v1/purchase-tasks/{taskId}/start 创建不可变 taskAttemptId
POST /api/agent/v1/purchase-tasks/{taskId}/order-submit-started 创建订单前先落不可逆标记;演练任务永远拒绝
POST /api/agent/v1/purchase-tasks/{taskId}/result 请求体携带 taskAttemptId 和 requestId;幂等提交演练、规格探测、订单或失败结果

结果提交至少关联 taskId、taskAttemptId、deviceId、规则快照哈希和结构化结果。相同 attempt 的相同结果重复提交返回同一事实;不同内容拒绝覆盖。慢路径第一趟提交规格后释放设备与已知账号租约,任务进入 spec_probe_pending;服务端固化同一 attempt 的 AI/人工决策后,第二趟使用新的 attempt 重新派发。无匹配结果则明确失败。order_result_unknown 只允许管理员或采购员人工解除,永不自动重派。

Agent Room 只保存恢复执行所需的任务、attempt 和 Outbox;服务端数据库是最终事实来源。双方均不保存原始控件树、截图、PDD 凭据或完整收货地址。

错误码 普通提示
PURCHASE_MODE_NOT_ALLOWED 当前任务模式不允许执行此操作
PURCHASE_RULE_INVALID 采购规则不可用,请联系管理员
AGENT_CAPABILITY_MISMATCH 当前手机版本不支持这个任务
PURCHASE_SPEC_NOT_MATCHED 没有找到可用的商品规格
PURCHASE_PRICE_OUT_OF_RANGE 当前商品单价超出允许范围
PURCHASE_ADDRESS_UPDATE_FAILED 收货地址修改失败,未创建订单
PURCHASE_ORDER_RESULT_UNKNOWN 无法确认订单是否创建,请人工检查
PURCHASE_PAYMENT_FORBIDDEN 系统禁止自动付款