1
T-269
ila edited this page 2026-08-07 16:37:09 +08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

同步来源:docs/tasks/T-269.md · commit afc651f75a3a


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 时,允许有界归位:

  1. 最多 MAX_HOME_RECOVERY_STEPS 次(常量定义,初值 3)返回操作;
  2. 每次返回后重新分类,一旦页面成为任一已正面识别的类型即停止归位并继续流程;
  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 门控诊断与既有诊断键的顺序、名称、取值;新键追加在末尾。
  • 不改候选卡片识别、签名算法、跨轮去重、价格解析或三轮预算。

方案

  1. PinduoduoPageClassifier 的 hasDetailBack 改为「自身或有界祖先可点」。由于 PinduoduoUiElement 是扁平模型无父引用,需在节点采集侧补充「该元素自身或祖先 可点」的布尔属性,而非在分类器内遍历树。
  2. AndroidPinduoduoImageSearchDriver.openApp() 在页面为 UNKNOWN 且通过第三节安全 前置条件时执行有界归位,沿用既有返回能力,不新增点击目标。
  3. 两个新计数并入诊断快照与 message 尾部。
  4. 单测覆盖:真机详情页特征被识别为 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。