同步来源:
docs/tasks/T-269.md· commitafc651f75a3a
id: T-269 title: 修正商品详情页识别并在开 App 时归位到已知页面 phase: 2 deps:
- T-268 status: DOING created: 2026-08-01 context_ref: 126507c work_branch: null write_paths:
- docs/tasks/T-269.md
- docs/current-state.md
- android-buyer/app/src/main/**
- android-buyer/app/src/test/**
问题 / 背景
真机任务 54ee4d8a-41a8-4a63-bba2-a7b77162f27d 三轮全部在 pdd_open_app 被
PAYMENT_BOUNDARY 拦下,一步都没跑。T-268 的门控诊断同时证明下单侧一切正常:
ORDER_DRY_RUN_GATE gateOpen=0 execution=1 command=0 cmdAck=0 storageOk=1
canStart=1 pddInstalled=1 a11yEnabled=1 a11yConnected=1
loginBlocker=UNKNOWN → NONE
command=0 属预期(该任务尚未走到人工确认,没有授权)。canStart=1 直接证伪了
「loginBlocker 粘滞缓存阻塞下单」这一先前推测。
失败归因
step=pdd_open_app reason=PAYMENT_BOUNDARY
payPage=UNKNOWN ← 症结
payMarkerIndex=1 ← 提交订单
payMarkerHits=2
payCheckoutRoot=0 payStructureHit=0 payTextSuppressed=0
T-263 的抑制规则是「页面被正面识别为商品详情/规格弹层/搜索结果/图搜结果/首页时,
文本命中放行」。UNKNOWN 不在名单内,因此文本判据强制生效——按设计执行,问题在于
页面本该被识别却没被识别。
payMarkerHits=2 且 payMarkerIndex=1(提交订单)与此前 dump 过的商品详情页特征
完全吻合:该页正好有「提交订单 ¥14.9」和「使用#微信支付…」两条。
直接原因:hasDetailBack 要求返回键自身可点
val hasDetailBack = visibleElements.any { element ->
element.clickable &&
normalize(element.contentDescription.orEmpty()) == "返回"
}
val detailMarkerCount = setOf("客服", "收藏", "店铺")
.count { marker -> normalized.any { it.contains(marker) } }
hasDetailBack && detailMarkerCount >= 2 -> PinduoduoPage.PRODUCT_DETAIL
真机 dump 的商品详情页:
'返回' clickable=false ← 不可点
'店铺' clickable=true
'收藏' clickable=true
'客服' clickable=true
detailMarkerCount = 3 >= 2 成立,但 hasDetailBack 为 false,判定链落到
else -> UNKNOWN。一个 clickable 属性导致整条链路瘫痪。
这与 T-262 发现的 payClickable 恒为 0 是同一个模式:拼多多把文字/图标放在不可点的
子节点里,真正可点的是祖先容器。代码库中已有 clickableNodeOrAncestor()
(BuyerAccessibilityService,受 MAX_CLICK_ANCESTORS 约束)处理此模式。
结构性问题:开 App 依赖上一轮遗留的页面
同一份代码,2026-08-01 00:37 那轮完整跑通并 SUCCEEDED(resolved=2 target=2),
当时拼多多停在中性页面;本轮因停在商品详情页而全军覆没。成败取决于上一轮把拼多多
留在哪一页,这是不可接受的脆弱性。
这已是第三次「残留页面拖垮下一轮」:T-261 是结算页滞留、T-263 是支付边界误报、 本次是详情页未识别。
关联需求与交互
- 功能:F-005、F-007
- 用户故事:US-004、US-006
- 交互:不新增采购操作
- 架构/API:沿用既有诊断机制,后端零改动
行为契约
一、hasDetailBack 改为祖先可点
判定改为:存在返回语义元素,且其自身或有界层数内的祖先可点击。沿用既有
clickableNodeOrAncestor() 的模式与 MAX_CLICK_ANCESTORS 上限,不新增遍历深度。
detailMarkerCount >= 2 的要求保持不变,返回语义的文本匹配规则也不变。
必须有回归测试证明:真机 dump 的商品详情页特征(返回不可点 + 客服/收藏/店铺 三者
齐全)被识别为 PRODUCT_DETAIL;而搜索结果页、首页、规格弹层、订单确认页不因本次
放宽而被误判为商品详情页。
二、开 App 时归位到已知页面
pdd_open_app 步骤中,当拼多多已在前台但页面为 UNKNOWN 时,允许有界归位:
- 最多
MAX_HOME_RECOVERY_STEPS次(常量定义,初值3)返回操作; - 每次返回后重新分类,一旦页面成为任一已正面识别的类型即停止归位并继续流程;
- 用尽次数仍为
UNKNOWN时,维持现有行为(文本命中则PAYMENT_BOUNDARY停止)。
只允许返回方向的导航,不得点击任何前进方向控件。
三、归位的安全前置条件
这是硬边界。 出现以下任一情况时立即停止,不得归位:
hasCheckoutRoot命中;- T-263 结构判据命中(
payStructureHit); - 页面被识别为
ORDER_CONFIRMATION; - 登录、验证码、风控任一安全停止成立。
即:只有「纯文本命中且无任何结算迹象的 UNKNOWN 页面」才允许归位。归位过程中每一步 前必须重新取快照检查上述条件,一旦出现立即中止归位并返回原安全停止结果。
归位不得点击提交订单、立即支付、免密支付、先用后付或任何扣款语义控件;必须有单测 证明该路径不调用任何此类动作。
四、诊断可见
追加计数,置于既有键之后:
homeRecoverySteps=<n> homeRecovered=<0|1>
| 键 | 含义 |
|---|---|
homeRecoverySteps |
本轮执行的归位返回次数 |
homeRecovered |
归位后是否成功到达已识别页面 |
沿用既有脱敏口径:只记计数,不记页面文本或商品信息。
五、既有能力一律不放松
- 不改
paymentMarkers内容与顺序、containsAny实现、hasCheckoutRoot规则。 - 不改 T-263 结构判据、抑制名单与连续确认次数。
- 不改 T-264 原生订单确认页判据、T-265 规格入口优先级与落到结算页的跳过逻辑。
- 不改登录、验证码、风控的检测逻辑与优先级。
- 不改 T-268 门控诊断与既有诊断键的顺序、名称、取值;新键追加在末尾。
- 不改候选卡片识别、签名算法、跨轮去重、价格解析或三轮预算。
方案
PinduoduoPageClassifier的hasDetailBack改为「自身或有界祖先可点」。由于PinduoduoUiElement是扁平模型无父引用,需在节点采集侧补充「该元素自身或祖先 可点」的布尔属性,而非在分类器内遍历树。AndroidPinduoduoImageSearchDriver.openApp()在页面为UNKNOWN且通过第三节安全 前置条件时执行有界归位,沿用既有返回能力,不新增点击目标。- 两个新计数并入诊断快照与 message 尾部。
- 单测覆盖:真机详情页特征被识别为
PRODUCT_DETAIL;其余页面类型不被误判; 归位在 UNKNOWN 且无结算迹象时执行并在到达已识别页面后停止;出现结算迹象或 风控时立即中止归位;归位路径不调用任何购买/支付语义动作;用尽次数仍 UNKNOWN 时 维持原停止行为;诊断键格式。
验收要点
- 真机 dump 的商品详情页特征(返回不可点 + 三个 marker)被识别为
PRODUCT_DETAIL。 - 搜索结果页、首页、规格弹层、订单确认页不被误判为商品详情页,有回归测试。
UNKNOWN且无结算迹象时执行有界归位,到达已识别页面后继续流程。- 出现
hasCheckoutRoot/ 结构判据 /ORDER_CONFIRMATION/ 风控时不归位, 立即维持原安全停止,有单测证明。 - 归位路径不调用任何提交订单、支付或扣款语义动作,有单测证明。
- 用尽
MAX_HOME_RECOVERY_STEPS仍为UNKNOWN时行为与本任务前一致。 homeRecoverySteps/homeRecovered出现在诊断 message 且不含页面文本。- T-263/T-264/T-265/T-268 的判据与诊断键未被改动,有回归测试。
- Android Debug/Release 单测、
lintDebug、assembleDebug、assembleRelease全部通过。 - 真机验证:故意把拼多多停在商品详情页后跑任务,不再被拦在
pdd_open_app; 从 Admin 读取payPage、homeRecoverySteps、homeRecovered记入执行记录。
边界
- 不放松任何支付边界判据,只修正页面识别并增加有界归位。
- 归位只允许返回方向导航,绝不点击前进控件、提交订单或支付。
- 不改
returnFromOrderConfirmation()与 T-265 落到结算页的跳过逻辑。 - 不改登录、验证码、风控的检测与优先级。
- 不改 T-261 清理逻辑、T-262 归因既有键、T-263 结构判据与抑制名单、T-268 门控诊断。
- 不改后端代码、数据库、API 契约或 Admin 模板。
- 不处理价格解析(前缀与节点拆分)问题,另行开任务。
- 不处理图搜结果混入非目标品类的问题,另行开任务。
执行记录
开工后记录修改、命令、结果、环境、决策和 blocker。
2026-08-01 实现
改动文件
android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoPageClassifier.kt—PinduoduoUiElement追加selfOrAncestorClickable: Boolean = clickable字段(扁平模型无父引用,需在采集侧带出该布尔量);hasDetailBack从element.clickable改为element.selfOrAncestorClickable。detailMarkerCount>=2与返回语义的文本匹配规则未改动。android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityService.kt—uiElement()用clickableNodeOrAncestor(node) != null计算selfOrAncestorClickable(复用既有函数与MAX_CLICK_ANCESTORS,不新增 遍历深度、不新增点击);新增performPinduoduoHomeRecoveryBack(),只调用performGlobalAction(GLOBAL_ACTION_BACK),不做任何页面判断(安全前置 条件由调用方在调用前决定)。android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoHomeRecoveryPolicy.kt(新增)— 纯 Kotlin 归位策略:MAX_HOME_RECOVERY_STEPS = 3;仅当页面为UNKNOWN且当前安全停止恰好是「纯文本命中」触发的PAYMENT_BOUNDARY(非checkoutRootHit、非structureHit)时才允许下一步返回;每步后重新 取快照,若出现ORDER_CONFIRMATION/checkoutRootHit/structureHit/ 登录验证码风控任一,立即停止且不算「归位成功」;到达其它已识别页面类型 才算成功。策略只接受goBack/snapshot两个回调,结构上不具备触发 提交订单/支付/免密支付/先用后付的能力。android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityBridge.kt— 新增homeRecoveryBack(),桥接performPinduoduoHomeRecoveryBack()。android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/AndroidPinduoduoImageSearchDriver.kt—openApp()在页面为UNKNOWN时,既有dismissTransientOverlay()之后 追加调用PinduoduoHomeRecoveryPolicy.attempt(),把结果记入新增的homeRecoverySteps/homeRecovered只读属性;不新增点击目标。android-buyer/app/src/main/java/com/roubao/autopilot/procurement/CandidateSearchDiagnostics.kt— 尾部追加homeRecoverySteps/homeRecovered两个字段、对应require边界校验、withHomeRecovery()合并方法(逐次覆盖,不累积),并在auditSuffix()末尾追加两个键。既有键顺序、名称、取值未改动。android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt— 把原先内联构造的AndroidPinduoduoImageSearchDriver提到val imageSearchDriver便于runner.run()结束后读取归位诊断;成功与失败两条 上报路径都追加.withHomeRecovery(...)。未改动候选搜索、门控、清理等 既有诊断字段的计算方式。- 测试:
PinduoduoPageClassifierTest.kt(element()helper 新增selfOrAncestorClickable参数,默认等于clickable;新增真机详情页 识别修复回归测试、「自身与祖先均不可点仍为 UNKNOWN」回归测试、以及 搜索结果页/首页/规格弹层/订单确认页四类不被误判为商品详情页的回归 测试)、PinduoduoHomeRecoveryPolicyTest.kt(新增,覆盖:纯文本命中时 执行归位并在到达已识别页面后停止;checkoutRootHit/结构判据命中时不 归位;归位途中出现ORDER_CONFIRMATION/登录/验证码/风控立即停止且不算 成功;无任何强制停止时不归位;用尽 3 步仍UNKNOWN时行为不变;goBack失败时不计入步数;结构性证明归位只调用goBack)、CandidateSearchDiagnosticsTest.kt(更新既有精确字符串断言以容纳两个 新增尾部键的默认值,新增默认值/withHomeRecovery/覆盖式合并/越界校验 测试)。
关键决策
homeRecoverySteps/homeRecovered的归位循环按任务方案放在AndroidPinduoduoImageSearchDriver.openApp()里(而非PinduoduoImageSearchAutomation),因为方案第 1 条明确把归位职责放在 该驱动;但驱动依赖真实无障碍服务、没有 Robolectric,无法直接单测。为此 把「何时允许归位、何时立即停止」的判定拆成纯 Kotlin 的PinduoduoHomeRecoveryPolicy(只接受注入的goBack/snapshot回调), 由驱动在生产代码里用真实的BuyerAccessibilityBridge调用桥接; 这样既遵循方案的落点,又能满足单测覆盖要求。- 归位判据里把「页面变为
ORDER_CONFIRMATION」明确算作「硬停止、不算 归位成功」,即使ORDER_CONFIRMATION本身是一个「已正面识别」的页面 类型——这是遵照任务契约第三节把ORDER_CONFIRMATION单独列入硬边界的 字面要求,与「二、开 App 时归位到已知页面」第 2 条的「到达已识别页面 即停止并继续流程」有轻微措辞张力,但契约第三节对ORDER_CONFIRMATION的处理更具体、更严格,故以第三节为准。既有awaitPage()仍会把ORDER_CONFIRMATION当作pdd_open_app的可接受落点(未改动该逻辑), 只是不算作本归位机制自身的「成功」。 - 未改动
AndroidPinduoduoUiDriver/PinduoduoSearchAutomation(文本关键词 搜索路径),因为任务方案第 1 条只点名AndroidPinduoduoImageSearchDriver, 且该路径不在真机故障任务涉及的自动化管线上;避免顺手扩大范围。 PinduoduoUiElement.selfOrAncestorClickable默认值取clickable(而非false),使所有未显式设置该字段的既有测试和调用方行为完全 不变,只有新增测试显式传入true/false来模拟「祖先可点但自身不可点」 的真机特征。
执行命令与结果(均在 Windows 侧执行)
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testDebugUnitTest --no-daemon --console=plain"
→ BUILD SUCCESSFUL;Debug 单测合计 412 项,全部通过(含新增
PinduoduoHomeRecoveryPolicyTest 9 项、PinduoduoPageClassifierTest 57 项、
CandidateSearchDiagnosticsTest 24 项)。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testReleaseUnitTest --no-daemon --console=plain"
→ BUILD SUCCESSFUL;Release 单测合计 412 项,全部通过。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat lintDebug --no-daemon --console=plain"
→ BUILD SUCCESSFUL。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat assembleDebug --no-daemon --console=plain"
→ BUILD SUCCESSFUL。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat assembleRelease --no-daemon --console=plain"
→ BUILD SUCCESSFUL。
五个目标全部通过。init.ps1 未运行(按指示不运行)。
待完成(人工/真机)
- 未做:真机验证——故意把拼多多停在商品详情页后跑任务,确认不再被拦在
pdd_open_app,并从 Admin 读取payPage、homeRecoverySteps、homeRecovered记入本节。这一步只能在真机上完成,本次未执行,任务 状态保持DOING,不改为DONE。