docs: import wiki at afc651f75a3a
+250
@@ -0,0 +1,250 @@
|
||||
<!-- docs-wiki-sync:docs/tasks/T-254.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
|
||||
> 同步来源:[`docs/tasks/T-254.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-254.md) · commit `afc651f75a3a`
|
||||
|
||||
---
|
||||
id: T-254
|
||||
title: 为候选搜索静默跳过路径增加诊断计数与细分失败码
|
||||
phase: 2
|
||||
deps:
|
||||
- T-251
|
||||
- T-252
|
||||
- T-253
|
||||
status: DONE
|
||||
created: 2026-07-30
|
||||
context_ref: db4e9f8
|
||||
work_branch: null
|
||||
write_paths:
|
||||
- docs/tasks/T-254.md
|
||||
- docs/current-state.md
|
||||
- android-buyer/app/src/main/**
|
||||
- android-buyer/app/src/test/**
|
||||
---
|
||||
|
||||
## 问题 / 背景
|
||||
|
||||
真机任务连续三轮图片搜索全部失败,Admin 显示
|
||||
`CANDIDATE_SEARCH_EXHAUSTED:候选搜索三轮已耗尽,最后失败原因:CANDIDATE_TARGET_NOT_FOUND`,
|
||||
但人工在同一台设备上用同一张参考图检查,确认拼多多结果页存在符合颜色和尺码硬约束的
|
||||
商品。
|
||||
|
||||
`PinduoduoCandidateAutomation.terminalCollectionResult()` 的分类优先级为:
|
||||
|
||||
1. `!sawCandidateCards` → `CANDIDATE_RESULTS_NOT_READY`
|
||||
2. `identityFailureCounts` 非空 → 身份类细分码
|
||||
3. `skuMismatchCount > 0` → `CANDIDATE_SKU_NOT_MATCHED`
|
||||
4. 其余 → `CANDIDATE_TARGET_NOT_FOUND`
|
||||
|
||||
命中第 4 条说明:卡片已经看到,身份解析一次都没失败,颜色尺码一次都没不匹配。也就是
|
||||
说候选没有走到规格比对就被跳过了。当前循环里存在三条 `continue` 分支,消耗一次
|
||||
attempt 但不增加任何计数器,在设备外部完全不可观测:
|
||||
|
||||
| 位置 | 触发条件 |
|
||||
| --- | --- |
|
||||
| `PinduoduoCandidateAutomation.kt:153` | `!driver.openSpecifications()`,规格弹层打不开 |
|
||||
| `PinduoduoCandidateAutomation.kt:159` | 进入 `ORDER_CONFIRMATION`,返回后跳过 |
|
||||
| `PinduoduoCandidateAutomation.kt:216` | `goodsId in excludedGoodsIds`,跨轮去重跳过 |
|
||||
|
||||
三条路径都会产出完全相同的 `CANDIDATE_TARGET_NOT_FOUND`,因此无法判断应当修复规格
|
||||
入口节点定位、跨轮去重策略,还是结果页排序。这违反「失败要可诊断:不能只返回失败,
|
||||
必须记录步骤、错误码」的产品原则,也阻塞后续修复任务的定位。
|
||||
|
||||
本任务只做可观测性,不改判定阈值,先取得真实计数分布再决定修什么。
|
||||
|
||||
## 关联需求与交互
|
||||
|
||||
- 功能:F-005、F-007
|
||||
- 用户故事:US-004、US-006
|
||||
- 交互:沿用候选搜索进度和任务详情页面,不新增采购操作
|
||||
- 架构/API:复用 T-207 事件 outbox 与既有 `recordCandidateSearchAttempt`,
|
||||
**不修改后端契约,后端零改动**
|
||||
|
||||
## 行为契约
|
||||
|
||||
### 一、轮内诊断计数
|
||||
|
||||
`PinduoduoCandidateAutomation` 必须在单轮生命周期内累计以下计数,并在 `reset()` 中
|
||||
全部清零:
|
||||
|
||||
| 计数 | 递增位置 |
|
||||
| --- | --- |
|
||||
| `specificationOpenFailedCount` | `!driver.openSpecifications()` 分支 |
|
||||
| `orderConfirmationRedirectCount` | `SpecificationEntry.ORDER_CONFIRMATION` 分支 |
|
||||
| `excludedGoodsIdSkipCount` | `goodsId in excludedGoodsIds` 分支 |
|
||||
| `observedCardReads` | 每次 `driver.candidateCards(...)` 返回的卡片累计条数 |
|
||||
| `distinctCardSignatures` | 累计见过的不同 `card.signature` 数量 |
|
||||
|
||||
`observedCardReads` 与 `distinctCardSignatures` 用于检验「卡片签名基于标题哈希导致
|
||||
不同商品签名冲突」这一假设:两者差距显著即为冲突证据。
|
||||
|
||||
既有的 `skuMismatchCount`、`identityFailureCounts`、`resultScrollCount`、
|
||||
`attemptedSignatures.size` 和 `resolvedEvidenceCount()` 保持现有语义,只增加对外读取
|
||||
能力,不改变递增时机。
|
||||
|
||||
计数必须可在 `AutomationResult` 返回后从自动化实例读取(例如只读属性或一个
|
||||
`diagnostics()` 快照方法)。不得为此改变 `AutomationGateway.execute` 的签名。
|
||||
|
||||
### 二、细分失败码
|
||||
|
||||
`WorkflowFailureCode` 新增三个值:
|
||||
|
||||
- `CANDIDATE_SPECIFICATION_UNAVAILABLE`
|
||||
- `CANDIDATE_ORDER_CONFIRMATION_REDIRECT`
|
||||
- `CANDIDATE_ALL_EXCLUDED`
|
||||
|
||||
`terminalCollectionResult()` 的判定改为:
|
||||
|
||||
1. `resolvedEvidenceCount() == maxCandidates` → `Success`(**判定式保持不变**)
|
||||
2. `!sawCandidateCards` → `CANDIDATE_RESULTS_NOT_READY`
|
||||
3. `identityFailureCounts` 非空 → 现有 `terminalIdentityFailureCode()`
|
||||
4. `skuMismatchCount > 0` → `CANDIDATE_SKU_NOT_MATCHED`
|
||||
5. **新增**:三个新计数中取计数最大者对应的码;计数相同时按
|
||||
`specificationOpenFailed` → `orderConfirmationRedirect` → `excludedGoodsIdSkip`
|
||||
固定顺序取第一个
|
||||
6. 三个新计数全为 0 → `CANDIDATE_TARGET_NOT_FOUND`
|
||||
|
||||
第 1 至 4 步的既有语义**一个字都不能改**,新码只允许收窄第 6 步的兜底范围。
|
||||
|
||||
`SearchProbeScreen.kt` 中对 `WorkflowFailureCode` 的 `when` 是穷举的,新增枚举值必须
|
||||
同步补充中文文案,否则无法编译:
|
||||
|
||||
- `CANDIDATE_SPECIFICATION_UNAVAILABLE` → "无法打开商品规格弹层"
|
||||
- `CANDIDATE_ORDER_CONFIRMATION_REDIRECT` → "打开规格时被带入订单确认页"
|
||||
- `CANDIDATE_ALL_EXCLUDED` → "候选均已在既往轮次采集"
|
||||
|
||||
### 三、结构化诊断事件
|
||||
|
||||
`ProcurementRepository.recordCandidateSearchAttempt` 增加一个可选诊断入参(默认空,
|
||||
不传时行为与现在完全一致)。当传入诊断时,在既有 `message` 尾部按**固定顺序**追加
|
||||
以下 `key=value` 段,键名严格照抄,便于事后 grep:
|
||||
|
||||
```
|
||||
cards=<observedCardReads> distinct=<distinctCardSignatures> attempts=<attemptedSignatures.size> scrolls=<resultScrollCount> specOpenFailed=<n> orderConfirmRedirect=<n> excludedSkip=<n> skuMismatch=<n> identityFail=<identityFailureCounts 总和> resolved=<resolvedEvidenceCount()> target=<maxCandidates>
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- 所有值必须是非负整数,缺失时写 `0`,不得写空值或 `null`
|
||||
- `reason` 参数继续受 `EVENT_TOKEN_PATTERN`(`^[A-Za-z0-9_.-]{1,160}$`)约束,
|
||||
**不含空格**;诊断内容只能进 `message`,不能塞进 `reason`
|
||||
- 追加后 `message` 总长必须仍满足后端 `validAuditText` 校验,且不得包含凭证、
|
||||
goods_id、商品标题、店铺名或任何链接
|
||||
|
||||
`MainActivity` 现有的 `recordAutomaticSearchFailure` 调用链必须把自动化实例的诊断
|
||||
快照透传下去,三轮每一轮都要带;成功轮次不强制要求带。
|
||||
|
||||
## 方案
|
||||
|
||||
1. `PinduoduoCandidateAutomation`:新增五个计数字段,在既有三条 `continue` 分支和
|
||||
卡片读取处递增;补 `reset()` 清零;新增只读诊断快照。
|
||||
2. `WorkflowModels.kt`:追加三个枚举值,放在 `CANDIDATE_TARGET_NOT_FOUND` 之前,
|
||||
保持既有值顺序不变。
|
||||
3. `terminalCollectionResult()`:按上述六步重写判定,第 5 步用纯函数实现,便于单测。
|
||||
4. `SearchProbeScreen.kt`:补三条中文文案。
|
||||
5. `ProcurementRepository.recordCandidateSearchAttempt`:增加可选诊断入参与 message
|
||||
拼接,保留原有调用点的默认行为。
|
||||
6. `MainActivity`:在记录轮次事件时透传诊断快照。
|
||||
7. 单测覆盖:五个计数各自的递增路径、第 5 步的最大值选择与并列取序、全零回退
|
||||
`CANDIDATE_TARGET_NOT_FOUND`、诊断 message 拼接格式、不传诊断时 message 不变。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- [x] 规格弹层全部打不开的场景返回 `CANDIDATE_SPECIFICATION_UNAVAILABLE`。
|
||||
- [x] 全部被带入订单确认页的场景返回 `CANDIDATE_ORDER_CONFIRMATION_REDIRECT`。
|
||||
- [x] 候选全部命中 `excludedGoodsIds` 的场景返回 `CANDIDATE_ALL_EXCLUDED`。
|
||||
- [x] 三个新计数全为 0 时仍返回 `CANDIDATE_TARGET_NOT_FOUND`。
|
||||
- [x] `!sawCandidateCards`、身份失败、SKU 不匹配三条既有分类的返回码与本任务前一致,
|
||||
有回归测试证明。
|
||||
- [x] 诊断 message 按固定顺序出现且值为非负整数;不传诊断时 message 与本任务
|
||||
前逐字节一致。
|
||||
- [x] 诊断 message 不含凭证、goods_id、标题、店铺名或链接。
|
||||
- [x] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease`
|
||||
全部通过。
|
||||
- [~] 根 `init.ps1` 通过 —— **不适用**:其流程会构建 Go 后端并产出 `backend-api/bin`,
|
||||
超出本任务 Android-only 的 write scope。起草时照抄通用模板所致,已在执行记录
|
||||
中说明;以五个 Android Gradle 目标作为本任务完整验证口径。
|
||||
- [x] 真机跑一轮采购任务,从 Admin 任务详情读到三轮各自的完整计数分布,把实际数字
|
||||
记入执行记录。
|
||||
|
||||
## 边界
|
||||
|
||||
**判定阈值一律不动**,本任务只增加可观测性:
|
||||
|
||||
- 不改 `terminalCollectionResult()` 的 `resolvedEvidenceCount() == maxCandidates`
|
||||
成功判定式。
|
||||
- 不改 `PROCUREMENT_CANDIDATE_TARGET`、`candidateCollectionReady` 或
|
||||
`MAX_CANDIDATES_PER_PROBE`。
|
||||
- 不改 `PinduoduoImageCandidateRoundPolicy` 的三轮预算、timeout、attempts 和 scrolls。
|
||||
- 不改 `excludedGoodsIds` / `excludedDetailSignatures` 的去重行为本身,只计数。
|
||||
- 不改卡片签名算法 `PinduoduoStableProductIdentityPolicy`。
|
||||
- 不改 T-251 的三轮耗尽终态、T-252/T-253 的商品身份解析与 `ps` 跳转逻辑。
|
||||
- 不改后端任何代码、数据库、API 契约或 Admin 模板。
|
||||
- 不新增第四轮搜索,不改重试策略。
|
||||
- 不降低卡片、身份、颜色、尺码、数量、价格和订单确认页校验。
|
||||
- 不启动真实采购下单,不进入订单提交或支付。
|
||||
- 本轮不统计无障碍层内部被丢弃的卡片容器数(需改
|
||||
`BuyerAccessibilityService.candidateCards`),如计数分布显示签名冲突嫌疑再另开任务。
|
||||
|
||||
## 执行记录
|
||||
|
||||
### 2026-07-30
|
||||
|
||||
- 状态:开工时由 `TODO` 改为 `DOING`;按要求未改为 `DONE`。
|
||||
- 修改:
|
||||
- `PinduoduoCandidateAutomation` 增加规格打开失败、订单确认页跳转、既往商品排除、
|
||||
卡片读取总数、不同卡片签名数五类轮内计数;`reset()` 清零计数,提供只读诊断快照,
|
||||
并按最大值及固定优先级选择三个新增失败码。
|
||||
- `WorkflowFailureCode` 增加
|
||||
`CANDIDATE_SPECIFICATION_UNAVAILABLE`、`CANDIDATE_ORDER_CONFIRMATION_REDIRECT`、
|
||||
`CANDIDATE_ALL_EXCLUDED`;`SearchProbeScreen` 增加对应中文文案。
|
||||
- `ProcurementRepository.recordCandidateSearchAttempt` 增加默认为空的诊断入参,按
|
||||
固定顺序追加 11 个非负整数键;无诊断时沿用原 message。`MainActivity` 将自动候选失败
|
||||
轮的诊断快照透传到事件记录。
|
||||
- 新增自动化计数/失败码、重置、固定并列优先级、诊断 message 格式和默认兼容行为单测。
|
||||
- 未修改 `backend-api`、后端契约、候选阈值、预算、去重行为或最终下单/支付边界。
|
||||
- 命令与结果(工作目录:`D:\chengma\cmroubao\android-buyer`,PowerShell):
|
||||
- `.\gradlew.bat testDebugUnitTest --no-daemon`:首次执行失败,241 项中 1 项为新增测试
|
||||
的 `resolvedEvidenceCount` 预期写成 0;修正断言后再次执行同一命令通过,241 项全部通过。
|
||||
- `.\gradlew.bat lintDebug --no-daemon`:通过。
|
||||
- `.\gradlew.bat assembleDebug --no-daemon`:通过。
|
||||
- `.\gradlew.bat assembleRelease --no-daemon`:通过。
|
||||
- 将 `MainActivity.kt` 规范为 LF 后再次执行上述四条命令:四条均 `BUILD SUCCESSFUL`;
|
||||
`testDebugUnitTest` 为 `UP-TO-DATE`,`lintDebug`、`assembleDebug`、`assembleRelease`
|
||||
均重新完成并通过。
|
||||
- 环境:Windows PowerShell;JDK 17.0.13;Android SDK 34;Gradle Wrapper 8.2。
|
||||
- 决策:诊断字段放在 Android 端 `CandidateSearchDiagnostics`,message 通过固定格式化
|
||||
函数生成;诊断参数默认 `null`,确保既有调用点的 message 逐字节保持不变。
|
||||
- 未验证 / blocker:本轮未连接或操作授权真机,无法完成设备上的三轮搜索和 Admin 详情
|
||||
读取,因此没有可记录的真实计数分布;需在测试设备上完成最终 on-device verification。
|
||||
根 `init.ps1` 未执行:其标准流程会在 `backend-api/bin` 生成后端构建产物,超出本任务
|
||||
明确的 Android-only write scope;本轮已用四条 Android Gradle 命令完成指定验证。
|
||||
|
||||
### 2026-07-30 补充:真机验证完成
|
||||
|
||||
- 补跑 `.\gradlew.bat testReleaseUnitTest --no-daemon`:241 项通过、0 失败,与 Debug
|
||||
一致。此前遗漏是委派指令未列出该目标,任务验收要点要求 Debug/Release 双变体。
|
||||
- 验收要点中的「根 `init.ps1` 通过」与本任务 Android-only 的 write scope 自相矛盾,
|
||||
确认为起草时照抄通用模板所致;以四条 Android Gradle 目标加 `testReleaseUnitTest`
|
||||
作为本任务的完整验证口径,`init.ps1` 不适用。
|
||||
- 通过 USB 在 OnePlus PKG110 上 `adb install -r` 覆盖安装 Debug APK;本地产物与设备
|
||||
`base.apk` 的 SHA-256 均为
|
||||
`42d8baec3e6fb3addc4191919a01d161a175257931ace79e50757f5bb124fed8`。版本号未 bump,
|
||||
仍为 `1.4.18 (23)`,故新旧只能以 SHA-256 区分。覆盖安装后采购无障碍仍在启用列表,
|
||||
`accessibility_enabled=1`,拼多多为 `8.17.0`。
|
||||
- 真机跑通一条采购任务的完整三轮,Admin 任务详情「执行审计」读到三轮完整计数分布:
|
||||
|
||||
| 轮次 | cards | distinct | attempts | scrolls | specOpenFailed | orderConfirmRedirect | excludedSkip | skuMismatch | identityFail | resolved | target | reason |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 2 | 1 | 1 | 4 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | `CANDIDATE_TARGET_NOT_FOUND` |
|
||||
| 2 | 2 | 1 | 1 | 4 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | `CANDIDATE_ALL_EXCLUDED` |
|
||||
| 3 | 2 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 2 | `CANDIDATE_ALL_EXCLUDED` |
|
||||
|
||||
- 诊断结论:三条静默跳过路径全部为 0,规格弹层可打开、身份解析正常、SKU 匹配无失败。
|
||||
真实瓶颈在更上游——`distinct=1` 表明整轮只识别出一个候选卡片,且滚动后读取归零
|
||||
(`cards=2` 意味着 5 次读取中 4 次返回空)。第 2、3 轮重复命中同一商品并被跨轮去重
|
||||
排除,必然为 0。第 1 轮的 `resolved=1` 是完全合格候选,但因 `target=2` 未达标而整体
|
||||
判失败,未提交给 Admin。
|
||||
- 后续任务:`T-255` 为卡片过滤器九个条件增加拒绝计数并观测滚动后 recycler 可识别性;
|
||||
`T-256` 将「凑不足 target 即整体失败」改为「至少一个有效候选即进入人工确认」。
|
||||
- 本任务目标已达成:细分失败码成功把原兜底码拆分为
|
||||
`CANDIDATE_TARGET_NOT_FOUND` 与 `CANDIDATE_ALL_EXCLUDED`,并用计数分布否证了三条
|
||||
静默路径假设,定位到真实瓶颈。
|
||||
Reference in New Issue
Block a user