2026-09-03 用户报告:本地服务已安装 #208 的 Debug APK(16:30 完成安装)后,采购任务 CG63 于 16:34 重试两次仍然失败,错误仍为「没有找到安全的商品规格入口」。用户原话:「已连上手机,最新agent,cg63重试采集报错如下,找出原因:没有找到安全的商品规格入口,可以参考"D:\chengma\cmautobuy\client"的代码实现的逻辑。」
目标:在不放宽到任意文字点击的前提下,让底部购买栏在出现「单独购买 + 发起拼单」双按钮时能确定性地选出唯一入口,而不是因候选不唯一直接失败。
任务与商品:
purchase_task
pdd_product_id=1325
goods_id=320573752790
spec_source=exact_match
status=failed
purchase_task_attempt
PurchaseRehearsalExecutor
collection_task
页面形态(USB 设备 3B65BD02H7F00000,屏幕 1080x2376,仅读取控件树,未点击、未下单、未支付):
商品页自上而下为轮播图、价格、标题、服务标签、评价区,不存在任何「请选择 / 已选 + 规格词」的规格行。因此 explicitSpecEntry 与 nestedSpecEntry 均为空,只能落到底部购买栏兜底。
explicitSpecEntry
nestedSpecEntry
底部购买栏为两个并列可点击 ViewGroup:
两者全部通过 safeBottomSpecEntries 的既有几何约束(centerX≥0.4W=432、centerY≥0.8H=1901、bottom≥0.88H=2091、width≥0.08W=86、height≤0.3H=713),因此 bottomSpecEntries.size == 2。
safeBottomSpecEntries
bottomSpecEntries.size == 2
失败点(PddProductDetailCollector.kt):
PddProductDetailCollector.kt
val bottomSpecEntry = bottomSpecEntries.singleOrNull() // 候选为 2 时返回 null
singleOrNull() 在双按钮下返回 null,specEntry 与 quickConfirmationEntry 同时为空,PurchaseRehearsalExecutor.openSpecPanel 的 candidates 为空集,直接返回 PURCHASE_SPEC_ENTRY_NOT_FOUND /「没有找到安全的商品规格入口」。
singleOrNull()
specEntry
quickConfirmationEntry
PurchaseRehearsalExecutor.openSpecPanel
candidates
PURCHASE_SPEC_ENTRY_NOT_FOUND
值得注意的是,safeBottomSpecEntries 内部已经计算了排序:
.sortedWith(compareBy<Pair<SafeSpecEntry, Long>> { it.second }.thenByDescending { it.first.clickTarget.bounds.centerX })
即按可点击区域面积升序、再按 centerX 降序,但该优先级随后被 singleOrNull() 丢弃。
参考实现对照(D:\chengma\cmautobuy\client\src\util\get_size_panle_coord.py):该实现遇到多候选时不放弃,而是按 10 + 3*(含价格数字) + y_ratio*2 + x_ratio 打分并取最大值。本场景两个按钮前三项相同,最终由 x_ratio 决出,即取最右侧入口。
D:\chengma\cmautobuy\client\src\util\get_size_panle_coord.py
10 + 3*(含价格数字) + y_ratio*2 + x_ratio
x_ratio
诊断现状:specEntryEvidence 生成的标量证据(specEntryCandidates、explicit、nested、bottomPurchase 等计数)当前仅通过 AgentForegroundService.kt:476 写入 logcat(tag GoAutoPurchasePanel),不随失败结果回传服务端,导致后台只能看到一句无上下文的错误文案,本次必须接手机拉取控件树才能定位。
specEntryEvidence
specEntryCandidates
explicit
nested
bottomPurchase
AgentForegroundService.kt:476
GoAutoPurchasePanel
底部购买栏出现多个合法候选时,按最右优先选择唯一入口,与 cmautobuy 参考实现的语义一致。
specEntrySource
SPEC_ENTRY_NOT_FOUND
SPEC_ENTRY_TARGET_AMBIGUOUS
SPEC_PANEL_NOT_OPENED
[...]
buyWords
nonConfigurableClickDenylist
「单独购买」与「发起拼单」打开的是同一个规格面板,对本工单涉及的规格探测无差异;但同一段选择逻辑同时服务真实采购路径,届时两者对应不同成交形态与价格(本商品为 ¥27.9 与 ¥17.72)。本次按用户确认采用最右优先,该业务后果由人工承担。
PDD 新版详情页普遍不再暴露独立规格行,仅能由底部按钮进入规格面板,因此本形态预计影响面较广,而非单一商品。
属于恢复既有「打开安全规格面板」行为的 Android 缺陷修复,无新页面、导航或用户可见交互变化,不需要 UI 原型。设计证据为 PddProductDetailCollector 现有安全语义、上述真机控件树事实,以及 cmautobuy get_size_panel_coord 的同场景既有实现。
PddProductDetailCollector
get_size_panel_coord
:app:assembleDebug
需更新长期业务规则 Business-Rules-and-Glossary 中商品规格入口的安全边界描述,补充「底部购买入口多候选时按最右优先确定性选择」。按 Wiki-first 流程先改线上页面并在线回读 revision,再执行一次 sync 与一次 sync --check。
Business-Rules-and-Glossary
sync
sync --check
注意:仓库内 docs/12-syb-erp-interface.md 存在与本工单无关的既有改动,sync 的保护机制可能阻止同步(#208 已遇到同样情况)。不得重置或覆盖该无关改动,如被阻止则在本工单记录原因。
docs/12-syb-erp-interface.md
无阻塞依赖。与 #208(已实施,待验收)为同一 CG63 故障的不同成因,本工单处理 #208 未覆盖的底部双按钮形态。
实施由 Codex 执行;本仓库 Gitea 无 codex 账号,指派人记为 ila。实施完成后由 Claude 按本工单验收标准审核。
已完成并推送,待验收。
分支 feat/209-pdd-bottom-spec-entry,基于 fd013c6(#208)。实施由 Codex 执行,审核与收尾由 Claude 完成。
feat/209-pdd-bottom-spec-entry
fd013c6
ac6a57e fix(agent): 底部规格入口最右优先 (#209)
compareByDescending { centerX }.thenBy { area }
firstOrNull()
bottom_purchase
bottom_purchase_rightmost
9e800ce fix(agent): 区分规格入口点击目标不唯一 (#209)
FreshActionResult.AMBIGUOUS
specEntryCandidates=1
审核阶段由 Claude 独立重跑,未复用实施阶段的构建缓存:
./gradlew :app:testDebugUnitTest --rerun-tasks
bottomPurchaseEntriesPreferRightmostCandidate
equallyRightmostBottomPurchaseEntriesPreferSmallerArea
bottomPurchaseCandidatesStillExcludeReviewOrderAndPaymentContexts
spec entry failures include scalar diagnostics only
./gradlew :app:assembleDebug
git diff --check
git diff HEAD~1 --check
python dev_scripts/harness.py check --strict
server/config/settings.yml
用本工单立单时从 USB 设备 3B65BD02H7F00000 读取的 CG63 商品页控件树(1080x2376)离线回放新的选择逻辑:
候选数 = 2 ¥27.9单独购买 bounds=(446,2166)-(685,2328) centerX=565.5 area=38718 ¥17.72发起拼单 bounds=(685,2166)-(1080,2328) centerX=882.5 area=63990 旧 singleOrNull() -> null(即本工单要修复的失败) 新 firstOrNull() + centerX降序 -> ¥17.72发起拼单 specEntrySource -> bottom_purchase_rightmost
结论:该页面形态可产生唯一规格入口,且命中最右侧按钮,与已确认决策一致。
需要注意,若只把 singleOrNull() 改为 firstOrNull() 而不调整原有排序,将命中「单独购买」而非最右;本次已同时调整排序,故结果正确。
线上 Business-Rules-and-Glossary 已更新并在线回读,新 revision 276af363a24d663f78489da49c1c175493069711。新增内容:底部购买入口通过全部安全过滤后仍多候选时,按最右优先确定性选出唯一入口,记为 bottom_purchase_rightmost,唯一候选仍记为 bottom_purchase;并说明「单独购买」与「发起拼单」打开同一规格面板但对应不同成交形态与价格,最右优先属经人工确认的取舍。
276af363a24d663f78489da49c1c175493069711
python dev_scripts/harness.py sync 被保护机制阻止,本工单未导出镜像,原因是工作区存在与本工单无关的未提交改动 docs/12-syb-erp-interface.md。按仓库规则不得重置或覆盖无关改动,故未强制同步。这是继 #208 之后第二次被同一文件阻塞,docs/03-business-rules-and-glossary.md 的 wiki_revision 仍停在 9b195f3c578f980b808e6eb28f36e7bf5294e75b,已落后 #208 与本工单两个 revision。线上 Wiki 为权威源,内容正确;镜像滞后需在该无关改动处理后单独补同步。
python dev_scripts/harness.py sync
docs/03-business-rules-and-glossary.md
wiki_revision
9b195f3c578f980b808e6eb28f36e7bf5294e75b
另外 sync --check 报告 docs/02-architecture-and-code-map.md 的 wiki_revision 与内容均和线上不一致。该问题与本工单无关,为既有状态,未处理。
docs/02-architecture-and-code-map.md
origin/feat/209-pdd-bottom-spec-entry
No dependencies set.
The note is not visible to the blocked user.
来源与目标
2026-09-03 用户报告:本地服务已安装 #208 的 Debug APK(16:30 完成安装)后,采购任务 CG63 于 16:34 重试两次仍然失败,错误仍为「没有找到安全的商品规格入口」。用户原话:「已连上手机,最新agent,cg63重试采集报错如下,找出原因:没有找到安全的商品规格入口,可以参考"D:\chengma\cmautobuy\client"的代码实现的逻辑。」
目标:在不放宽到任意文字点击的前提下,让底部购买栏在出现「单独购买 + 发起拼单」双按钮时能确定性地选出唯一入口,而不是因候选不唯一直接失败。
当前事实(真机证据,2026-09-03)
任务与商品:
purchase_task#63(CG63),pdd_product_id=1325,goods_id=320573752790,spec_source=exact_match,目标规格「圖片色豹紋 / 均碼(建議40.0-62.5公斤)」,当前status=failed。purchase_task_attempt中 #63 自 15:53 至 16:34 连续失败 8 次;其中 116、117 两次发生在 #208 的 APK 安装(16:30)之后,证明 #208 的嵌套规格行兼容未覆盖该页面形态。PurchaseRehearsalExecutor),非采集任务;同期collection_task最新记录停在 2026-09-01。页面形态(USB 设备 3B65BD02H7F00000,屏幕 1080x2376,仅读取控件树,未点击、未下单、未支付):
商品页自上而下为轮播图、价格、标题、服务标签、评价区,不存在任何「请选择 / 已选 + 规格词」的规格行。因此
explicitSpecEntry与nestedSpecEntry均为空,只能落到底部购买栏兜底。底部购买栏为两个并列可点击 ViewGroup:
两者全部通过
safeBottomSpecEntries的既有几何约束(centerX≥0.4W=432、centerY≥0.8H=1901、bottom≥0.88H=2091、width≥0.08W=86、height≤0.3H=713),因此bottomSpecEntries.size == 2。失败点(
PddProductDetailCollector.kt):singleOrNull()在双按钮下返回 null,specEntry与quickConfirmationEntry同时为空,PurchaseRehearsalExecutor.openSpecPanel的candidates为空集,直接返回PURCHASE_SPEC_ENTRY_NOT_FOUND/「没有找到安全的商品规格入口」。值得注意的是,
safeBottomSpecEntries内部已经计算了排序:即按可点击区域面积升序、再按 centerX 降序,但该优先级随后被
singleOrNull()丢弃。参考实现对照(
D:\chengma\cmautobuy\client\src\util\get_size_panle_coord.py):该实现遇到多候选时不放弃,而是按10 + 3*(含价格数字) + y_ratio*2 + x_ratio打分并取最大值。本场景两个按钮前三项相同,最终由x_ratio决出,即取最右侧入口。诊断现状:
specEntryEvidence生成的标量证据(specEntryCandidates、explicit、nested、bottomPurchase等计数)当前仅通过AgentForegroundService.kt:476写入 logcat(tagGoAutoPurchasePanel),不随失败结果回传服务端,导致后台只能看到一句无上下文的错误文案,本次必须接手机拉取控件树才能定位。用户已确认的决策
底部购买栏出现多个合法候选时,按最右优先选择唯一入口,与 cmautobuy 参考实现的语义一致。
范围
safeBottomSpecEntries的消费方由singleOrNull()改为在既有安全过滤全部通过的候选中确定性取唯一入口,排序调整为最右优先(centerX 降序;并列时以面积升序兜底),使排序结果与已确认决策一致。specEntrySource需能区分该入口是唯一候选还是多候选按优先级选出,便于后续排查。SPEC_ENTRY_NOT_FOUND与SPEC_ENTRY_TARGET_AMBIGUOUS的失败消息追加已有的标量诊断串,格式与同函数内SPEC_PANEL_NOT_OPENED的[...]既有先例一致。该串只含计数与布尔量,不含节点文本、路径、控件树或截图。非目标
safeBottomSpecEntries既有的几何约束、buyWords语义要求、nonConfigurableClickDenylist与评价上下文排除。已知风险
「单独购买」与「发起拼单」打开的是同一个规格面板,对本工单涉及的规格探测无差异;但同一段选择逻辑同时服务真实采购路径,届时两者对应不同成交形态与价格(本商品为 ¥27.9 与 ¥17.72)。本次按用户确认采用最右优先,该业务后果由人工承担。
PDD 新版详情页普遍不再暴露独立规格行,仅能由底部按钮进入规格面板,因此本形态预计影响面较广,而非单一商品。
方案与设计证据
属于恢复既有「打开安全规格面板」行为的 Android 缺陷修复,无新页面、导航或用户可见交互变化,不需要 UI 原型。设计证据为
PddProductDetailCollector现有安全语义、上述真机控件树事实,以及 cmautobuyget_size_panel_coord的同场景既有实现。验收
:app:assembleDebug通过。文档影响
需更新长期业务规则
Business-Rules-and-Glossary中商品规格入口的安全边界描述,补充「底部购买入口多候选时按最右优先确定性选择」。按 Wiki-first 流程先改线上页面并在线回读 revision,再执行一次sync与一次sync --check。注意:仓库内
docs/12-syb-erp-interface.md存在与本工单无关的既有改动,sync的保护机制可能阻止同步(#208 已遇到同样情况)。不得重置或覆盖该无关改动,如被阻止则在本工单记录原因。依赖
无阻塞依赖。与 #208(已实施,待验收)为同一 CG63 故障的不同成因,本工单处理 #208 未覆盖的底部双按钮形态。
实施分工
实施由 Codex 执行;本仓库 Gitea 无 codex 账号,指派人记为 ila。实施完成后由 Claude 按本工单验收标准审核。
已完成并推送,待验收。
实现
分支
feat/209-pdd-bottom-spec-entry,基于fd013c6(#208)。实施由 Codex 执行,审核与收尾由 Claude 完成。ac6a57e fix(agent): 底部规格入口最右优先 (#209)safeBottomSpecEntries的排序由「面积升序、centerX 降序」改为compareByDescending { centerX }.thenBy { area },即最右优先、并列时可点击面积升序兜底。singleOrNull()改为firstOrNull(),多候选不再直接失败。specEntrySource区分来源:唯一候选仍为bottom_purchase,多候选按优先级选出记为bottom_purchase_rightmost。SPEC_ENTRY_NOT_FOUND与SPEC_ENTRY_TARGET_AMBIGUOUS的失败消息追加标量诊断串,写法沿用 #206 在SPEC_PANEL_NOT_OPENED上的既有先例。buyWords语义要求、nonConfigurableClickDenylist与评价上下文排除。9e800ce fix(agent): 区分规格入口点击目标不唯一 (#209)FreshActionResult.AMBIGUOUS分支原沿用「规格入口候选不唯一」文案,与同串证据中的specEntryCandidates=1自相矛盾。该处歧义来自控件树在点击时对同一目标的重复匹配,而非解析候选不唯一,故改为「规格入口点击目标不唯一」,并同步更新断言。验证
审核阶段由 Claude 独立重跑,未复用实施阶段的构建缓存:
./gradlew :app:testDebugUnitTest --rerun-tasks:260 通过,0 失败,0 错误,0 跳过。本工单新增的 4 个用例(bottomPurchaseEntriesPreferRightmostCandidate、equallyRightmostBottomPurchaseEntriesPreferSmallerArea、bottomPurchaseCandidatesStillExcludeReviewOrderAndPaymentContexts、spec entry failures include scalar diagnostics only)已确认实际执行。./gradlew :app:assembleDebug:BUILD SUCCESSFUL。git diff --check、git diff HEAD~1 --check:clean。python dev_scripts/harness.py check --strict:通过。docs/12-syb-erp-interface.md、server/config/settings.yml及 7 个未跟踪文件)未被修改、未被暂存、未被提交。真机页面回放验证
用本工单立单时从 USB 设备 3B65BD02H7F00000 读取的 CG63 商品页控件树(1080x2376)离线回放新的选择逻辑:
结论:该页面形态可产生唯一规格入口,且命中最右侧按钮,与已确认决策一致。
需要注意,若只把
singleOrNull()改为firstOrNull()而不调整原有排序,将命中「单独购买」而非最右;本次已同时调整排序,故结果正确。文档
线上
Business-Rules-and-Glossary已更新并在线回读,新 revision276af363a24d663f78489da49c1c175493069711。新增内容:底部购买入口通过全部安全过滤后仍多候选时,按最右优先确定性选出唯一入口,记为bottom_purchase_rightmost,唯一候选仍记为bottom_purchase;并说明「单独购买」与「发起拼单」打开同一规格面板但对应不同成交形态与价格,最右优先属经人工确认的取舍。python dev_scripts/harness.py sync被保护机制阻止,本工单未导出镜像,原因是工作区存在与本工单无关的未提交改动docs/12-syb-erp-interface.md。按仓库规则不得重置或覆盖无关改动,故未强制同步。这是继 #208 之后第二次被同一文件阻塞,docs/03-business-rules-and-glossary.md的wiki_revision仍停在9b195f3c578f980b808e6eb28f36e7bf5294e75b,已落后 #208 与本工单两个 revision。线上 Wiki 为权威源,内容正确;镜像滞后需在该无关改动处理后单独补同步。另外
sync --check报告docs/02-architecture-and-code-map.md的wiki_revision与内容均和线上不一致。该问题与本工单无关,为既有状态,未处理。未执行
origin/feat/209-pdd-bottom-spec-entry。