diff --git a/T-261.-.md b/T-261.-.md new file mode 100644 index 0000000..8af5777 --- /dev/null +++ b/T-261.-.md @@ -0,0 +1,242 @@ + +> 同步来源:[`docs/tasks/T-261.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-261.md) · commit `afc651f75a3a` + +--- +id: T-261 +title: 失败后清理拼多多现场并细分规格读取失败原因 +phase: 2 +deps: + - T-259 + - T-260 +status: DOING +created: 2026-07-31 +context_ref: c15f436 +work_branch: null +write_paths: + - docs/tasks/T-261.md + - docs/current-state.md + - android-buyer/app/src/main/** + - android-buyer/app/src/test/** +--- + +## 问题 / 背景 + +真机任务三轮全败,且**后两轮的失败是第一轮的后果**: + +| 轮次 | state | step | reason | 关键计数 | +| --- | --- | --- | --- | --- | +| 1 | FAILED | `pdd_browse_candidates` | `EVIDENCE_CAPTURE_FAILED` | `cards=1 attempts=1 resolved=0 priceTexts=0 specOpenFailed=0 orderConfirmRedirect=0` | +| 2 | BLOCKED | `pdd_open_app` | `PAYMENT_BOUNDARY` | 全部为 0,`containers=0` | +| 3 | BLOCKED | `pdd_open_app` | `PAYMENT_BOUNDARY` | 全部为 0,`containers=0` | + +### 第一轮:失败后不清理现场 + +```kotlin +val specificationEvidence = collectSpecificationEvidence() + ?: return AutomationResult.FatalFailure(EVIDENCE_CAPTURE_FAILED) // 直接返回 +``` + +`browseCandidates` 中五处 `EVIDENCE_CAPTURE_FAILED` 返回点**全都不做任何清理**——不关规格 +弹层、不返回结果页。拼多多被留在失败时所处的页面。 + +### 后两轮:被残留现场卡死在第一步 + +失败发生在 `pdd_open_app`,`containers=0` 表示什么都没做就被拦。判定来自: + +```kotlin +if (hasCheckoutRoot || visibleTexts.containsAny(paymentMarkers)) → PAYMENT_BOUNDARY +``` + +安全机制本身工作正常——它拒绝在疑似结算页面上操作。问题是**没有恢复路径**:残留现场 +不会自行消失,因此后续每一轮都必然重复失败。 + +`PinduoduoCandidateAutomation.recoverDetailPageIfNeeded()` 确实有轮次开始时的现场恢复, +但它在 `browseCandidates` 内部,而阻塞发生在更早的 `pdd_open_app` 步骤,根本没机会执行。 +且它第一行就是 `safetyResult(snapshot)?.let { return it }`,遇到支付边界会直接返回而不 +清理。 + +### 推断:第一轮很可能进入了订单确认页 + +`paymentMarkers` 为「确认订单、提交订单、确认支付、立即支付、收银台、微信支付、 +找好友支付、更多支付方式」,**不含「立即购买」**。单纯停留在规格弹层不会触发 +`PAYMENT_BOUNDARY`。 + +结合第一轮 `specOpenFailed=0`(弹层已打开)、`orderConfirmRedirect=0`(入口处未走订单 +确认分支)、`priceTexts=0`(解析器一次证据都没产出),最合理的解释是: + +**`collectSpecificationEvidence` 过程中的选规格点击把页面带进了订单确认页**,此后 +`readSpecifications()` 返回 null(已不是规格面板)→ observations 为空 → +`merge` 返回 null → `EVIDENCE_CAPTURE_FAILED` → 无清理返回 → 拼多多滞留在结算页。 + +**这是推断,需要本任务的细分诊断确认。** 若成立,说明自动化会在非预期路径下到达订单 +确认页——虽然从未提交订单,安全边界未被突破,但不应到达该页面。 + +### 一个需要单独留意的信号 + +同一份代码、同一台设备,本次 `containers=11 cards=1`,而此前成功运行是 +`containers=60~63 cards=4~6`。无障碍树可见节点数下降约 80%。这不是代码变化,属于页面 +或账号侧差异,**不在本任务范围**,但需记录以便后续排查。 + +## 关联需求与交互 + +- 功能:F-005、F-007 +- 用户故事:US-004、US-006 +- 交互:沿用候选搜索进度与任务详情,不新增采购操作 +- 架构/API:沿用 T-254/T-255/T-257 诊断机制,**后端零改动** + +## 行为契约 + +### 一、技术性失败必须清理现场 + +`browseCandidates` 中因**技术原因**退出前(规格读取失败、证据保存失败、候选保存失败 +等),必须尝试把拼多多恢复到结果页:关闭规格弹层 → 返回商品详情 → 返回结果页。 + +约束: + +- 清理是**尽力而为**,失败不改变返回值——**始终返回原始失败码**,不得用清理结果 + 覆盖或掩盖真实原因。 +- 清理动作有界,不得循环重试;沿用既有 `closeSpecifications` / `returnToResults` + 能力,不新增点击目标。 +- 清理动作同样受 T-260 的 pacer 限速。 + +### 二、安全停止绝不清理 + +**这是硬边界。** 因验证码、登录失效、风控提示、未知页面或 `PAYMENT_BOUNDARY` 而停止 +时,**不得执行任何清理点击**,维持现有立即停止行为。 + +清理过程中每一步动作前必须重新取快照检查安全状态;一旦出现上述任一情况,立即中止 +清理并返回原始安全停止结果。 + +不得为清理而点击任何前进方向的控件,只允许关闭弹层和返回。 + +### 三、订单确认页的有界退出 + +若技术性失败时页面已处于订单确认页,允许调用既有 +`returnFromOrderConfirmation()` **最多一次**退出该页,随后继续常规清理。 + +失败则返回 `AutomationResult.Blocked(SafetyStopReason.PAYMENT_BOUNDARY)`,与现有行为 +一致。**绝不点击提交订单、立即支付或任何扣款语义控件。** + +### 四、细分规格读取失败 + +新增 `WorkflowFailureCode` 值区分三种当前都归为 `EVIDENCE_CAPTURE_FAILED` 的情况: + +| 码 | 含义 | +| --- | --- | +| `CANDIDATE_SPECIFICATION_UNREADABLE` | 分类器判定为规格面板,但解析器未产出任何证据 | +| `CANDIDATE_SPECIFICATION_NAVIGATED_AWAY` | 读取过程中页面已离开规格面板(含进入订单确认页) | +| `EVIDENCE_CAPTURE_FAILED` | 保留原义:截图或证据保存本身失败 | + +判定依据取自读取失败当时的页面快照,不得依赖猜测。 + +`SearchProbeScreen.kt` 的 `when` 是穷举的,新增枚举值必须同步补中文文案: + +- `CANDIDATE_SPECIFICATION_UNREADABLE` → "规格面板无法解析" +- `CANDIDATE_SPECIFICATION_NAVIGATED_AWAY` → "读取规格时页面已跳转" + +### 五、诊断可见 + +并入 `CandidateSearchDiagnostics`,追加在既有键之后: + +``` +specUnreadable= specNavigatedAway= cleanupAttempts= cleanupFailures= +``` + +沿用既有脱敏口径:只记计数,不记页面文本、商品标题、店铺名、goods_id、链接或价格。 + +### 六、既有行为不变 + +- 不改 `PinduoduoPageClassifier` 的任何判定条件、`paymentMarkers` 或规格面板识别规则。 +- 不改 T-254/T-255/T-257/T-260 既有诊断键的顺序、名称与取值。 +- 不改成功判定式、三轮预算、去重、SKU 硬约束或价格解析。 +- 不改下单 dry-run、订单提交、授权价格策略或支付边界。 + +## 方案 + +1. 抽出 `recoverToResultsPage()`:有界、每步前重新检查安全状态、只做关闭与返回。 +2. 五处 `EVIDENCE_CAPTURE_FAILED` 及其他技术性退出点改为「先清理再返回原始失败码」。 +3. 安全停止退出点保持原样,明确不接入清理。 +4. 规格读取失败处按当时页面快照细分三种失败码。 +5. `WorkflowModels.kt` 追加两个枚举值,`SearchProbeScreen.kt` 补文案。 +6. 四个新计数并入诊断快照与 message 尾部,`reset()` 清零。 +7. 单测覆盖:技术性失败会清理且返回原始码、安全停止不清理、清理中途出现风控立即中止、 + 订单确认页最多退出一次、三种规格失败码各自判定、清理失败不改变返回值、诊断键格式。 + +## 验收要点 + +- [ ] 规格读取失败后会尝试清理并仍返回原始失败码,有单测证明。 +- [ ] 因验证码、风控、登录失效、未知页面或 `PAYMENT_BOUNDARY` 停止时**不执行任何清理 + 点击**,有单测证明。 +- [ ] 清理过程中出现风控或支付边界时立即中止并返回原始安全停止结果。 +- [ ] 订单确认页最多调用一次 `returnFromOrderConfirmation`,失败返回 + `Blocked(PAYMENT_BOUNDARY)`。 +- [ ] 清理路径不调用任何提交订单、立即支付或扣款语义控件,有单测证明。 +- [ ] 三种规格读取失败码按页面快照正确区分,各有单测。 +- [ ] `SearchProbeScreen` 补齐新枚举文案且可编译。 +- [ ] 诊断 message 出现四个新键且不含页面文本或商品信息。 +- [ ] T-254/T-255/T-257/T-260 既有诊断键顺序、名称、取值不变。 +- [ ] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease` 全部通过。 +- [ ] 真机验证:构造一次规格读取失败后,后续轮次**不再**因残留现场被 + `PAYMENT_BOUNDARY` 拦在 `pdd_open_app`;并从 Admin 读到细分失败码确认第一轮的 + 真实原因。 + +## 边界 + +- **不放松任何安全停止**:验证码、登录失效、风控、未知页面、支付边界的判定和立即停止 + 行为一律不改。 +- 不改 `PinduoduoPageClassifier` 的判定条件、`paymentMarkers` 或规格面板识别规则。 +- 不为清理而点击任何前进方向控件,不点击提交订单或支付。 +- 不新增第四轮搜索,不改三轮门禁、预算或成功判定式。 +- 不改候选卡片识别、签名算法、跨轮去重、SKU 硬约束或价格解析。 +- 不改 T-256 提交门槛、T-258 后端校验、T-259 安全停止补报、T-260 节奏配置。 +- 不改后端代码、数据库、API 契约或 Admin 模板。 +- 不处理 `containers` 由 60+ 降至 11 的页面/账号侧差异,另行排查。 + +## 执行记录 + +### 2026-07-31 实现与自动化验证 + +- 环境:Windows PowerShell,仓库根目录 + `D:\chengma\cmroubao`;Android 命令在 `android-buyer` 执行。 +- 状态:开工时从 `TODO` 改为 `DOING`。保持 `DOING`,未执行真机验收。 +- 修改: + - `PinduoduoCandidateAutomation` 为五处证据读取/保存技术失败接入有界清理;清理只会 + 调用既有 `closeSpecifications` 和 `returnToResults`,每个动作前重新读取安全快照。 + - 安全停止不进入清理;清理途中出现登录、验证码、风控、未知页、支付边界或非拼多多 + 前台时立即中止,不再发出动作。 + - 清理是尽力而为;动作失败、快照异常或安全边界出现都只增加失败计数,不覆盖最初 + `WorkflowFailureCode`。这是本轮用户明确的 CRITICAL SAFETY 3,优先于任务正文中 + “清理失败改为安全停止结果”的旧表述。 + - 规格读取失败时按失败快照新增 + `CANDIDATE_SPECIFICATION_UNREADABLE` 和 + `CANDIDATE_SPECIFICATION_NAVIGATED_AWAY`;真实安全停止仍保持原有 + `Blocked` 立即停止行为。 + - `SearchProbeScreen` 补充两个中文失败文案;诊断尾部追加 + `specUnreadable`、`specNavigatedAway`、`cleanupAttempts`、 + `cleanupFailures`,既有键顺序、名称和值不变。 + - 新增单测覆盖五类安全停止零清理、真实未知页零清理、规格面板不可解析、读取后跳页、 + 读取时进入支付边界零点击、技术失败回到结果页、清理中出现风控/支付边界立即中止、 + 清理失败保留原码、候选保存失败清理、清理动作白名单和诊断格式/脱敏。 +- 安全决策:按本轮用户 CRITICAL SAFETY 2,技术失败后的清理若快照已经是 + `ORDER_CONFIRMATION/PAYMENT_BOUNDARY`,不调用 + `returnFromOrderConfirmation`,而是零点击中止清理;既有“打开规格直接跳入订单确认页” + 的非清理路径及其最多一次返回测试保持不变。 +- 定向测试命令与结果: + - `.\gradlew.bat testDebugUnitTest --tests "com.roubao.autopilot.pinduoduo.PinduoduoCandidateAutomationTest" --tests "com.roubao.autopilot.procurement.CandidateSearchDiagnosticsTest" --no-daemon` + - 首次:失败,测试文件第 1105 行存在多余右花括号;生产 Debug Kotlin 编译已通过。 + - 修正后:通过。 + - 补充候选保存、未知页和清理中支付边界覆盖后再次运行:通过。 +- 要求的五项验证命令与结果: + - `.\gradlew.bat testDebugUnitTest --no-daemon`:通过;最终 282 项测试,0 失败、0 错误、 + 0 跳过。 + - `.\gradlew.bat testReleaseUnitTest --no-daemon`:通过;最终 282 项测试,0 失败、0 错误、 + 0 跳过。 + - `.\gradlew.bat lintDebug --no-daemon`:通过。 + - `.\gradlew.bat assembleDebug --no-daemon`:通过。 + - `.\gradlew.bat assembleRelease --no-daemon`:通过,含 `lintVitalRelease`。 +- 未运行:按任务边界未运行 `init.ps1`;未运行任何 Go/backend 命令;未提交、未推送。 +- 未修改:`PinduoduoPageClassifier`、`paymentMarkers`、规格面板识别规则、 + `backend-api` 及所有 `write_paths` 外文件。 +- Blocker:无法在本环境执行最终真机验收。仍需在测试设备构造一次规格读取技术失败, + 确认后续轮次不再因残留现场阻塞于 `pdd_open_app/PAYMENT_BOUNDARY`,并从 Admin 核对 + 细分失败码和四个新诊断计数。