1
T-254
ila edited this page 2026-08-07 16:37:01 +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-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 不变。

验收要点

  • 规格弹层全部打不开的场景返回 CANDIDATE_SPECIFICATION_UNAVAILABLE。
  • 全部被带入订单确认页的场景返回 CANDIDATE_ORDER_CONFIRMATION_REDIRECT。
  • 候选全部命中 excludedGoodsIds 的场景返回 CANDIDATE_ALL_EXCLUDED。
  • 三个新计数全为 0 时仍返回 CANDIDATE_TARGET_NOT_FOUND。
  • !sawCandidateCards、身份失败、SKU 不匹配三条既有分类的返回码与本任务前一致, 有回归测试证明。
  • 诊断 message 按固定顺序出现且值为非负整数;不传诊断时 message 与本任务 前逐字节一致。
  • 诊断 message 不含凭证、goods_id、标题、店铺名或链接。
  • Android Debug/Release 单测、lintDebug、assembleDebug、assembleRelease 全部通过。
  • [~] 根 init.ps1 通过 —— 不适用:其流程会构建 Go 后端并产出 backend-api/bin, 超出本任务 Android-only 的 write scope。起草时照抄通用模板所致,已在执行记录 中说明;以五个 Android Gradle 目标作为本任务完整验证口径。
  • 真机跑一轮采购任务,从 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,并用计数分布否证了三条 静默路径假设,定位到真实瓶颈。