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

650 lines
50 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.
<!-- gitea-wiki-mirror:start -->
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: b722977b0c8b7db2ce0d3c8c2a1351f42941cb24
synchronized_at: 2026-08-27T07:27:38Z
<!-- gitea-wiki-mirror:end -->
# 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`。列表项另返回 `collectionSelectable`、可选的 `collectionDisabledReason` 和 `activeCollectionTaskId`,供当前页批量选择;服务端提交时仍必须重新校验。
Android 提交结果的契约不增加字段:服务端在保存任务结果的同一事务更新 PDD 商品最新档案。`completed` 全量覆盖,`completed_partial` 合并明确获得的数据,`failed` 不更新;人工 `disabled` 状态不会被采集自动改回 `active`。
## 管理端:虾皮商品
```http
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/{productId}/specs/mapping/preview-auto-size
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`。
`preview-auto-size` 是只读计算接口(使用 `POST` 触发计算,不写数据库),无需 `requestId`。它读取当前蝦皮尺码与关联 PDD 的可选尺码,只返回格式统一后唯一确定的匹配;响应含 `items[]`(`valueName`、可选 `pddValue`、`status=preserved|matched|pending`、`reason`)、`pddValues`、`matchedCount` 和 `pendingCount`。已确认且目标仍存在的映射标为 `preserved`;无唯一结果标为 `pending`。Admin 的显式“保存修改”仍通过既有设置/确认接口落库。
设置映射时,`pddValue` 必须是关联 PDD 商品同角色下当前可选的原始规格标签,否则返回 `INVALID_REQUEST`。PDD 重新采集后旧目标消失时,Admin 标记失效;采购预检与任务创建返回 `PURCHASE_SPEC_MAPPING_REQUIRED` 和“规格匹配已失效,请重新选择 PDD 规格”,不得把旧标签下发给 Agent。
`batch-delete` 提交 `requestId` 和 `ids`(1~500 个),逐条校验引用后返回每条的 `status`(`deleted` / `skipped`)与 `reason`;引用检查覆盖 SYB 明细(#41)与采购任务(#33/#34),两张表落地前恒不阻塞删除。`restore` 恢复一条已软删除商品,恢复后原有 PDD 关联与规格映射保持不变。列表接口 `status=deleted` 筛选已删除商品,默认只返回存活商品。
## 管理端:SYB 商品明细
```http
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 店铺
```http
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`);发现只读,不自动新增。店铺比较固定执行首尾去空白、全角/半角统一和忽略大小写。同步先完成原始列表完整性校验,再按一次性启用店铺快照过滤;明细响应中的店铺名为空或不在快照内时不得写入。
## 管理端:采集规则
```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. 在商品页有限次纵向查找安全规格入口;规格面板必须同时具备规格维度和确认摘要或可滚动区域等强证据。
- 入口候选排除评价、评论、晒单和问答上下文,不使用全页面“规格词 + 任意数字”猜测;底部兜底必须命中真实购买/拼单文字及其可点击链路,且排除提交订单与支付语义。
- 商品评价页与详情页共用 `NewPageActivity` 时,首次误触允许全局返回、重新验证详情页并重新定位一次;第二次误触停止。点击无变化、误入评价页和面板证据不匹配分别返回 `SPEC_ENTRY_CLICK_NO_EFFECT`、`SPEC_ENTRY_OPENED_REVIEW`、`SPEC_PANEL_EVIDENCE_NOT_MATCHED`。
3. 规格面板以有界稳定读取恢复到顶部;横向颜色容器和纵向面板容器必须由已识别规格节点的祖先关系锁定,不能只按屏幕中最大滚动区域猜测。
4. 颜色先归左,再按视觉行执行左到右、右到左交替的蛇形遍历;每次点击都在最新无障碍树中重新定位唯一文字控件。点击后必须取得目标选中、已选摘要或规格面板选中状态/价格变化证据,等待短暂刷新后连续读取相同价格;其他颜色残留选中标记不能阻塞当前颜色,同价合法,但动作无效果时不能沿用点击前旧价格。结构化轨迹只记录短颜色标签、点击结果、证据类型、价格采样和 `selection_not_confirmed`、`price_not_found`、`price_not_stable` 原因。
5. 滚动优先调用目标容器的无障碍前进/后退动作;回退坐标手势时必须等待系统完成回调。视口签名包含规格文字和 bounds,连续稳定后才确认到边。
6. 尺码仅通过有限次纵向滑动读取,不点击尺码;首次定位后保存容器和选项结构,标题滚出屏幕后仍可续页。颜色价格展开到该颜色下的可用尺码 SKU。
7. 缺失颜色价格、尺码、第三规格维度或超过 SKU 上限时提交有界的 `completed_partial`;不猜测缺失值。采集期间离开商品页或出现验证码、登录、风控时明确失败。
无障碍节点只投影为 Agent 进程内的瞬时不可变模型,不序列化、不上传、不写入文件。完整状态机接入后 Agent 才上报 `collector.pdd.product-detail.v1`。
## 管理端:AI 规格匹配设置
```http
GET /api/admin/v1/ai-matching-settings
PUT /api/admin/v1/ai-matching-settings
POST /api/admin/v1/ai-matching-settings/test
```
这是采购规格匹配的单例配置,Provider 固定为 `openai_compatible`。管理员可读取、保存和测试连接;保存请求包含 `enabled`、`baseUrl`、`model`、`timeoutSeconds` 与 `apiKey`。`timeoutSeconds` 默认 15,允许范围为 3~600 秒。根据 #62 已确认的内部部署例外,管理员 `GET` 响应会返回已保存的明文 `apiKey`,供下次查看和替换;采购员 `GET` 只返回 `enabled`,不能读取 Provider、地址、模型或 API Key,也不能保存或测试。
Base URL 支持内网或公网的 `http://`、`https://`,不限制为局域网地址。服务端只在管理员点击测试或采购规格没有唯一确定性结果时,才向 Provider 发送目标颜色/尺码和 PDD 可选颜色/尺码;请求中不包含控件树、截图、地址、账号或订单。HTTP 不加密 API Key 的传输,生产环境建议使用 HTTPS。服务端禁止跟随 Provider 重定向,Provider 返回的颜色/尺码必须逐字等于当前候选原始标签,否则按无匹配处理。
## 管理端:采集任务
```http
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}
```
创建请求:
```json
{
"requestId": "uuid",
"pddProductId": "product-id",
"ruleId": "rule-id",
"deviceId": "device-id-or-null"
}
```
服务端在事务中复制当前 `url`、`goods_id` 和完整规则内容到任务。同一商品已有 `pending` 或 `running` 任务时返回 `PDD_PRODUCT_TASK_ACTIVE`。
批量创建请求:
```json
{
"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:注册与心跳
```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`,以及采购任务的 `running` / `order_submit_started`。不一致返回 HTTP 409 和 `DEVICE_TASK_MISMATCH`,不更新心跳。响应返回服务端确认的 `currentTaskId`、`online`、由两类任务统一派生的 `busy`、服务端时间和下一次心跳间隔。相同 `requestId` 重放不推进心跳时间;若数据库异常存在跨任务域同时运行,服务端返回内部一致性错误,不静默选择其中一条。
默认心跳间隔 15 秒、离线阈值 45 秒、扫描间隔 15 秒。超过阈值后设备标为 `offline`:运行中的采集任务和未进入订单提交边界的采购任务清除租约与运行 guard,转为 `failed`,对应采购 attempt 同步失败并结束,错误码为 `DEVICE_OFFLINE`,且不自动重试或换机。采购任务已处于 `order_submit_started` 时必须转为 `order_result_unknown`,释放设备/账号运行槽并等待人工核对,禁止自动重派或再次点击创建订单。设备重新发送合法心跳后可以恢复 `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` | 找不到唯一控件或页面 | 否 |
| `SPEC_ENTRY_CLICK_NO_EFFECT` | 规格入口点击后页面没有变化 | 否 |
| `SPEC_ENTRY_OPENED_REVIEW` | 规格入口误进入商品评价页且受控恢复失败 | 否 |
| `SPEC_PANEL_EVIDENCE_NOT_MATCHED` | 页面发生变化,但规格面板强证据不足 | 否 |
| `RULE_AMBIGUOUS` | 规则同时匹配多个控件 | 否 |
| `TASK_ALREADY_CLAIMED` | 未指定任务已被其他设备领取 | 否 |
| `DEVICE_BUSY` | 设备已有活动任务 | 否 |
| `DEVICE_CAPABILITY_MISMATCH` | 设备缺少任务规则要求的版本化能力 | 否 |
## 采购任务共享契约(#33、#34、#36、#42、#44、#53、#67)
本节固定采购域的数据和接口边界;`purchase_task`、`purchase_task_attempt`、规则校验、服务端 HTTP 状态机、Admin 已有任务管理、SYB 创建入口、Android 演练执行及 #36 正式地址/订单动作已经落地。#36 尚未获得真机创建订单授权和验收。任何实现都不得扩展为自动支付。
### 任务与快照
- `executionMode` 只能是 `rehearsal` 或 `live`,创建后不可修改。
- 正式 `live` 任务必须引用一条 `syb_product`;无副作用的 `rehearsal` 可以不引用 SYB。
- 创建时固化 SYB、蝦皮订单号、虾皮商品、PDD 商品、目标规格、映射规格、数量、价格区间、币种、URL、`goods_id`、规则和可选 PDD 账号引用。正式任务的 `shopeeOrderNoSnapshot` 来自创建时的 `syb_product.order_code`;同一订单号可对应多条任务,之后来源商品修改不会改写任务快照。
- 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 对象。#53 在既有 schema v1 中增加受限参数,不引入任意脚本:
```json
{
"schemaVersion": 1,
"ruleType": "pddPurchase",
"requiredCapabilities": ["purchase.rehearsal.v1"],
"actions": [
{
"type": "openProduct",
"textAliases": ["打开拼多多APP", "打开"],
"waitAfterMs": 1000
},
{
"type": "openSpecPanel",
"textAliases": ["选择规格"],
"swipeAfter": {
"direction": "up",
"count": 2,
"durationMs": 500,
"intervalMs": 1000
}
},
{"type": "selectSpec"},
{"type": "setQuantity"},
{"type": "verifyUnitPrice"},
{"type": "verifyOrderSummary"}
]
}
```
安全动作参数矩阵:
| action | `textAliases` | `waitAfterMs` | `swipeAfter` |
|---|---:|---:|---:|
| `openProduct` | 是 | 是 | 是 |
| `verifyProduct` | 是 | 是 | 否 |
| `openSpecPanel` | 是 | 是 | 是 |
| `selectSpec` | 是 | 是 | 是 |
| `setQuantity` | 是 | 是 | 否 |
| `verifyUnitPrice` | 是 | 是 | 否 |
| `verifyOrderSummary` | 是 | 是 | 否 |
| `probeSpecs` | 是 | 是 | 是 |
参数规则:
- `textAliases` 省略时使用 Agent 对该类型化动作的内置语义目标;显式提供时必须包含 1~16 个互不重复、无首尾空白的非空文字,每项最多 64 个字符。
- 文字候选按控件文字或内容描述**精确匹配**;候选合并后必须唯一命中。点击动作只允许点击唯一文字节点或其最近的可点击父容器,不允许模糊匹配、猜测相近候选、改点兄弟节点。
- `textAliases` 不能包含地址修改、创建/提交订单、订单号或支付相关文字,防止用安全 action 绕过危险动作类型和能力门禁。
- `waitAfterMs` 表示动作成功后的等待时间,范围为 0~30000 毫秒;省略时为 0。
- `swipeAfter` 表示动作成功后执行一个有限滑动计划。`direction` 只能为 `up` / `down` / `left` / `right`,`count` 为 1~10,`durationMs` 为 100~2000,`intervalMs` 为 0~5000 且省略时为 0。
- 未在矩阵中授权的 action/参数组合、未知字段、空候选和越界值一律拒绝。`updateShippingAddress`、`createOrder`、`readOrderResult` 等正式动作在其独立高风险契约完成前不接受上述参数。
- 旧的仅含 `actions[].type` 的规则继续有效:候选使用 Agent 内置语义,等待为 0,不执行动作后滑动。
- 服务端保存创建任务时收到的完整原始规则快照;规则后来更新为规则 B,不会改变已有任务中的规则 A 快照。
演练规则必须包含 `purchase.rehearsal.v1`,并且不能包含 `updateShippingAddress`、`createOrder`、`readOrderResult`。正式规则必须包含 `purchase.live.v1`;改地址和创建订单还分别要求 `purchase.address-update.v1`、`purchase.order-create.v1`。`probeSpecs` 要求 `purchase.spec-probe.v1`。任意模式下,`pay`、名称包含 `payment` 的动作以及未知动作一律拒绝;服务端不下发任意脚本。
### 管理端接口
| 方法 | 路径 | 幂等键 / 说明 |
|---|---|---|
| `GET` | `/api/admin/v1/purchase-tasks` | 分页列表;可按任务 ID、蝦皮订单号 `shopeeOrderNo`、状态、模式、SYB 商品 ID 和 PDD 订单号筛选 |
| `GET` | `/api/admin/v1/purchase-tasks/{taskId}` | 只读任务详情与 attempt 历史;不返回规则原文、Token、凭据、完整地址、控件树或截图 |
| `POST` | `/api/admin/v1/purchase-tasks` | `requestId`;单条创建 |
| `POST` | `/api/admin/v1/purchase-tasks/batch-preview` | `sybProductIds`(1~100)和可选 `deviceId`;逐条返回是否可创建、价格区间、原因和下一步,不创建任务 |
| `POST` | `/api/admin/v1/purchase-tasks/batch` | 批次 `requestId`、`sybProductIds`(1~100)和可选 `deviceId`;每条派生稳定幂等键并独立创建,部分失败不回滚成功项 |
| `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 实现;#67 增加 `shopeeOrderNoSnapshot` 的列表/详情返回和 `shopeeOrderNo` 筛选,空快照返回空字符串并由页面显示“—”,不与 `pddOrderNo` 混用;#44 在 SYB 商品列表提供单条/当前页批量创建入口。页面预检只负责提前解释,`batch` 提交时仍逐条执行现有单任务事务和活动任务、订单、支付、设备能力门禁;返回项包含任务号,或失败原因与下一步建议。只有管理员和采购员可以调用。
创建请求的价格保护使用整数分:`referenceUnitPriceCent`、`minUnitPriceCent`、`maxUnitPriceCent` 和 `currency`。执行时以 PDD App 实际单价校验;低于最小值或高于最大值均返回普通人可理解的价格越界错误,不考虑优惠券,不以订单总价替代单价判断。 #44 批量创建不接受浏览器提交价格:服务端从 PDD 颜色规格价格生成 CNY 快照,已映射颜色按该颜色的 0.2~1.5 倍,待规格探测时按全部可用颜色最低价的 0.2 倍到最高价的 1.5 倍;没有可用颜色价格时逐条拒绝。SYB/Shopee 的 TWD 售价不参与 PDD 采购价格保护。
### 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`;服务端先使用已确认人工映射,否则对实时/档案可选规格做繁简、空白/全半角/大小写及公斤/斤的唯一确定性匹配,仍无唯一结果才调用 AI。第二趟只会收到服务端已固化的精确 PDD 原始标签;Agent 只在已打开的规格面板内做有限纵向滑动,每次重新读取节点并按完整文字精确点击,连续没有新证据或达到上限即停止。若第二趟仍提交 `spec_probe_completed`,服务端将任务和当前 attempt 明确标记失败、释放租约并保留第一次规格决策,不再进入 `spec_probe_pending` 或再次派发。无匹配、候选不完整、歧义或 Provider 异常同样使任务失败。`order_result_unknown` 只允许管理员或采购员人工解除,永不自动重派。
`openSpecPanel.textAliases` 是可选的候选过滤条件,不是原始页面文本选择器。省略该字段时,Agent 使用语义安全的规格入口或底部购买入口;提供时也只能与这些安全候选取交集,匹配不到即返回 `RULE_NOT_MATCHED`。
Android #42/#36 使用本地 SQLite 保存恢复与重传所需的任务、attempt 和 Outbox;最终结果和 Outbox 在同一事务写入。演练执行器仍在安全点停止。Agent 0.2.0 另外声明 `purchase.live.v1`、`purchase.address-update.v1` 和 `purchase.order-create.v1`,仅在 `live` 规则同时包含受限 `updateShippingAddress`、`createOrder`、`readOrderResult` 动作时启用。
正式地址动作在已经选好规格、数量的当前规格/下单面板内执行,不额外点击进入确认页。若脱敏手机号地址入口被裁切,Agent 只在唯一、可见且占据主要屏幕宽度的纵向面板容器内执行有限向下拉动,每次重新读取页面;不得退化为选择页面最大滚动区域。地址入口通过唯一脱敏手机号文字节点确认并在每次动作前重新定位;即使文字节点不可点击,也只允许在该文字节点中心执行一次精确手势,禁止向上查找并点击可能覆盖整个下单面板的宽泛可点击祖先。近乎完全重叠无障碍重复节点合并为一个目标,多个独立目标、多个候选面板、滚动无进展或页面切换超时均以分阶段错误结束并禁止创建订单。入口出现后,Agent 精确选择唯一修改按钮和详细地址输入框;“修改”“保存”和“提交订单”等文字节点即使要沿父链执行点击,也始终保留原始文字、类名和位置作为新鲜定位锚点,可点击父节点只用于验证和最终动作,禁止用无文字父节点替换锚点。按首个 `-` 或 `_` 截断旧后缀,追加任务 `addressSuffix`,保存并回读完整新地址。地址全文只在本次执行内存中存在,不进入本地最终确认快照、日志或服务端。
地址编辑页存在多个输入框时,以“详细地址”标签为结构锚点,只接受与标签纵向相交且位于标签右侧的唯一、可见、启用、非空输入框;不能要求页面只有一个输入框,也不能按输入框顺序猜测。
创建订单前先在本地事务保存 `order_submit_started`、不可逆时间、稳定的 `orderSubmitRequestId` 和脱敏最终确认快照,再调用服务端同名接口;只有两侧标记完成并重新校验商品、规格、数量、单价、地址后缀、PDD 包/Activity 与唯一创建订单按钮后,才点击一次。重启时重放同一标记请求并只读核单;无法取得唯一未付款订单号和 PDD 下单时间时提交 `order_result_unknown`。支付文字仅用于识别未付款/离开支付页,永不点击。服务端数据库仍是最终事实来源;双方均不保存原始控件树、截图、PDD 凭据或完整收货地址。
创建订单后若出现 Android 多微信应用选择器,只读核单器必须同时确认前台包为 `android` / `com.android.intentresolver`、Activity 为白名单 `ChooserActivity` / `ResolverActivity`、页面出现已知系统选择器标题且至少一个候选以“微信”开头,才允许执行一次系统返回;不得点击任何微信候选。随后若前台为 PDD `com.xunmeng.pinduoduo.app_pay.core.PayActivity` 或当前页面出现支付动作文字,最多再返回一次。返回后只读解析唯一订单号和下单时间;选择器或支付页重复出现、白名单不成立、返回失败、结果不唯一或超时均返回未知结果,禁止再次点击创建订单、取消订单或支付。
| 错误码 | 普通提示 |
|---|---|
| `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` | 系统禁止自动付款 |
## Agent 当前设备任务历史(#90)
四个只读接口统一使用设备注册所得的 Device Token,只返回该 Token 对应设备最近 30 天内的任务;设备 A 查询设备 B 的任务时按不存在处理,不泄露任务是否存在。
```http
GET /api/agent/v1/collection-tasks?page=1&pageSize=20&status=failed&taskNo=%2335
GET /api/agent/v1/collection-tasks/{taskId}
GET /api/agent/v1/purchase-tasks?page=1&pageSize=20&status=failed&taskNo=CG-12
GET /api/agent/v1/purchase-tasks/{taskId}
```
- `pageSize` 最大为 50;`status` 与 `taskNo` 可以组合过滤。
- 采集任务编号允许 `35` 或 `#35`,采购任务编号允许 `12` 或不区分大小写的 `CG-12`;服务端按精确编号匹配。
- 列表和详情不返回 Device Token、URL、收货地址、规则快照或原始控件树。采购项额外返回服务端计算的 `retryable` 和可选 `retryDisabledReason`;除受控失败重试外,不提供取消、修改既有订单或支付入口。
- 采集详情返回标题、店铺、销量、评价数、规格维度、颜色价格、SKU、缺失项和结构化错误。
- 采购详情返回蝦皮订单号、PDD 商品、已匹配颜色/尺码、数量、实际单价、PDD 订单号、下单时间和结构化错误。
- Agent 提交采购结果时可携带 `actualUnitPriceCent`(人民币分,非负)。服务端只保存 Agent 实际观察到的值;历史任务或未观察到价格的结果保持 `null`,客户端显示“未记录”。
## Agent 受控重新采集(#91)
```http
POST /api/agent/v1/collection-tasks/{taskId}/reset
Authorization: Bearer <device-token>
Content-Type: application/json
{"requestId":"<uuid>"}
```
- 只允许任务原关联设备使用自身 Device Token 重置 `completed`、`completed_partial` 或 `failed` 采集任务;跨设备统一返回 `TASK_NOT_FOUND`。
- `requestId` 必须为 UUID。同一请求重复提交不会再次清空结果,响应中的 `replayed` 为 `true`。
- 服务端在事务内锁定设备和任务,重新校验设备在线、同商品无其他 `pending/running` 采集任务,并确认该设备没有正在执行或持有有效租约的其他采集/采购任务。
- 成功后保留 URL、goods_id、设备和规则快照,事务删除旧结构化结果及错误,把原任务恢复为 `pending`;Agent 随后只能通过既有 `next → claim → start` 调度执行。
- 响应只返回 `taskId`、`status` 和可选的 `replayed`,不返回规则快照、URL、Token、控件树或截图。
- 设备离线、任务非终态、设备忙或同商品存在活动任务时返回明确冲突,不支持离线排队。
## Agent 受控采购重试(#95)
```text
POST /api/agent/v1/purchase-tasks/{taskId}/retry
Authorization: Bearer <device-token>
Content-Type: application/json
{"requestId":"<uuid>"}
```
- 原任务必须属于当前 Device Token,且在最近 30 天历史范围内;跨设备或超期按任务不存在处理。
- 服务端复用正式采购失败重试校验:状态为 `failed`、未进入不可逆边界、无 PDD 订单号和下单时间、非结果未知、同一 SYB 商品无更新任务,当前设备在线/空闲/能力满足,并重新校验档案、规格映射、价格保护和采购规则。
- 成功响应 `data` 返回 `sourceTaskId`、`sourceTaskNo`、新 `taskId`、新 `taskNo` 和 `replayed`。旧任务不清空;新任务为固定当前设备的 `pending`,并使用新地址后缀。
- `requestId` 必须是 UUID;相同请求重放返回同一新任务,不重复创建。请求体不得指定设备、规格、规则、地址、订单或支付参数。
- Android 必须使用服务端 `retryable` 决定是否展示入口,提交前明确提示可能产生新的待付款订单且系统不会支付。真机调用属于可能创建订单的高风险验证,必须先取得人工授权。
## Agent 任务记录范围与同步(#99)
采集、采购任务列表接口新增可选查询参数 `days`:
```http
GET /api/agent/v1/collection-tasks?page=1&pageSize=20&days=7&status=failed&taskNo=%2335
GET /api/agent/v1/purchase-tasks?page=1&pageSize=20&days=7&status=failed&taskNo=CG-12
```
- `days` 只允许 1–30 的正整数;省略时为 30,保持旧客户端兼容。详情接口仍限制最近 30 天。
- 记录范围、状态、任务编号和分页只作用于当前 Device Token 对应设备,不能查询其他设备。
- Agent 采集/采购 Tab 的顶部下拉只重新读取当前 Tab、当前筛选、当前编号和当前页;不领取、执行、重置或重试任务。
- 设置 Tab 可输入最近 1~30 天的任意整数,默认 7 天;空值、非整数或越界时不发起请求并提示“请输入 1~30 天”。一次同步当前设备的采集与采购列表摘要;同步为服务端到 Agent 的单向读取,允许分别成功并显示部分同步结果。
- 本地只缓存列表摘要和同步时间;详情仍按需请求。缓存不得包含 Device Token、完整规则快照、PDD URL、地址、控件树或截图。同步本身不打开 PDD、不修改地址、不创建订单、不支付。
## Agent 当前页面临时采集(#101)
### 管理端默认规则
```http
GET /api/admin/v1/collection-rules/agent-manual-setting
PUT /api/admin/v1/collection-rules/agent-manual-setting
Content-Type: application/json
{"requestId":"<uuid>","ruleId":3}
```
- 只允许管理员读取和修改“Agent 手动采集默认规则”;PUT 的 `requestId` 必须为 UUID,重复请求幂等。
- 规则必须是仍存在的 PDD 商品详情 v2 规则。数据库迁移首次执行时,优先选择最近成功/部分成功任务使用的存活规则;没有成功历史时选择最近更新的存活 v2 规则;仍没有时保持未配置。
- 创建任务时重新验证规则与设备能力,并把完整规则快照固化到任务;以后修改默认规则不影响已创建任务。
### 创建并占用当前设备
```http
POST /api/agent/v1/current-page-collection-tasks
Authorization: Bearer <device-token>
Content-Type: application/json
{"requestId":"<uuid>"}
```
- Device Token 确定设备。服务端在事务中锁定设备,校验在线、空闲、无活动采集/采购任务、默认规则和 `collector.pdd.current-page-share.v1` 能力。
- 成功即创建来源为 `agent_current_page` 的标准采集任务,固定当前设备、状态为 `running`、建立租约并返回规则快照;不再调用普通 `next → claim → start`。
- 同一设备与 `requestId` 重放返回原任务且 `replayed=true`,不创建第二条任务、不续租。
- 身份识别前响应中的 `pddProductId` 为 `null`,`urlSnapshot` 与 `goodsIdSnapshot` 为空字符串;普通 `admin` 来源任务仍必须在创建时具备完整商品身份。
### 识别当前商品
```http
POST /api/agent/v1/current-page-collection-tasks/{taskId}/identify
Authorization: Bearer <device-token>
Content-Type: application/json
{"requestId":"<uuid>","shareUrl":"https://p.pinduoduo.com/..."}
```
- Android 只上传从本次新鲜剪贴板内容中唯一提取出的分享 URL,不上传完整分享文案。
- Agent 从剪贴板文本提取唯一白名单链接:含 goods_id 的链接直接提交;白名单内但 URL 本身无 goods_id 的链接(包括 `p.pinduoduo.com` 短链和 `mobile.yangkeduo.com/goods2.html?ps=...`)优先在手机侧用无 Cookie、无项目凭据的移动端 GET 展开,最多跟随 4 次跳转、每跳校验 HTTPS/主机/端口/userinfo,连接与读取各超时 5 秒,正文最多读取 64KB。URL 或正文只接受边界明确的 `goods_id`,不得把 `refer_goods_id` 等相近字段当作商品身份。展开失败可提交原链接,由服务端以相同白名单、最多 4 次重定向和 10 秒总超时兜底;请求字段仍只传 `shareUrl`。
- 服务端提取 5~32 位纯数字 `goods_id`,形成标准 URL,并在事务中创建或复用唯一 PDD 商品;同商品已有活动采集任务或身份冲突时拒绝。
- 相同识别 `requestId` 直接返回已确认身份且不再次访问短链;任务首次确认身份后,不允许不同请求覆盖为其他商品。
- 识别后继续复用 `POST /api/agent/v1/tasks/{taskId}/result` 与 `/fail`。结果接口要求任务已绑定身份且结果 goods_id 一致;完成、部分完成和失败继续按既有状态机释放设备槽。
- Agent 历史的任务摘要增加 `source`;`agent_current_page` 展示为“Agent 当前页面”,不新增任务状态。该来源任务不支持重置,失败后由用户从当前商品页重新发起新任务。