同步来源:
docs/tasks/T-255.md· commitafc651f75a3a
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 语义、点击目标选择必须逐字节等价。
方案
BuyerAccessibilityService候选卡片提取:把九个条件的continue改为先记归因 再continue;新增 recycler 识别成败标记;产出一份不可变拒绝分布随卡片列表返回。PinduoduoCandidateDriver/BuyerAccessibilityBridge:扩展candidateCards的 返回类型以携带拒绝分布,保持既有调用点语义不变。PinduoduoCandidateAutomation:在readCandidateCards处累计分布,reset()清零, 并入diagnostics()快照。CandidateSearchDiagnostics:追加 11 个字段与auditSuffix()输出段,保持既有 11 个键的顺序和格式不变。- 单测覆盖:九类拒绝各自归因正确、短路只计一次、恒等式成立、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:通过。