2026-09-03,CG67(purchase_task #67)采购失败,停留在商品页,错误为「没有找到安全的商品规格入口」。用户原话:「最新agent,cg67采购失败:没有找到安全的商品规格入口,停留在商品页.分析原因」。
purchase_task
经排查,这不是 #209 的回归,而是采购路径在寻找规格入口时完全没有等待导致的竞态:商品页底部操作栏尚未挂载到无障碍树时就被判定为「找不到入口」并立即失败。
目标:让采购路径在判定规格入口不存在之前,具备与采集路径同等的、有界的入口就绪等待能力。
服务端已落库的错误(诊断串由 #209 引入,本次正是靠它定位):
purchase_task #67 error_code: PURCHASE_SPEC_ENTRY_NOT_FOUND error_message: 没有找到安全的商品规格入口 [specEntryCandidates=0;explicit=0;nested=0;bottomPurchase=0; panelAlreadyOpen=false;reviewPage=false;pageEvidence=true]
关键:bottomPurchase=0。#209 修复的是 bottomPurchase=2(候选不唯一),本次是一个候选都没有,属于不同成因。
bottomPurchase=0
bottomPurchase=2
两次 attempt:
spec_probe_completed
failed
attempt 124 的规格探测成功打开了规格面板,证明 #209 的底部入口最右优先在该页面形态上生效。失败的是其后仅 111ms 启动的采购 attempt。
设备与页面核对:
base.apk
bottom_purchase_rightmost
点击目标不唯一
paymentBackAttempts
[446,2166][685,2328]
[685,2166][1080,2328]
safeBottomSpecEntries
android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt:
android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt
currentScreen()
private fun currentScreen(input: PurchaseExecutionInput): ParsedPddScreen = PddScreenParser.parse(driver.capture(), DEFAULT_COLLECTOR, input.goodsId, null)
openSpecPanel() 只调用一次 currentScreen(),候选为空即刻返回失败,中间没有 pause 也没有重采样。
openSpecPanel()
pause
动作循环中形似重试的写法实际不重试——这些函数返回 null 表示成功,?: 链在失败(返回非 null)时直接短路:
null
?:
OPEN_SPEC_PANEL -> openSpecPanel(input, action) ?: recoverSoldOut(input, closeSpecPanel = true) ?: openSpecPanel(input, action)
verifyProduct()
if (snapshot.packageName == PDD_PACKAGE && snapshot.nodes.any { it.visible }) { return recoverSoldOut(input, PddScreenParser.parse(snapshot, DEFAULT_COLLECTOR, input.goodsId, null)) }
repeat(30) { pause(100) }
采集路径早已解决同一问题,PddProductDetailCollector 中存在有界的入口就绪等待,随后还有向上滑动重试:
PddProductDetailCollector
val entryReadyDeadline = minOf(deadline, now() + 2_000) while (!current.specPanelOpen && specEntry == null && now() <= entryReadyDeadline) { pause(100) current = parse(goodsId, config, evidence) ... }
该能力从未被带到采购路径。
尝试实测商品页底部操作栏的渲染延迟,但 uiautomator dump 单次往返约 2.3 秒,无法捕捉亚秒级窗口;第一个可采样点(约 5 秒)时按钮已存在。因此底部栏的确切渲染耗时未测得,仅能确定:失败的 attempt 全程仅 3.75 秒(含 openProduct 的 waitAfterMs=1000 与 PDD 前台检测),而按钮在约 5 秒时稳定存在。等待窗口的取值需要实施方在此约束下自行判断,并说明理由。
uiautomator dump
waitAfterMs=1000
SPEC_ENTRY_NOT_FOUND
entryReadyDeadline
reviewPageOpen
screen.problem
specPanelOpen
ParsedPddScreen.hasPurchaseProductEvidence()
packageMatched && rootAvailable && (specPanelOpen || specEntry != null || quickConfirmationEntry != null || summary.title != null)
recoverSoldOut
PDD_DETAIL_ENTRY_FAILED
verifyProduct
buyWords
nonConfigurableClickDenylist
isSpecEntry
isSpecEntryContext
openSpecPanel
spec_source=exact_match
attempt 124 的 phase 为 purchase,但回传 result_type=spec_probe_completed,而任务 spec_source=exact_match。按 server/app/goauto/purchase/reset.go 的逻辑,仅当 spec_source == "unresolved" 时才应进入探测阶段。这次多余的规格探测导致采购 attempt 在 111ms 后对商品页发起第二次导航,是本次竞态的直接触发条件之一。此现象需要单独确认,不在本工单范围内修改。
phase
purchase
result_type=spec_probe_completed
server/app/goauto/purchase/reset.go
spec_source == "unresolved"
属于恢复既有「打开商品规格面板」行为的 Android 缺陷修复,无新页面、导航或用户可见交互变化,不需要 UI 原型。设计证据为上述数据库与真机证据、PurchaseRehearsalExecutor 现有安全语义,以及 PddProductDetailCollector 中已验证的同类等待实现。
PurchaseRehearsalExecutor
:app:assembleDebug
若最终方案改变了 Business-Rules-and-Glossary 中已记录的商品规格入口识别安全边界的对外描述,则需按 Wiki-first 流程更新;若仅为等待时序的内部实现调整、未改变对外安全语义,则记录为无长期文档影响并说明原因。
Business-Rules-and-Glossary
工作区已知存在与本工单无关的既有改动 docs/12-syb-erp-interface.md(曾在 #208、#209 两次阻止 sync),若再次阻止,记录原因,不得重置或覆盖。
docs/12-syb-erp-interface.md
sync
无阻塞依赖。与 #209(已实施,待验收)为同一函数的不同成因:#209 解决候选不唯一,本工单解决候选尚未渲染即判失败。基于 494969d(#210 完成态),以保持与设备上已安装构建一致。
494969d
实施由 Codex 执行;本仓库 Gitea 无 codex 账号,指派人记为 ila。实施完成后由 Claude 按本工单验收标准审核。
基于基线 494969d(#210 完成态),在分支 feat/211-spec-entry-ready-wait 完成 #211,提交 22755e0:fix(agent): 有界等待采购规格入口就绪并收紧商品页证据放行 (#211)。
feat/211-spec-entry-ready-wait
22755e0
fix(agent): 有界等待采购规格入口就绪并收紧商品页证据放行 (#211)
有界入口就绪等待与重采样:openSpecPanel() 在判定 SPEC_ENTRY_NOT_FOUND 前改为 while (target == null) 循环,每次迭代继续执行既有的 reviewPageOpen、screen.problem、specPanelOpen 判定,不跳过任何安全检查;无候选时 pause(SPEC_ENTRY_READY_POLL_MILLIS) 并重采样。等待上限取 SPEC_ENTRY_READY_WAIT_POLLS = 20 x SPEC_ENTRY_READY_POLL_MILLIS = 100L = 2000ms,与采集路径 PddProductDetailCollector 既有 entryReadyDeadline 的 2 秒有界上限保持一致。窗口取值理由:工单已说明无法测得底部栏亚秒级渲染耗时,且首个稳定采样点(约 5 秒)时按钮已存在,故采用与采集路径一致的 2 秒有界窗口,把「等得不够久」的竞态窗口纳入处理,同时保持明确有限上限。
while (target == null)
pause(SPEC_ENTRY_READY_POLL_MILLIS)
SPEC_ENTRY_READY_WAIT_POLLS = 20
SPEC_ENTRY_READY_POLL_MILLIS = 100L
收紧 verifyProduct() 放行条件:不再以「任意可见节点」放行,改为复用既有语义判据 screen.hasPurchaseProductEvidence()(packageMatched && rootAvailable && (specPanelOpen || specEntry != null || quickConfirmationEntry != null || summary.title != null))。纯加载中间帧(仅有 content 节点)不再放行;保留原有 repeat(50) 有界轮询与超时失败路径 PDD_DETAIL_ENTRY_FAILED。
screen.hasPurchaseProductEvidence()
content
repeat(50)
失败诊断完善:specEntryEvidence() 追加标量 entryReadyWaitPolls 与 entryReadyWaitMillis,超时路径在 SPEC_ENTRY_NOT_FOUND 前用 entryReadyWaitPolls=20;entryReadyWaitMillis=2000 表达「等待后仍无候选」;不包含任何节点文本、路径、控件树或截图。
specEntryEvidence()
entryReadyWaitPolls
entryReadyWaitMillis
entryReadyWaitPolls=20;entryReadyWaitMillis=2000
单测补充:新增/调整三类场景——「首次采样无底部入口、等待若干轮后入口出现并成功打开面板」;「等待窗口内始终无任何候选仍在有界时间内失败」;「verifyProduct 在仅有加载中间帧时不放行、在具备商品页证据后放行」。entryReadyWaitPolls 计数断言一并校验等待迭代确切为 20(另 1 个 100ms pause 来自既有 open-product 前台轮询)。
android: gradlew clean test
testDebugUnitTest
testReleaseUnitTest
BUILD SUCCESSFUL
android: gradlew assembleDebug
无长期文档影响并说明原因:本次仅为商品规格入口的等待时序内部实现调整(有界就绪等待 + 收紧商品页放行判据),未改变对外安全语义(几何约束、购买词语义、拒绝名单、评价上下文排除、评价页处理均保持不变),未新增/删除页面、导航或用户可见交互,故不触发 Wiki 更新;未运行 sync。
工作区已知的无关改动 docs/12-syb-erp-interface.md 未触及、未重置、未覆盖;另发现 server/config/settings.yml(writertimeout 2 -> 620)亦与本工单无关,同样未纳入本次提交。本次提交仅含两个 Android 文件。
server/config/settings.yml
writertimeout
需在真机(含 #209/#210 构建的设备形态)人工授权后单独验证:首次采样无底部入口、随后入口出现能打开的竞态场景。
用户授权后,已构建 assembleDebug 并安装到真机 3B65BD02H7F00000(adb install -r,Success,lastUpdateTime=2026-09-03 17:56:17)。
assembleDebug
3B65BD02H7F00000
adb install -r
Success
lastUpdateTime=2026-09-03 17:56:17
从设备拉回安装的 base.apk 解包核对 dex(classes8.dex):确认包含 entryReadyWaitPolls、entryReadyWaitMillis(本工单新增诊断标量)、hasPurchaseProductEvidence(verifyProduct 收紧判据),并仍包含 bottom_purchase_rightmost(#209,证明是在 #209/#210 之上的累积构建),即设备上运行的确为含 #211 的构建。
classes8.dex
hasPurchaseProductEvidence
尚未执行真实采购演练任务的功能性真机验证(需单独运行任务,涉及采购流程,未做下单/支付)。
No dependencies set.
The note is not visible to the blocked user.
来源与目标
2026-09-03,CG67(
purchase_task#67)采购失败,停留在商品页,错误为「没有找到安全的商品规格入口」。用户原话:「最新agent,cg67采购失败:没有找到安全的商品规格入口,停留在商品页.分析原因」。经排查,这不是 #209 的回归,而是采购路径在寻找规格入口时完全没有等待导致的竞态:商品页底部操作栏尚未挂载到无障碍树时就被判定为「找不到入口」并立即失败。
目标:让采购路径在判定规格入口不存在之前,具备与采集路径同等的、有界的入口就绪等待能力。
当前事实(真机与数据库证据,2026-09-03)
服务端已落库的错误(诊断串由 #209 引入,本次正是靠它定位):
关键:
bottomPurchase=0。#209 修复的是bottomPurchase=2(候选不唯一),本次是一个候选都没有,属于不同成因。两次 attempt:
spec_probe_completedfailedattempt 124 的规格探测成功打开了规格面板,证明 #209 的底部入口最右优先在该页面形态上生效。失败的是其后仅 111ms 启动的采购 attempt。
设备与页面核对:
base.apk解包核对,dex 中确认包含bottom_purchase_rightmost、点击目标不唯一、paymentBackAttempts,即设备上运行的确为含 #209、#210 的构建,排除装错包。[446,2166][685,2328](单独购买)与[685,2166][1080,2328](发起拼单),屏幕 1080x2376,safeBottomSpecEntries的五项几何约束(centerX≥0.4W、centerY≥0.8H、bottom≥0.88H、width≥0.08W、height≤0.3H)全部通过。即页面本身完全合格,只是失败瞬间这些节点不在树中。代码事实
android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt:currentScreen()为单次抓取,无等待、无重采样:openSpecPanel()只调用一次currentScreen(),候选为空即刻返回失败,中间没有pause也没有重采样。动作循环中形似重试的写法实际不重试——这些函数返回
null表示成功,?:链在失败(返回非 null)时直接短路:verifyProduct()的放行条件极弱,任意一个可见节点即放行,包含尚在加载、底部操作栏未挂载的中间帧:repeat(30) { pause(100) }等待面板出现),寻找入口之前等待 0 秒。采集路径早已解决同一问题,
PddProductDetailCollector中存在有界的入口就绪等待,随后还有向上滑动重试:该能力从未被带到采购路径。
未能取得的证据(限制说明)
尝试实测商品页底部操作栏的渲染延迟,但
uiautomator dump单次往返约 2.3 秒,无法捕捉亚秒级窗口;第一个可采样点(约 5 秒)时按钮已存在。因此底部栏的确切渲染耗时未测得,仅能确定:失败的 attempt 全程仅 3.75 秒(含 openProduct 的waitAfterMs=1000与 PDD 前台检测),而按钮在约 5 秒时稳定存在。等待窗口的取值需要实施方在此约束下自行判断,并说明理由。范围
openSpecPanel()判定SPEC_ENTRY_NOT_FOUND之前,增加有界的入口就绪等待与重采样,参照采集路径PddProductDetailCollector中entryReadyDeadline的既有做法。等待期间必须继续执行既有的reviewPageOpen、screen.problem、specPanelOpen判定,不得跳过任何安全检查。verifyProduct()的放行条件,不再以「任意可见节点」放行。ParsedPddScreen.hasPurchaseProductEvidence()是既有的、语义正确的判据(packageMatched && rootAvailable && (specPanelOpen || specEntry != null || quickConfirmationEntry != null || summary.title != null)),且已在recoverSoldOut中使用;应优先复用它而非新造判据。收紧后仍须保留既有的超时失败路径(PDD_DETAIL_ENTRY_FAILED),不得因页面始终不达标而无限等待。verifyProduct在仅有加载中间帧时不放行、在具备商品页证据后放行」三类场景。非目标
safeBottomSpecEntries的几何约束、buyWords语义要求、nonConfigurableClickDenylist与评价上下文排除,不放宽isSpecEntry/isSpecEntryContext的语义判据。本工单只解决「等得不够久」,不解决「认得不够宽」。openSpecPanel点击之后既有的面板出现等待逻辑。spec_source=exact_match的任务上执行了规格探测(见下方观察),该问题另行处理。相邻观察(不在本工单范围)
attempt 124 的
phase为purchase,但回传result_type=spec_probe_completed,而任务spec_source=exact_match。按server/app/goauto/purchase/reset.go的逻辑,仅当spec_source == "unresolved"时才应进入探测阶段。这次多余的规格探测导致采购 attempt 在 111ms 后对商品页发起第二次导航,是本次竞态的直接触发条件之一。此现象需要单独确认,不在本工单范围内修改。方案与设计证据
属于恢复既有「打开商品规格面板」行为的 Android 缺陷修复,无新页面、导航或用户可见交互变化,不需要 UI 原型。设计证据为上述数据库与真机证据、
PurchaseRehearsalExecutor现有安全语义,以及PddProductDetailCollector中已验证的同类等待实现。验收
verifyProduct不再被纯加载中间帧放行,且仍保留原有超时失败路径。:app:assembleDebug通过。文档影响
若最终方案改变了
Business-Rules-and-Glossary中已记录的商品规格入口识别安全边界的对外描述,则需按 Wiki-first 流程更新;若仅为等待时序的内部实现调整、未改变对外安全语义,则记录为无长期文档影响并说明原因。工作区已知存在与本工单无关的既有改动
docs/12-syb-erp-interface.md(曾在 #208、#209 两次阻止sync),若再次阻止,记录原因,不得重置或覆盖。依赖
无阻塞依赖。与 #209(已实施,待验收)为同一函数的不同成因:#209 解决候选不唯一,本工单解决候选尚未渲染即判失败。基于
494969d(#210 完成态),以保持与设备上已安装构建一致。实施分工
实施由 Codex 执行;本仓库 Gitea 无 codex 账号,指派人记为 ila。实施完成后由 Claude 按本工单验收标准审核。
实施完成(待验收)
基于基线
494969d(#210 完成态),在分支feat/211-spec-entry-ready-wait完成 #211,提交22755e0:fix(agent): 有界等待采购规格入口就绪并收紧商品页证据放行 (#211)。方案落实(对照工单范围)
有界入口就绪等待与重采样:
openSpecPanel()在判定SPEC_ENTRY_NOT_FOUND前改为while (target == null)循环,每次迭代继续执行既有的reviewPageOpen、screen.problem、specPanelOpen判定,不跳过任何安全检查;无候选时pause(SPEC_ENTRY_READY_POLL_MILLIS)并重采样。等待上限取SPEC_ENTRY_READY_WAIT_POLLS = 20xSPEC_ENTRY_READY_POLL_MILLIS = 100L= 2000ms,与采集路径PddProductDetailCollector既有entryReadyDeadline的 2 秒有界上限保持一致。窗口取值理由:工单已说明无法测得底部栏亚秒级渲染耗时,且首个稳定采样点(约 5 秒)时按钮已存在,故采用与采集路径一致的 2 秒有界窗口,把「等得不够久」的竞态窗口纳入处理,同时保持明确有限上限。收紧
verifyProduct()放行条件:不再以「任意可见节点」放行,改为复用既有语义判据screen.hasPurchaseProductEvidence()(packageMatched && rootAvailable && (specPanelOpen || specEntry != null || quickConfirmationEntry != null || summary.title != null))。纯加载中间帧(仅有content节点)不再放行;保留原有repeat(50)有界轮询与超时失败路径PDD_DETAIL_ENTRY_FAILED。失败诊断完善:
specEntryEvidence()追加标量entryReadyWaitPolls与entryReadyWaitMillis,超时路径在SPEC_ENTRY_NOT_FOUND前用entryReadyWaitPolls=20;entryReadyWaitMillis=2000表达「等待后仍无候选」;不包含任何节点文本、路径、控件树或截图。单测补充:新增/调整三类场景——「首次采样无底部入口、等待若干轮后入口出现并成功打开面板」;「等待窗口内始终无任何候选仍在有界时间内失败」;「
verifyProduct在仅有加载中间帧时不放行、在具备商品页证据后放行」。entryReadyWaitPolls计数断言一并校验等待迭代确切为 20(另 1 个 100ms pause 来自既有 open-product 前台轮询)。验证
android: gradlew clean test:testDebugUnitTest与testReleaseUnitTest全部通过(BUILD SUCCESSFUL)。android: gradlew assembleDebug:通过。文档影响
无长期文档影响并说明原因:本次仅为商品规格入口的等待时序内部实现调整(有界就绪等待 + 收紧商品页放行判据),未改变对外安全语义(几何约束、购买词语义、拒绝名单、评价上下文排除、评价页处理均保持不变),未新增/删除页面、导航或用户可见交互,故不触发 Wiki 更新;未运行
sync。工作区已知的无关改动
docs/12-syb-erp-interface.md未触及、未重置、未覆盖;另发现server/config/settings.yml(writertimeout2 -> 620)亦与本工单无关,同样未纳入本次提交。本次提交仅含两个 Android 文件。遗留
需在真机(含 #209/#210 构建的设备形态)人工授权后单独验证:首次采样无底部入口、随后入口出现能打开的竞态场景。
真机安装(补充)
用户授权后,已构建
assembleDebug并安装到真机3B65BD02H7F00000(adb install -r,Success,lastUpdateTime=2026-09-03 17:56:17)。从设备拉回安装的
base.apk解包核对 dex(classes8.dex):确认包含entryReadyWaitPolls、entryReadyWaitMillis(本工单新增诊断标量)、hasPurchaseProductEvidence(verifyProduct 收紧判据),并仍包含bottom_purchase_rightmost(#209,证明是在 #209/#210 之上的累积构建),即设备上运行的确为含 #211 的构建。尚未执行真实采购演练任务的功能性真机验证(需单独运行任务,涉及采购流程,未做下单/支付)。