docs: import wiki at afc651f75a3a

ila
2026-08-07 16:37:01 +08:00
parent f5098bc880
commit fd13b0824b
+190
@@ -0,0 +1,190 @@
<!-- docs-wiki-sync:docs/tasks/T-255.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
> 同步来源:[`docs/tasks/T-255.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-255.md) · commit `afc651f75a3a`
---
id: T-255
title: 为候选卡片过滤器增加逐条件拒绝计数与滚动后容器可见性观测
phase: 2
deps:
- T-254
status: DOING
created: 2026-07-30
context_ref: c488e50
work_branch: null
write_paths:
- docs/tasks/T-255.md
- docs/current-state.md
- android-buyer/app/src/main/**
- android-buyer/app/src/test/**
---
## 问题 / 背景
T-254 的真机计数分布否证了三条静默跳过路径(`specOpenFailed`、`orderConfirmRedirect`
全为 0),把瓶颈定位到更上游的**候选卡片识别**:
| 轮次 | cards | distinct | attempts | scrolls | resolved |
| --- | --- | --- | --- | --- | --- |
| 1 | 2 | **1** | 1 | 4 | 1 |
| 2 | 2 | **1** | 1 | 4 | 0 |
| 3 | 2 | **1** | 1 | 1 | 0 |
事实推演:整轮只识别出**一个**不同签名的卡片;第一轮 5 次读取中约 4 次返回空,说明
**滚动之后结果页读不到任何卡片**。`maxCandidateAttempts=10` 只用掉 1 次——不是不想试,
是没有可试的候选。而人工在同一台设备、同一张参考图下确认结果页存在多个合格商品。
`BuyerAccessibilityService` 的候选卡片提取(约 `:1023-1069`)要求同时满足九个条件,
任一不满足即 `continue` 丢弃,外部完全无法区分是哪一条否决的:
| # | 条件 |
| --- | --- |
| 1 | className 为 `FrameLayout` / `ViewGroup` |
| 2 | 宽度占 recycler 的 `MIN_CARD_WIDTH_PERCENT`(35)–`MAX_CARD_WIDTH_PERCENT`(60) |
| 3 | 高度 ≥ 屏高 1/8 |
| 4 | 可见面积 ≥ `MIN_VISIBLE_CARD_PERCENT`(80) |
| 5 | 语义文本条数 ≥ `MIN_CANDIDATE_TEXTS`(2) |
| 6 | 存在可见且启用的 `ImageView` |
| 7 | 存在价格语义文本 |
| 8 | 找得到安全点击目标 |
| 9 | `PinduoduoStableProductIdentityPolicy.signature` 非空 |
第 4 和第 9 条嫌疑最大(网格首尾行被裁切;图搜卡片标题过短导致
`PinduoduoObservedTitlePolicy.select()` 返回 null),但**无法从代码断定**。此外
recycler 容器本身的识别(`MIN_MAIN_RECYCLER_NODES`(20)、
`MIN_MAIN_RECYCLER_WIDTH_PERCENT`(90))在滚动后是否仍然成立,同样不可观测。
本任务与 T-254 同性质:**只做可观测性,不调任何阈值**,先拿到拒绝分布再决定改什么。
T-254 边界中「如计数分布显示签名冲突嫌疑再另开任务」的触发条件已满足。
## 关联需求与交互
- 功能:F-005、F-007
- 用户故事:US-004、US-006
- 交互:沿用候选搜索进度与任务详情,不新增采购操作
- 架构/API:复用 T-254 的诊断快照与事件 message 机制,**后端零改动**
## 行为契约
### 一、逐条件拒绝计数
候选卡片提取每次调用产出一份拒绝分布,按第一个不满足的条件归类(**短路归因**,
一个容器只计一次,避免重复计数):
| 键 | 对应条件 |
| --- | --- |
| `rejClass` | 1 className |
| `rejWidth` | 2 宽度占比 |
| `rejHeight` | 3 最小高度 |
| `rejVisible` | 4 可见面积 |
| `rejTexts` | 5 语义文本条数 |
| `rejImage` | 6 无 ImageView |
| `rejPrice` | 7 无价格语义 |
| `rejClick` | 8 无安全点击目标 |
| `rejSignature` | 9 签名为空 |
另需 `containers`:本次遍历到的候选容器总数(进入九条件判定前的基数)。
`containers` 减去九项拒绝之和,应等于本次返回的卡片数,该恒等式必须有单测保证。
### 二、recycler 容器可见性
增加 `recyclerFound`:本次提取是否成功识别出主 recycler 容器。识别失败时九项拒绝
计数全为 0 且 `containers=0`——这两种「零卡片」原因必须能区分。
### 3、诊断汇总口径
以上计数在 `PinduoduoCandidateAutomation` 单轮内**累计**,并入 T-254 的
`CandidateSearchDiagnostics`,按固定顺序追加到既有 11 个键之后:
```
containers=<n> recyclerMiss=<n> rejClass=<n> rejWidth=<n> rejHeight=<n> rejVisible=<n> rejTexts=<n> rejImage=<n> rejPrice=<n> rejClick=<n> rejSignature=<n>
```
`recyclerMiss` 为本轮内 `recyclerFound == false` 的读取次数。
约束沿用 T-254:全部为非负整数、缺失写 0;诊断只进 `message` 不进 `reason`;
**不得写入任何商品标题、店铺名、goods_id、链接或节点原始文本**——只要计数,不要内容。
### 四、既有行为不变
九个过滤条件的**判定逻辑、阈值常量和先后顺序一律不动**,只在既有 `continue` 前
记录归因。卡片提取的返回值、`limit` 语义、点击目标选择必须逐字节等价。
## 方案
1. `BuyerAccessibilityService` 候选卡片提取:把九个条件的 `continue` 改为先记归因
再 `continue`;新增 recycler 识别成败标记;产出一份不可变拒绝分布随卡片列表返回。
2. `PinduoduoCandidateDriver` / `BuyerAccessibilityBridge`:扩展 `candidateCards` 的
返回类型以携带拒绝分布,保持既有调用点语义不变。
3. `PinduoduoCandidateAutomation`:在 `readCandidateCards` 处累计分布,`reset()` 清零,
并入 `diagnostics()` 快照。
4. `CandidateSearchDiagnostics`:追加 11 个字段与 `auditSuffix()` 输出段,保持既有
11 个键的顺序和格式不变。
5. 单测覆盖:九类拒绝各自归因正确、短路只计一次、恒等式成立、recycler 识别失败与
容器全被拒绝两种零卡片可区分、诊断 message 追加格式、既有 11 键输出不变。
## 验收要点
- [ ] 九类拒绝原因各有单测,构造对应的节点条件后只有目标计数递增。
- [ ] 恒等式 `containers - Σrej = 返回卡片数` 有单测保证。
- [ ] recycler 识别失败与「容器都被过滤掉」两种零卡片场景产出不同的诊断分布。
- [ ] 诊断 message 中 T-254 既有 11 个键的顺序、名称和取值逐字节不变。
- [ ] 诊断 message 不含标题、店铺名、goods_id、链接或任何节点原始文本。
- [ ] 九个过滤条件的阈值常量和判定顺序未改变,卡片提取返回值等价,有回归测试。
- [ ] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease` 全部通过。
- [ ] 真机复跑一条采购任务,从 Admin 读到三轮的完整拒绝分布并记入执行记录。
## 边界
**只加观测,不调阈值**:
- 不改 `MIN_CARD_WIDTH_PERCENT`、`MAX_CARD_WIDTH_PERCENT`、`MIN_VISIBLE_CARD_PERCENT`、
`MIN_CANDIDATE_TEXTS`、`MIN_MAIN_RECYCLER_NODES`、`MIN_MAIN_RECYCLER_WIDTH_PERCENT`
或最小高度算式。
- 不改 `PinduoduoObservedTitlePolicy` 的取词规则和 6–160 字符范围。
- 不改 `PinduoduoStableProductIdentityPolicy` 的签名算法。
- 不改滚动策略、三轮预算、去重行为或成功判定式。
- 不改 T-254 的诊断字段语义,不改 T-256 的候选提交门槛。
- 不改后端代码、数据库、API 契约或 Admin 模板。
- 不启动真实下单,不进入订单提交或支付。
- 本任务不修复识别率,只产出定位依据;修复另开任务。
## 执行记录
### 2026-07-30(DOING)
- 修改:将任务状态设为 `DOING`。在 `BuyerAccessibilityService` 的既有九个过滤
`continue` 前增加短路拒绝归因,保留原判断表达式、阈值常量、先后顺序、卡片字段、
`limit` 截断和安全点击目标选择;新增每次读取的 `containers`、`recyclerFound` 和
九类拒绝计数。未改任何条件判断结果。
- 修改:通过 `BuyerAccessibilityBridge` 和 `PinduoduoCandidateDriver` 传递不可变的
`PinduoduoCandidateCardRead`,候选卡片列表保持原顺序和内容;订单 dry-run 调用点
仅取同一列表,行为不变。
- 修改:`PinduoduoCandidateAutomation` 在每次实际读取中累计容器、recycler miss 和
九类拒绝计数,`reset()` 清零,并将字段追加到既有 11 个诊断键之后;诊断只含计数,
未写入节点原始文本、标题、店铺名、goods_id 或链接。
- 修改:补充诊断格式、非负计数、容器恒等式和自动化汇总相关单测;未修改
`MainActivity.kt`、`CandidateCollectionPolicy.kt` 或 `backend-api`。
- 环境:Windows,工作目录 `D:\chengma\cmroubao\android-buyer`,Gradle wrapper,
Android SDK processing warning(SDK XML v4,而当前工具最多理解 v3)仅为警告。
- 命令与结果:
- `.\gradlew.bat testDebugUnitTest --no-daemon`:首次执行失败,245 个测试中 1 个
既有期望未包含新增的 `containers` 值;修正测试 fixture 后重跑通过,245 个测试
全部通过。
- `.\gradlew.bat testReleaseUnitTest --no-daemon`:通过。
- `.\gradlew.bat lintDebug --no-daemon`:通过,生成 `app/build/reports/lint-results-debug.html`。
- `.\gradlew.bat assembleDebug --no-daemon`:通过。
- `.\gradlew.bat assembleRelease --no-daemon`:通过。
- 未运行 `init.ps1`,未运行真机采购任务、未进入订单提交或支付页面。
- 决策:拒绝计数按现有条件链的第一个失败条件归类;首个原有可见/启用/className
短路分支归入 `rejClass`,宽度/高度共用的原有短路分支按宽度优先、否则高度归类。
这样只增加归因记录,不改变原条件的求值和返回卡片列表。
- Blocker / 未验证:最终验收要求的真机复跑(从 Admin 读取三轮完整拒绝分布)无法由
本次执行完成;需采购人员在已授权 PKG110 和拼多多 8.17.0 测试环境中执行并补充
三轮脱敏计数证据。因此任务保持 `DOING`,未标记 `DONE`。
- 最终树复核:新增 3 个单测后再次执行以下原命令,Debug/Release 各 248 个测试且
`failures=0`;`lintDebug`、`assembleDebug`、`assembleRelease` 均为 `BUILD SUCCESSFUL`:
- `.\gradlew.bat testDebugUnitTest --no-daemon`:通过,248/248。
- `.\gradlew.bat testReleaseUnitTest --no-daemon`:通过,248/248。
- `.\gradlew.bat lintDebug --no-daemon`:通过。
- `.\gradlew.bat assembleDebug --no-daemon`:通过。
- `.\gradlew.bat assembleRelease --no-daemon`:通过。