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: a5c60ff2d9d64c824be176bb1630d884379f6c7c # MVP 共享 API 契约 本文件是服务端、Web 和 Android 的共享契约事实来源。字段可以在实施工单中补充,但语义变化必须同步更新本文档。 ## 通用约定 - 正式环境仅允许 HTTPS。 - 每台设备使用独立 Bearer Token。 - 时间使用 RFC 3339 UTC,金额使用整数分。 - 所有写接口携带 `requestId` 并保证幂等。 - 错误响应包含稳定的 `code`、可读的 `message` 和 `retryable`。 ## 管理端:PDD 商品 ```http 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`。 Android 提交结果的契约不增加字段:服务端在保存任务结果的同一事务更新 PDD 商品最新档案。`completed` 全量覆盖,`completed_partial` 合并明确获得的数据,`failed` 不更新;人工 `disabled` 状态不会被采集自动改回 `active`。 ## 管理端:采集规则 ```http 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 契约如下: ```json { "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 商品详情规则](rules/pdd-product-detail-v2.proposed.json)。规则声明: - `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 上限。 例如规格面板打开后向上滑动两次,只修改规则: ```json {"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`。 ## 管理端:采集任务 ```http POST /api/admin/v1/collection-tasks 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} ``` 创建请求: ```json { "requestId": "uuid", "pddProductId": "product-id", "ruleId": "rule-id", "deviceId": "device-id-or-null" } ``` 服务端在事务中复制当前 `url`、`goods_id` 和完整规则内容到任务。同一商品已有 `pending` 或 `running` 任务时返回 `PDD_PRODUCT_TASK_ACTIVE`。 重置只允许 `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:注册与心跳 ```http POST /api/agent/v1/register POST /api/agent/v1/heartbeat ``` 客户端首次启动生成并持久化 `installId`,直接提交设备信息;服务端收到合法请求即创建或更新设备,不要求注册码、后台审核或人工确认。`installId` 在服务端唯一,硬件标识不能作为替代唯一键。响应返回 `deviceId`、Device Token 和心跳间隔,客户端安全保存 Token,服务端只保存其不可逆摘要。后续用 `installId` 保持同一安装实例的设备身份;心跳上报设备状态和 `currentTaskId`,服务端据此判断在线/空闲,但任务领取仍以数据库原子约束为准。 注册请求: ```json { "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 任务只能由包含规则所需全部能力的设备领取。 管理员设备动作: ```http POST /api/admin/v1/devices/{deviceId}/disable POST /api/admin/v1/devices/{deviceId}/token/revoke ``` 两者都要求 go-admin 管理端认证与角色授权。吊销同时停用设备;重新启用和重新签发 Token 不在本工单范围。 心跳请求使用设备 Bearer Token: ```json { "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:获取、领取和开始任务 ```http 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 进程还必须以本地互斥锁确保同一时刻只有一个任务进入执行器。 任务载荷包含: ```json { "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:提交结果 ```http POST /api/agent/v1/tasks/{taskId}/result ``` ```json { "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:提交失败 ```http 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` | 设备缺少任务规则要求的版本化能力 | 否 |