skus_json
#30 做完后 Client 能领到采集任务,但领了不会做任何事—— 任务会一直停在本地,Admin 那边永远等不到结果。
本工单把这条链路接通:领到任务 → 派给安卓设备 → 打开商品页 → 采集标题、店铺名、所有颜色、所有尺码、各颜色价格 → 提交给 Admin。
client/AGENTS.md:「『获取任务』会真的去操作手机、可能下单」。 在 #30 之前这个按钮只是发 HTTP,本工单之后它会真的驱动设备。
client/AGENTS.md
[必须] 本工单只做采集。采集是只读操作——打开商品页、点规格、读文字, 不加购、不下单、不付款。真实下单开关保持关闭。
[必须]
样本:client/image_xml/737116531267_home.xml 和 _size.xml。
client/image_xml/737116531267_home.xml
_size.xml
整个规格面板只有一处价格(折后¥4.7 和划线价 ¥19.9), 8 个颜色选项旁边没有各自的价格。
折后¥4.7
¥19.9
→ 必须逐个点击颜色才能拿到各自价格。
【店铺热销】冰丝吊带裙女夏季薄款内搭连 衣裙宽松a字裙中长款打底裙衬裙纯色
→ 必须按位置拼接,取单个节点会得到半截标题。
浅蓝色中长款 的 bounds 是 [1044,1561][1080,1669]——宽度只有 36px, 被右边缘切掉。
浅蓝色中长款
[1044,1561][1080,1669]
→ 直接读 text 会拿到不完整的名字,进而算出错的 OptionKey, 规格映射会静默失效或匹配到错的规格。
text
OptionKey
已选: 黑色中长款 2XL(140-160斤) 证明有尺码维度, 但 8 个颜色已占满屏幕,尺码要滚动才可见。
已选: 黑色中长款 2XL(140-160斤)
做:
pdd_data
不做(各自独立工单):
settings_ui*.py
android_device_service.py
已定案:颜色逐个点击读价(约 8 次),尺码只读列表不点击。
但 skus[] 必须输出颜色×尺码的完整组合,不能只输出 8 个颜色。
skus[]
[必须] 理由:后续「蝦皮 SKU ↔ PDD 规格」匹配是按完整组合做的 (Admin 的 OptionKey() 对整个 options 求值)。只输出颜色的话, 蝦皮那个带尺码的 SKU 永远匹配不上,规格匹配功能直接废掉。
OptionKey()
options
颜色: [黑色中长款, 白色中长款, ...] ← 逐个点击,各读一次价格 尺码: [M, L, XL, 2XL, ...] ← 只读文字,不点击 skus[] = 笛卡尔积,每个组合的 price_cent 用它所属颜色的价格
[必须] 每条 SKU 带 price_observed_at,记录读到这个价格时实际选中的组合。 同颜色下只有一条是实测的,其余是推断的。
price_observed_at
[必须] 顶层 price_granularity: "color"。
price_granularity: "color"
契约详见 #31。
[必须] 不得直接用被截断节点的 text。
按下面顺序取,取到即止:
content-desc
error.code = SKU_NAME_TRUNCATED
[必须] 第 3 条是失败,不是"尽力而为"。名字不全会算出错的 OptionKey, 规格映射静默失效——或者更糟,撞上另一个规格,将来按它下单就是买错东西。 宁可这次采不到,也不能存一份看起来正常、实际是错的数据。
[必须] 按 bounds 的 y 坐标排序后拼接同一区域的多个文本节点。
[必须] 拼完要有长度下限校验(比如 < 6 个字视为没采到,整单失败)。 半截标题存进去,操作员核对「是不是我要的商品」时会被误导。
已定案:并入本工单,采不到留空。
[必须] 但采不到时必须在报告里给出证据——说明在哪一屏找过、 用什么定位方式找的,并导出一份该屏的 XML 到 client/image_xml/。
client/image_xml/
理由:「尽力而为」没法客观验收,事后分不清是真的拿不到还是没写好。 有 XML 就能判断。
[建议] 已知样本里主页没有店铺名,只有底部导航的「店铺」按钮 (bounds [54,2258][132,2311])。可能需要向下滚动。
[建议]
[54,2258][132,2311]
[必须] 根 AGENTS.md 红线。整个采集流程放 QObject + moveToThread 的 Worker,模板见 02 架构 §5.1。 不得用 QThread 子类、QRunnable、threading。
AGENTS.md
QObject
moveToThread
QThread
QRunnable
threading
[必须] 一个设备同一时间只由一个工作线程控制,不跨线程共享 uiautomator2 Device。
[必须] 窗口关闭时按 §5.2 断开连接。
[必须] 必须处理:启动、运行、安全停止、成功、失败、取消、超时、 设备断开、迟到结果(client/AGENTS.md 明确列了这 9 种)。
[必须] 顺序:采集完成 → 写本地 SQLite → 通过 Outbox 提交 Admin。 不得采完直接发 HTTP。
[必须] 提交带 Idempotency-Key,重试只重发、不重采。 重采会在拼多多上多点几十次,而且两次结果可能不一致。
Idempotency-Key
[必须] 整单采集超时上限设为 10 分钟,超了主动失败并提交 submit_failure。
submit_failure
理由:Admin 侧 #24 规定 collecting 超过 15 分钟就允许别人重新建任务。 Client 这边如果拖到 15 分钟以上,会出现「Admin 已经放行、又建了一个新任务, 而这台还在采」的重叠局面。留 5 分钟余量。
collecting
[必须] 失败时提交 submit_failure,error.code 用可枚举的常量 (如 SKU_NAME_TRUNCATED / SKU_PANEL_NOT_FOUND / TITLE_TOO_SHORT / DEVICE_DISCONNECTED / COLLECT_TIMEOUT),不要只发一句自由文本。
error.code
SKU_NAME_TRUNCATED
SKU_PANEL_NOT_FOUND
TITLE_TOO_SHORT
DEVICE_DISCONNECTED
COLLECT_TIMEOUT
[必须] diagnostics 里带本地 XML/截图的路径引用, 按已定案的 Artifact 策略:只报本地引用、不上传文件 (docs/client/04 §10)。
diagnostics
docs/client/04
client/src/pdd_collect_service.py
client/src/pdd_page_parser.py
client/src/pdd_ui_event.py
client/src/http_admin_gateway.py
submit_result
client/src/task_repository.py
client/test/test_pdd_page_parser.py
client/test/test_pdd_collect_service.py
docs/client/*
解析(用真实 XML 固件,不许自己编)
737116531267_home.xml
737116531267_size.xml
4.7
19.9
price_cent = 470
采集流程
price_granularity = "color"
红线
提交
联调
1. 单元测试(不需要设备)
cd D:\chengma\cmautobuy\client C:/Python310/python.exe -m pytest test/ -q
[必须] 解析相关的测试必须用 client/image_xml/ 里的真实 XML。 自己编一份字段恰好对得上的固件,等于没测——本工单已经证明真实页面 有截断、有拆分节点这些编不出来的情况。
2. 界面离屏冒烟
$env:QT_QPA_PLATFORM="offscreen" C:/Python310/python.exe -c "from src.ui_main import MainWindow; from PyQt5.QtWidgets import QApplication; app=QApplication([]); w=MainWindow(); print('OK'); app.quit()" Remove-Item Env:QT_QPA_PLATFORM
3. 真机联调
[必须] 这一步必须单独标记为真机测试,并说明没有执行任何下单操作。
起 Admin → PDD 页建商品建采集任务 → Client 连设备 → 点「获取任务」。
期望并贴出证据:
/tasks
/pdd
price_granularity
回退:git revert。采集是只读操作,最坏情况是 Admin 上几条任务失败, 重新建一个即可。不会产生订单。
git revert
补充执行与恢复要求(合并重复工单 #33 的有效内容):
claimed
result_pending
sending
pending
succeeded
以上作为 #32 的验收补充,不扩大到采购、下单或付款。
实现进度(2026-08-07):已完成单条采集主链代码与自动化测试。当前包含:本地 claimed/retry_wait 优先、Outbox 优先补交、QObject 工作线程、HTTP result/failure 幂等提交、采集结果与 Outbox 同事务、Admin 超时只重发不重采、10 分钟总超时、真实 XML 的标题拼接/8 色/实付价与划线价测试、颜色逐个采价且尺码只读、截断规格名保护。尚在执行全量回归与真机只读联调;没有加入任何加购、下单或付款代码。
实现已提交,等待验收(工单保持 open)。\n\n- 功能提交:2d1fdbe eat: 执行并提交 PDD 采集任务 (#32)\n- 归档提交:51c2292 docs: 归档任务 #32\n- Client:143 项测试通过,离屏 UI 冒烟通过\n- Admin:go test ./... 通过\n- 真实 XML:完整标题、8 个颜色、470 分实付价、1990 分划线价均通过\n- 真机只读验证:USB 设备在进入规格面板前从 ADB 消失,触发 device not found;没有执行任何加购、下单或付款。已补充 DEVICE_DISCONNECTED 稳定分类。\n\n未完成的验收项:稳定真机上的全部颜色/尺码扫描,以及真实 Admin 接收后页面状态与规格表核对。详细记录见 docs/task/32-client-执行采集任务并提交-admin.md。
2d1fdbe
docs/task/32-client-执行采集任务并提交-admin.md
4022fee
架构角色复核要点:
skus 是颜色×尺码的笛卡尔积,每条带 price_observed_at。这是本工单最要紧的一条——只输出颜色的话,后续「蝦皮 SKU ↔ PDD 规格」匹配(按完整组合求 OptionKey)会直接废掉。实现独立得出了与工单一致的结论。
skus
字段名(price_observed_at / list_price_cent / shop_name)与 Admin 侧 #31 完全对得上。
list_price_cent
shop_name
本工单交付后的真机问题分别由 #35(页面误判 + 失败回执)、#36(进入规格面板)、#37(蛇形逐色采价)、#40(滚动采店铺名和评价数)、#42(允许颜色缺价)、#44(等待时间)处理,均仍在进行中。
No dependencies set.
The note is not visible to the blocked user.
基本信息
skus_json结构)、#24(采集超时 15 分钟)要解决什么
#30 做完后 Client 能领到采集任务,但领了不会做任何事——
任务会一直停在本地,Admin 那边永远等不到结果。
本工单把这条链路接通:领到任务 → 派给安卓设备 → 打开商品页 →
采集标题、店铺名、所有颜色、所有尺码、各颜色价格 → 提交给 Admin。
这是「获取任务」第一次真正操作手机
client/AGENTS.md:「『获取任务』会真的去操作手机、可能下单」。在 #30 之前这个按钮只是发 HTTP,本工单之后它会真的驱动设备。
[必须]本工单只做采集。采集是只读操作——打开商品页、点规格、读文字,不加购、不下单、不付款。真实下单开关保持关闭。
已经确认的事实(读你的 XML 样本得出,不是猜的)
样本:
client/image_xml/737116531267_home.xml和_size.xml。① 价格只有当前选中组合的那一个
整个规格面板只有一处价格(
折后¥4.7和划线价¥19.9),8 个颜色选项旁边没有各自的价格。
→ 必须逐个点击颜色才能拿到各自价格。
② 标题被拆成多个文本节点
→ 必须按位置拼接,取单个节点会得到半截标题。
③ 选项会被屏幕边缘截断
浅蓝色中长款的 bounds 是[1044,1561][1080,1669]——宽度只有 36px,被右边缘切掉。
→ 直接读
text会拿到不完整的名字,进而算出错的OptionKey,规格映射会静默失效或匹配到错的规格。
④ 尺码维度在屏幕外
已选: 黑色中长款 2XL(140-160斤)证明有尺码维度,但 8 个颜色已占满屏幕,尺码要滚动才可见。
⑤ 「已选:」行是判断当前状态的唯一可靠锚点
做什么 / 不做什么
做:
pdd_data,先写本地库再提交不做(各自独立工单):
settings_ui*.py/android_device_service.py(另一会话在改)怎么做
价格按颜色采样,但输出笛卡尔积
已定案:颜色逐个点击读价(约 8 次),尺码只读列表不点击。
但
skus[]必须输出颜色×尺码的完整组合,不能只输出 8 个颜色。[必须]理由:后续「蝦皮 SKU ↔ PDD 规格」匹配是按完整组合做的(Admin 的
OptionKey()对整个options求值)。只输出颜色的话,蝦皮那个带尺码的 SKU 永远匹配不上,规格匹配功能直接废掉。
[必须]每条 SKU 带price_observed_at,记录读到这个价格时实际选中的组合。同颜色下只有一条是实测的,其余是推断的。
[必须]顶层price_granularity: "color"。契约详见 #31。
选项名被截断怎么办
[必须]不得直接用被截断节点的text。按下面顺序取,取到即止:
content-desc(无障碍文本,通常是完整的)texterror.code = SKU_NAME_TRUNCATED[必须]第 3 条是失败,不是"尽力而为"。名字不全会算出错的OptionKey,规格映射静默失效——或者更糟,撞上另一个规格,将来按它下单就是买错东西。
宁可这次采不到,也不能存一份看起来正常、实际是错的数据。
标题拼接
[必须]按 bounds 的 y 坐标排序后拼接同一区域的多个文本节点。[必须]拼完要有长度下限校验(比如 < 6 个字视为没采到,整单失败)。半截标题存进去,操作员核对「是不是我要的商品」时会被误导。
店铺名:采不到必须给证据
已定案:并入本工单,采不到留空。
[必须]但采不到时必须在报告里给出证据——说明在哪一屏找过、用什么定位方式找的,并导出一份该屏的 XML 到
client/image_xml/。理由:「尽力而为」没法客观验收,事后分不清是真的拿不到还是没写好。
有 XML 就能判断。
[建议]已知样本里主页没有店铺名,只有底部导航的「店铺」按钮(bounds
[54,2258][132,2311])。可能需要向下滚动。不得在主线程做任何这些事
[必须]根AGENTS.md红线。整个采集流程放QObject+moveToThread的Worker,模板见 02 架构 §5.1。
不得用
QThread子类、QRunnable、threading。[必须]一个设备同一时间只由一个工作线程控制,不跨线程共享 uiautomator2 Device。[必须]窗口关闭时按 §5.2 断开连接。[必须]必须处理:启动、运行、安全停止、成功、失败、取消、超时、设备断开、迟到结果(
client/AGENTS.md明确列了这 9 种)。先写本地库,再提交(Outbox)
[必须]顺序:采集完成 → 写本地 SQLite → 通过 Outbox 提交 Admin。不得采完直接发 HTTP。
[必须]提交带Idempotency-Key,重试只重发、不重采。重采会在拼多多上多点几十次,而且两次结果可能不一致。
超时要卡在 15 分钟以内
[必须]整单采集超时上限设为 10 分钟,超了主动失败并提交submit_failure。理由:Admin 侧 #24 规定
collecting超过 15 分钟就允许别人重新建任务。Client 这边如果拖到 15 分钟以上,会出现「Admin 已经放行、又建了一个新任务,
而这台还在采」的重叠局面。留 5 分钟余量。
失败要能定位
[必须]失败时提交submit_failure,error.code用可枚举的常量(如
SKU_NAME_TRUNCATED/SKU_PANEL_NOT_FOUND/TITLE_TOO_SHORT/DEVICE_DISCONNECTED/COLLECT_TIMEOUT),不要只发一句自由文本。[必须]diagnostics里带本地 XML/截图的路径引用,按已定案的 Artifact 策略:只报本地引用、不上传文件
(
docs/client/04§10)。预计修改文件
client/src/pdd_collect_service.pyclient/src/pdd_page_parser.pyclient/src/pdd_ui_event.pyclient/src/http_admin_gateway.pysubmit_result/submit_failureclient/src/task_repository.pyclient/test/test_pdd_page_parser.pyclient/image_xml/的真实 XML 做固件client/test/test_pdd_collect_service.pydocs/client/*验收标准
解析(用真实 XML 固件,不许自己编)
737116531267_home.xml能拼出完整标题,不是半截737116531267_size.xml能枚举出全部 8 个颜色浅蓝色中长款能取到完整名字,取不到则整单失败4.7和划线价19.9,price_cent = 470采集流程
skus[]是颜色×尺码的完整组合,不是只有颜色price_observed_at,同颜色下只有一条与之相同price_granularity = "color"submit_failure红线
QObject+moveToThread提交
Idempotency-Key,重试只重发不重采error.code,不是自由文本diagnostics只带本地引用,不上传文件联调
怎么验证
1. 单元测试(不需要设备)
[必须]解析相关的测试必须用client/image_xml/里的真实 XML。自己编一份字段恰好对得上的固件,等于没测——本工单已经证明真实页面
有截断、有拆分节点这些编不出来的情况。
2. 界面离屏冒烟
3. 真机联调
[必须]这一步必须单独标记为真机测试,并说明没有执行任何下单操作。起 Admin → PDD 页建商品建采集任务 → Client 连设备 → 点「获取任务」。
期望并贴出证据:
/tasks该任务:待分配 → 已领取 → 成功/pdd该商品:采集中 → 已采集,规格数 = 颜色数 × 尺码数风险和回退
price_granularity和price_observed_at标注,下单时价格保护据此判断回退:
git revert。采集是只读操作,最坏情况是 Admin 上几条任务失败,重新建一个即可。不会产生订单。
补充执行与恢复要求(合并重复工单 #33 的有效内容):
claimed采集任务;存在则直接执行,不再向 Admin 领取。这样当前已经领取的任务不会被遗留。pdd_data、执行记录和 Outbox 事件必须在同一个 SQLite 事务中落库。result_pending和 Outbox,不得重新采集;恢复后只重发同一事件并复用持久化的Idempotency-Key。sending恢复为pending,并允许继续处理claimed/result_pending。succeeded。以上作为 #32 的验收补充,不扩大到采购、下单或付款。
实现进度(2026-08-07):已完成单条采集主链代码与自动化测试。当前包含:本地 claimed/retry_wait 优先、Outbox 优先补交、QObject 工作线程、HTTP result/failure 幂等提交、采集结果与 Outbox 同事务、Admin 超时只重发不重采、10 分钟总超时、真实 XML 的标题拼接/8 色/实付价与划线价测试、颜色逐个采价且尺码只读、截断规格名保护。尚在执行全量回归与真机只读联调;没有加入任何加购、下单或付款代码。
实现已提交,等待验收(工单保持 open)。\n\n- 功能提交:2d1fdbe eat: 执行并提交 PDD 采集任务 (#32)\n- 归档提交:51c2292 docs: 归档任务 #32\n- Client:143 项测试通过,离屏 UI 冒烟通过\n- Admin:go test ./... 通过\n- 真实 XML:完整标题、8 个颜色、470 分实付价、1990 分划线价均通过\n- 真机只读验证:USB 设备在进入规格面板前从 ADB 消失,触发 device not found;没有执行任何加购、下单或付款。已补充 DEVICE_DISCONNECTED 稳定分类。\n\n未完成的验收项:稳定真机上的全部颜色/尺码扫描,以及真实 Admin 接收后页面状态与规格表核对。详细记录见 docs/task/32-client-执行采集任务并提交-admin.md。
验收通过,关闭
2d1fdbefeat: 执行并提交 PDD 采集任务 (#32)docs/task/32-client-执行采集任务并提交-admin.md(状态已更新,4022fee)架构角色复核要点:
skus是颜色×尺码的笛卡尔积,每条带price_observed_at。这是本工单最要紧的一条——只输出颜色的话,后续「蝦皮 SKU ↔ PDD 规格」匹配(按完整组合求OptionKey)会直接废掉。实现独立得出了与工单一致的结论。字段名(
price_observed_at/list_price_cent/shop_name)与 Admin 侧 #31 完全对得上。后续修复已另立工单
本工单交付后的真机问题分别由 #35(页面误判 + 失败回执)、#36(进入规格面板)、#37(蛇形逐色采价)、#40(滚动采店铺名和评价数)、#42(允许颜色缺价)、#44(等待时间)处理,均仍在进行中。
遗留