feat: 实现受控真实下单安全边界 (#99)

This commit is contained in:
chengma
2026-08-10 16:35:38 +08:00
parent 7106f6b024
commit e960cab1fb
30 changed files with 1562 additions and 66 deletions
+18 -12
View File
@@ -344,19 +344,21 @@ Client 与 Admin 的任务交互只有三种调用:领一个任务、提交结
### 任务领取与执行
1. 优先补交一条本地 Outbox。提交结果不依赖 Android 设备。
2. 没有待提交结果时,在工作线程通过 ADB 检查已保存设备号是否仍为 `device` 状态。检查失败立即停止,不执行本地任务,也不领取新任务。
3. 执行最早的本地待处理任务;没有本地任务时才调用 Admin `claim`。返回 204 表示暂时没活,按轮询周期退避后再试。
4. Client 持久化新领取的任务,状态置 `claimed`。
5. 根据 `task_type` 分派给采集或采购执行器。
6. 工作线程执行,持续更新 `current_step`(只写本地,不上报 Admin)。
7. 完成后在同一事务里写入结果与 Outbox,状态置 `result_pending`。
8. Outbox 提交成功、Admin 返回 `accepted: true` 后标记 `succeeded`。
9. 回到第 1 步处理下一轮。**同一时间只做一个任务。**
1. 若存在 `irreversible_action_at`,只允许先核对该采购订单;不得补交其他结果、执行任务或领取任务。
2. 没有待核对采购时,优先补交一条本地 Outbox;提交结果不依赖 Android 设备。
3. 没有待提交结果时,在工作线程通过 ADB 检查已保存设备号是否仍为 `device` 状态。检查失败立即停止,不执行本地任务,也不领取新任务。
4. 执行最早的本地待处理任务;没有本地任务时才调用 Admin `claim`。返回 204 表示暂时没活,按轮询周期退避后再试。
5. Client 持久化新领取的任务,状态置 `claimed`。
6. 根据 `task_type` 分派给采集或采购执行器。
7. 工作线程执行,持续更新 `current_step`(只写本地,不上报 Admin)。
8. 完成后在同一事务里写入结果与 Outbox,状态置 `result_pending`。
9. Outbox 提交成功、Admin 返回 `accepted: true` 后标记 `succeeded`,然后继续下一轮。**同一时间只做一个任务。**
`TaskDispatcher` 是能力声明的唯一入口。没有可用采购演练 Adapter、没有已保存
`TaskDispatcher` 是能力声明的唯一入口。没有可用采购 Adapter、没有已保存
Android 设备或本地持久化未准备好时,只声明 `collect`;条件满足时才声明
`collect,purchase`,且 `purchase_mode` 永远是 `dry_run`。领取响应必须先完整校验并
`collect,purchase`。真实下单授权默认关闭;只有本地授权同时匹配当前 Client ID、
已保存 Android 设备且 live Adapter 就绪时才声明 `purchase_mode=live`,否则声明
`dry_run`。领取响应必须先完整校验并
写入 SQLite,Repository 提交成功后才能分派,避免任务已在 Admin 领取却在本地丢失。
中途 Admin 是否取消了这个任务、是否重派给了别人,Client 不查也不管,做完照样提交——
@@ -385,11 +387,15 @@ reconcile_purchase(task, run) -> PurchaseResult | ManualReview
采购演练通过 `PddPurchaseAdapter` 的窄接口逐步读取最新页面状态。该接口只提供
打开商品、读取状态、精确选择动态规格、设置数量、进入提交前确认页和停止,
**不提供提交订单或付款方法**。这样即使应用层调用错误,也没有可误触的真实下单入口。
真实采购另用 `PddLivePurchaseAdapter`,只增加 `submit_order_once`。服务层在最新
页面复核规格、数量、价格、库存和唯一提交目标后,先提交
`irreversible_action_at` 事务,再允许 Adapter 点击一次;之后无论点击结果是否明确,
都只进入订单核对。该接口不提供付款或取消订单方法。
采购规格使用完整 `options` 对象精确比较,不假定只有颜色和尺码两个维度。
只读核对另用 `PddPurchaseReconcileAdapter`,只暴露 `read_order_match`
和 `close`,不暴露选规格、设数量、下单或付款方法。
正式 Client 由 `pdd_u2_purchase_adapter.py` 实现上述演练接口,并在
正式 Client 由 `pdd_u2_purchase_adapter.py` 分别实现演练和 live 接口,并在
`ui_main.py` 注入工厂。Adapter 通过 `PddDeviceService` 独占连接,每次判断
都重新读取当前包名和控件树。当前 Admin 下发的 `color` 和 `size`
使用精确文字匹配;其他动态维度直接停止,不做相似匹配。原生 PDD
+8
View File
@@ -125,6 +125,8 @@ CREATE TABLE pdd_tasks (
remote_task_id TEXT NOT NULL UNIQUE,
task_type TEXT NOT NULL
CHECK (task_type IN ('collect', 'purchase')),
execution_mode TEXT NOT NULL DEFAULT 'dry_run'
CHECK (execution_mode IN ('dry_run', 'live')),
goods_id TEXT,
goods_url TEXT NOT NULL,
title TEXT,
@@ -167,6 +169,7 @@ CREATE INDEX idx_pdd_tasks_goods_id
### 字段语义
- `remote_task_id`:跨 Admin、Client、日志、提交和归档使用的稳定任务编号。Mock 任务也必须分配稳定编号。
- `execution_mode`:Admin 领取时确定的不可变执行模式;历史记录默认为 `dry_run`,Client 不得把任务自行提升为 `live`。
- `price_cent`:列表摘要价格;采集任务通常为最低可用 SKU 价格,详细价格以 `pdd_data.skus` 为准。
- `status`:**本机执行状态**,只由本机的执行流程和 Outbox 提交响应驱动。Admin 那边把任务标成什么,本地不知道也不需要知道(见 §3.1)。
- `current_step`:当前安全步骤,例如 `open_goods`、`collect_skus`、`select_options`、`placing_order`、`reconcile_order`。
@@ -407,12 +410,17 @@ CREATE TABLE app_settings (
- `automation.poll_interval_seconds`
- `automation.max_retries`
- `automation.dry_run`
- `purchase.live_enabled`
- `purchase.live_client_id`
- `purchase.live_device_serial`
- `purchase.live_confirmed_at`
- `safety.max_quantity`
- `safety.price_tolerance_cent`
- `diagnostics.artifact_directory`
- `diagnostics.retention_days`
访问令牌、密码和 Cookie 不得存入本表。
`purchase.live_*` 只保存非敏感的本地安全门禁状态;缺失、损坏或绑定不一致时一律按关闭处理。
## 7. 任务状态转换
+4 -3
View File
@@ -210,9 +210,10 @@ POST /api/v1/client/tasks/claim
无可领取任务时返回 `204 No Content`。
Client 的 `HttpAdminGateway.claim_next` 已实现本接口,并原样序列化应用层已经
安全确认的 `ClaimCapabilities`。没有采购演练 Adapter 或设备未准备好时只声明
`supported_types: ["collect"]`;条件满足时声明 `collect,purchase`,但
`purchase_mode` 固定为 `dry_run`。一次调用最多领取一个任务,结果或失败通过
安全确认的 `ClaimCapabilities`。没有采购 Adapter 或设备未准备好时只声明
`supported_types: ["collect"]`;条件满足时声明 `collect,purchase`。只有设置页
授权同时匹配当前 Client ID、已保存 Android 设备且 live Adapter 就绪时才声明
`purchase_mode: "live"`,其他情况均为 `dry_run`。一次调用最多领取一个任务,结果或失败通过
本页 §6 或 §7 提交,完整请求会先进入本地 Outbox。
采购响应在写入本地前必须校验非空 `goods_url`、`goods_id`、动态 `options`、
+10 -2
View File
@@ -64,8 +64,8 @@
才向 Admin 领取一条。领取、手机采集和提交都在单独的工作线程完成,一轮结束且
线程完全退出后,由主线程的单次定时器安排下一轮。暂无任务时默认 5 秒后重试;
可恢复的 Admin 错误按 5、10、20、30 秒退避。需要人工处理、不可恢复错误、设备
或配置错误会停止自动获取。采购演练执行器和 Android 设备都准备好时,可以领取
采购任务;界面必须明确显示“演练”,任何路径都不得显示成真实下单。
或配置错误会停止自动获取。采购 Adapter 和 Android 设备都准备好时,可以领取
采购任务;任务详情必须按任务的不可变执行模式明确显示“演练”或“真实下单”。
- 补交本地 Outbox 不依赖手机,可以在 Android 设备断开时继续执行。
- 执行本地任务或向 Admin 领取新任务前,工作线程必须用 `adb devices -l` 检查已保存的精确设备号。USB 和 Wi-Fi 设备都只接受 `device` 状态。
@@ -288,6 +288,14 @@ class TaskTableModel(QAbstractTableModel):
- 最大重试次数;
- 演练模式开关。
### 真实下单(不支付)
- 默认关闭,并明确显示当前状态、绑定 Client、绑定 Android 设备和确认时间;
- 只有输入完整确认文字“创建未付款订单”后才能启用,不能通过环境变量或调试参数开启;
- 启用和关闭在后台线程保存,执行期间禁用重复操作,失败时保留原状态并说明恢复办法;
- 启用仅代表允许创建一笔未付款订单,不允许自动付款;设备或 Client 绑定变化后自动降级为演练;
- 关闭操作始终可见,状态不能只用颜色表达。
### 安全与诊断
- 最大购买数量;
+11 -8
View File
@@ -81,10 +81,11 @@ PDD 解析测试优先使用脱敏的 XML 固件,不要求每次连接真实
7. 演练模式在代表性商品和设备上通过验收。
8. 操作人员明确确认真实下单范围和安全设置。
真实下单不得通过调试参数、默认配置或界面误操作意外开启。
当前版本的 `ClaimCapabilities` 仅接受 `purchase_mode=dry_run`,采购
Adapter 不提供提交订单或付款方法。开发者不能把自动化测试通过当成真实
下单门禁放行;仍需项目负责人和实际操作人员共同确认。
真实下单不得通过调试参数、默认配置或界面误操作意外开启。当前版本仅在设置页
精确输入确认文字后保存授权,并绑定 Client ID 和已保存 Android 设备;任一绑定
不一致都降级为 `purchase_mode=dry_run`。演练 Adapter 不提供提交方法,live Adapter
只提供一次性提交方法,两者都不提供付款或取消订单方法。开发者不能把自动化测试
通过当成真机门禁放行;仍需项目负责人和实际操作人员共同确认。
### 3.1 当前自动化安全检查
@@ -93,12 +94,14 @@ Adapter 不提供提交订单或付款方法。开发者不能把自动化测试
- 有不可逆标记的中断只调用只读核对,采购 Adapter 调用次数为 0。
- 已落库 Outbox 只补交,不重跑手机流程。
- 停止、设备断开、验证码、登录失效和结果不确定都保留稳定错误和诊断。
- `purchase_mode=live` 会在数据对象创建时被拒绝。
- `execution_mode=live` 只允许采购任务,且 Client 不得自行升级 Admin 下发的模式。
- 最终点击前重新读取页面并核对规格、数量、价格、库存和唯一提交目标。
- `irreversible_action_at` 必须在点击前以事务提交;点击最多一次,返回或抛错后均只进入核单。
- 正式 uiautomator2 采购 Adapter 只点击商品页采购入口和目标规格,
`enter_confirmation` 与 `stop_before_submit` 均为只读检查。
- 单元测试必须断言到达最终确认页后没有产生第二次底部按钮点击。
演练的 `enter_confirmation` 与 `stop_before_submit` 均为只读检查。
- 单元测试必须断言演练没有最终点击、live 只有一次最终点击,且模糊目标不会点击。
上述只证明演练和恢复代码的默认安全性,**不代表真实下单已放行**。
上述只证明本地安全边界;真实设备联调仍须按独立验收工单获得当次明确授权。
## 4. 自动化防护