docs: import wiki at afc651f75a3a
+307
@@ -0,0 +1,307 @@
|
||||
<!-- docs-wiki-sync:docs/tasks/T-269.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
|
||||
> 同步来源:[`docs/tasks/T-269.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/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` 要求返回键自身可点
|
||||
|
||||
```kotlin
|
||||
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 时
|
||||
维持原停止行为;诊断键格式。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- [x] 真机 dump 的商品详情页特征(返回不可点 + 三个 marker)被识别为 `PRODUCT_DETAIL`。
|
||||
- [x] 搜索结果页、首页、规格弹层、订单确认页不被误判为商品详情页,有回归测试。
|
||||
- [x] `UNKNOWN` 且无结算迹象时执行有界归位,到达已识别页面后继续流程。
|
||||
- [x] 出现 `hasCheckoutRoot` / 结构判据 / `ORDER_CONFIRMATION` / 风控时**不归位**,
|
||||
立即维持原安全停止,有单测证明。
|
||||
- [x] 归位路径不调用任何提交订单、支付或扣款语义动作,有单测证明。
|
||||
- [x] 用尽 `MAX_HOME_RECOVERY_STEPS` 仍为 `UNKNOWN` 时行为与本任务前一致。
|
||||
- [x] `homeRecoverySteps` / `homeRecovered` 出现在诊断 message 且不含页面文本。
|
||||
- [x] T-263/T-264/T-265/T-268 的判据与诊断键未被改动,有回归测试。
|
||||
- [x] 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`。
|
||||
Reference in New Issue
Block a user