diff --git a/T-264.-.md b/T-264.-.md new file mode 100644 index 0000000..57f60f3 --- /dev/null +++ b/T-264.-.md @@ -0,0 +1,386 @@ + +> 同步来源:[`docs/tasks/T-264.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-264.md) · commit `afc651f75a3a` + +--- +id: T-264 +title: 停止以购买按钮作为规格入口并识别原生订单确认页 +phase: 2 +deps: + - T-263 +status: DOING +created: 2026-07-31 +context_ref: 3b04fb0 +work_branch: null +write_paths: + - docs/tasks/T-264.md + - docs/current-state.md + - android-buyer/app/src/main/** + - android-buyer/app/src/test/** +--- + +## 问题 / 背景 + +T-263 上机后,第 2 轮**首次成功进入候选采集**(`cards=3 distinct=2 attempts=2`, +`payTextSuppressed=1` 证明文本抑制生效)。但随后自动化**走进了真实的订单确认页并卡死 +在那里**,第 3 轮开 App 即被拦。 + +设备取证显示当时页面内容为:收件人姓名、掩码手机号、详细收货地址、 +`已选:#心相印 新品【4包 400抽】…`、数量 `1`、`已包邮`、`仅1次¥0.01`、`¥8.01`,以及 +`使用#微信支付,更换先用后付可0元下单`。**这是一个原生订单确认页**(147 节点、 +26 条文本、**0 个 WebView**),商品是纸巾——图片搜索结果中混入的非服装品类。 + +### 因果链 + +**第一处缺口:找不到规格入口时会去点购买按钮** + +`BuyerAccessibilityService.openPinduoduoSpecifications()`: + +```kotlin +val target = directTargets.singleOrNull() // 选择规格/颜色分类/已选:… + ?: uniqueClickableTargets(... endsWith("免拼购买")).singleOrNull() // 兜底点购买 + ?: return@withPinduoduoRoot false +``` + +纸巾没有颜色/尺码规格,详情页缺少安全入口文本(或存在多个导致 `singleOrNull()` 为 +null),于是落入兜底分支点击「免拼购买」。正常商品点击后会先弹出规格面板,但该商品 +为 `仅1次¥0.01` 促销且启用先用后付,**点击后直接跳转到订单确认页,跳过了规格面板**。 + +**第二处缺口:该订单确认页识别不出来** + +`PinduoduoPageClassifier` 的 `hasOrderConfirmation` 三个判据全部落空: + +| 判据 | 实际 | +| --- | --- | +| `hasCheckoutRoot`(`order_checkout`) | 未命中 | +| 存在文本恰为「确认订单」 | 页面无此文本 | +| 含「收货地址」+ 两个支付方式标记 + 「实付款¥」前缀 | 只有地址内容而无「收货地址」标签;价格为「¥8.01」而非「实付款¥…」 | + +因此 `page != ORDER_CONFIRMATION`。而 `awaitSpecificationEntry()` 的判断顺序是: + +```kotlin +if (page == SPECIFICATION_PANEL) return PANEL +if (page == ORDER_CONFIRMATION) return ORDER_CONFIRMATION // 走不到 +safetyResult(snapshot)?.let { return FAILED(it) } // 落到这里 +``` + +页面认不出来,**既有的 `returnFromOrderConfirmation()` 退出路径根本没有机会执行**—— +这正是「卡在那里出不来」的直接原因,也解释了 `orderConfirmRedirect=0`。 + +最终由「微信支付」促销横幅触发 `PAYMENT_BOUNDARY` 拦下。**当前防线只剩这一条促销 +文案**,而该文案随时可能变更。 + +### 风险评估 + +自动化在非预期路径下到达了订单确认页,页面上是 `仅1次¥0.01` 促销价并启用先用后付。 +虽然从未点击提交、安全边界未被突破,但**距离产生一笔真实订单只差一次点击**,且 +拦截依赖的是一条营销文案而非稳定特征。 + +## 关联需求与交互 + +- 功能:F-005、F-007 +- 用户故事:US-004、US-006 +- 交互:不新增采购操作 +- 架构/API:沿用既有诊断机制,**后端零改动** + +## 行为契约 + +### 一、不得以购买类控件作为规格入口 + +移除 `openPinduoduoSpecifications()` 中以「免拼购买」为兜底点击目标的分支。 + +找不到安全规格入口(`SAFE_ENTRY_TEXTS` / `SAFE_ENTRY_PREFIXES`)时**直接返回 false**, +由既有 `specOpenFailed` 路径跳过该候选,**不得点击任何购买、下单、拼单、支付语义的 +控件**。 + +新增诊断计数 `specEntryAbsent`,记录因缺少安全入口而放弃的候选数,用于评估本次收紧 +造成的覆盖损失。若真机上该值偏高,说明有相当比例的正常商品依赖该兜底,需另开任务以 +更安全的方式补回,**但不得退回点击购买按钮**。 + +### 二、识别原生订单确认页 + +`hasOrderConfirmation` **追加**一个判据(现有三个判据一律保留): + +同时满足即判定为订单确认页: + +1. 存在掩码手机号形态的文本(如 `\d{3}\*{4}\d{4}`); +2. 存在以 `已选` 开头的文本; +3. 存在数量或运费语义(如纯数字数量、`已包邮`、`运费`)。 + +依据:掩码手机号仅出现在涉及收件信息的页面,商品详情页不会出现;三者同时出现足以与 +商品详情页、规格弹层区分。 + +**隐私硬约束**:仅做**形态匹配**用于判定,**绝不记录**收件人姓名、手机号(含掩码 +形式)、详细地址或任何原文,不得进入诊断 message、事件、日志或截图元数据。依据业务 +规则 29。 + +识别成功后,既有 `awaitSpecificationEntry()` 的 `ORDER_CONFIRMATION` 分支即可生效, +调用既有 `returnFromOrderConfirmation()` 安全退出,解决「卡住出不来」。 + +### 三、记录支付边界触发时的页面类型 + +T-262 的归因只记了命中哪个标记,未记当时页面类型,导致本次必须手动 dump 才能定位。 + +`PaymentBoundaryAttribution` 追加页面类型,诊断 message 追加: + +``` +payPage= +``` + +取值为本项目自定义枚举的名称(ASCII,如 `UNKNOWN`、`PRODUCT_DETAIL`、 +`ORDER_CONFIRMATION`),**不是页面内容**,因此可以记录。未触发支付边界时记 `NONE`。 + +### 四、安全边界一律不放松 + +- 不得削弱 `PAYMENT_BOUNDARY` 的任何判据:T-263 结构判据、`hasCheckoutRoot`、 + `paymentMarkers` 文本判据及其抑制规则全部保持现状。 +- 不得点击提交订单、立即支付、免密支付、先用后付或任何扣款语义控件。 +- 退出订单确认页只允许使用既有 `returnFromOrderConfirmation()`,且沿用其现有的有界 + 次数与失败即 `Blocked(PAYMENT_BOUNDARY)` 行为。 +- 登录、验证码、风控三类安全停止的判定与优先级完全不变。 + +### 五、既有诊断不变 + +T-254/T-255/T-257/T-260/T-261/T-262/T-263 既有诊断键的顺序、名称与取值**逐字节不变**, +新键一律追加在末尾。 + +## 方案 + +1. `openPinduoduoSpecifications()` 删除「免拼购买」兜底分支,缺少安全入口时返回 false。 +2. 候选自动化统计 `specEntryAbsent`,并入诊断快照与 message 尾部。 +3. `PinduoduoPageClassifier` 为 `hasOrderConfirmation` 追加原生结算页判据;掩码手机号 + 等只做形态匹配,不留存原文。 +4. `PaymentBoundaryAttribution` 追加页面类型字段,诊断追加 `payPage` 键。 +5. 单测覆盖:无安全入口时不点击任何购买控件且计数递增;原生结算页样本被识别为 + `ORDER_CONFIRMATION`;商品详情页与规格弹层**不得**被新判据误判;掩码号码不出现在 + 任何诊断输出中;`payPage` 取值正确且未触发时为 `NONE`;既有支付边界判据未削弱。 + +## 验收要点 + +- [ ] 详情页无安全规格入口时返回 false,且**不点击任何购买/下单/支付语义控件**,有 + 单测证明。 +- [ ] `specEntryAbsent` 计数正确并出现在诊断 message。 +- [ ] 用真机取证的原生结算页特征构造夹具,被识别为 `ORDER_CONFIRMATION`。 +- [ ] 商品详情页、规格弹层、搜索结果页**不被**新判据误判为订单确认页,有回归测试。 +- [ ] 掩码手机号、收件人、地址不出现在诊断 message、事件或日志中,有单测证明。 +- [ ] `payPage` 出现在诊断 message,未触发支付边界时为 `NONE`。 +- [ ] T-263 结构判据、`hasCheckoutRoot`、`paymentMarkers` 及抑制规则未被削弱,有回归 + 测试。 +- [ ] 登录、验证码、风控判定与优先级未变。 +- [ ] 既有诊断键顺序、名称、取值不变。 +- [ ] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease` 全部通过。 +- [ ] 真机验证:同一任务重跑时,遇到无颜色/尺码规格的商品**不再进入订单确认页**; + 若仍进入,应能被识别并自动退出而非卡死;从 Admin 读取 `specEntryAbsent` 与 + `payPage` 记入执行记录。 + +## 边界 + +- **不放松任何支付边界判据**,只增加订单确认页的识别能力。 +- 不点击提交订单、立即支付、先用后付或任何扣款语义控件。 +- 不改 `returnFromOrderConfirmation()` 的既有实现与有界次数。 +- 不改登录、验证码、风控的检测逻辑与优先级。 +- 不改 T-261 清理逻辑、T-262 归因既有五键、T-263 结构判据与抑制名单。 +- 不改 T-256 提交门槛、T-258 后端校验、T-259 安全停止补报、T-260 节奏配置。 +- 不改候选卡片识别、签名算法、跨轮去重、价格解析或三轮预算。 +- 不改后端代码、数据库、API 契约或 Admin 模板。 +- 不处理「图片搜索结果混入非目标品类」本身——那需要在候选筛选阶段引入品类判断, + 另行开任务。 +- 不处理第 1 轮 `containers=550` 却 0 卡片的识别问题,另行开任务。 + +## 执行记录 + +开工后记录修改、命令、结果、环境、决策和 blocker。 + +### 2026-07-31 实现与自动化验证 + +环境:WSL 无 Android SDK/JDK,全部构建与单测经 +`/mnt/c/Windows/System32/cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat <目标> --no-daemon --console=plain"` +在 Windows 侧执行;未运行 `init.ps1`。 + +#### 改动文件 + +- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoCandidateModels.kt`: + 新增 `PinduoduoSpecificationOpenOutcome` 枚举(`OPENED`/`ENTRY_ABSENT`/ + `UNAVAILABLE`),`PinduoduoCandidateDriver.openSpecifications()` 的返回类型 + 由 `Boolean` 改为该枚举,用于向候选自动化区分「找不到唯一安全入口」与其它 + 不可用情形。仅此接口改动;`PinduoduoOrderDryRunDriver`(下单 dry-run 专用 + 的独立接口,定义于 `PinduoduoOrderDryRunAutomation.kt`)保持 + `openSpecifications(): Boolean` 不变,未受影响。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityService.kt`: + `openPinduoduoSpecifications()` 删除「找不到 `directTargets.singleOrNull()` + 就点击以『免拼购买』结尾的兜底控件」分支(连同私有常量 + `SPECIFICATION_PURCHASE_ENTRY`),改为纯投影——新增 + `openPinduoduoSpecificationsDetailed(): PinduoduoSpecificationOpenOutcome` + 作为唯一的点击实现:未验证到商品详情页时返回 `UNAVAILABLE`; + `directTargets.singleOrNull()` 为空(找不到唯一安全入口,含零个或多个两种 + 情形)时返回 `ENTRY_ABSENT`,**不再有任何兜底点击**;点击成功返回 + `OPENED`,点击本身失败返回 `UNAVAILABLE`。`openPinduoduoSpecifications(): + Boolean` 改为 `openPinduoduoSpecificationsDetailed() == + PinduoduoSpecificationOpenOutcome.OPENED` 的布尔投影,供既有下单 dry-run + 驱动(经 `BuyerAccessibilityBridge.openSpecifications()`)复用同一份点击 + 逻辑,不重复点击、不改变其行为。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityBridge.kt`: + 新增 `openSpecificationsDetailed(): PinduoduoSpecificationOpenOutcome`, + 专供候选驱动使用;既有 `openSpecifications(): Boolean`(下单 dry-run 驱动 + 使用)逐字节未动。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/AndroidPinduoduoCandidateDriver.kt`: + `openSpecifications()` 改为调用 `BuyerAccessibilityBridge.openSpecificationsDetailed()` + 并保留既有的动作节奏停顿(`actionPacer.pause()`),返回值改为枚举。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoCandidateAutomation.kt`: + 新增会话内累计字段 `specEntryAbsentCount`(`reset()` 一并清零);打开规格 + 处的 `if (!driver.openSpecifications())` 改为对三态枚举的 `when`—— + `OPENED` 正常继续;`ENTRY_ABSENT` 与 `UNAVAILABLE` 都仍使 + `specificationOpenFailedCount`(既有 `specOpenFailed` 口径)递增并触发既有 + `returnToResultsAfterUnsupportedCandidate()` 安全返回,`ENTRY_ABSENT` 额外 + 使 `specEntryAbsentCount` 递增;`diagnostics()` 追加 + `specEntryAbsent = specEntryAbsentCount`。除新增分支外,候选采集的其余判定 + /重试/清理逻辑未动。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoPageClassifier.kt`: + - `hasOrderConfirmation` 追加第四个判据 `hasNativeOrderConfirmation`(既有 + 三个判据逐字节未动,新判据用 `||` 追加):同时满足「存在掩码手机号形态 + 文本(`\d{3}\*{4}\d{4}`,新增私有常量 `MASKED_PHONE_PATTERN`)」「存在以 + `已选` 开头的文本」「存在数量或运费语义(纯数字文本、`已包邮`、包含 + `运费`)」三者才成立;只产出布尔量,不留存任何原文。 + - `computePaymentBoundaryAttribution()` 追加入参 `page: PinduoduoPage`, + 写入 `PaymentBoundaryAttribution.page = page.name`(本项目自定义枚举的 + ASCII 名称,不是页面内容);`classify()` 调用处相应传入已计算好的 + `page` 局部变量,未改变 `page` 本身的判定逻辑(仅追加一次赋值读取,不重 + 排既有计算顺序之外的任何语句)。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/workflow/WorkflowModels.kt`: + `PaymentBoundaryAttribution` 追加字段 `page: String = "NONE"`(末尾追加, + 既有八个字段——含 T-263 追加的三个——顺序/含义未动),`init` 增加 + `PAGE_NAME_PATTERN = Regex("^[A-Z_]+$")` 校验。选择 `String` 而非直接引用 + `PinduoduoPage` 类型,是为了保持既有的单向依赖方向(`pinduoduo` 包依赖 + `workflow` 包,若反过来会让 `workflow.AutomationResult.Blocked`/ + `WorkflowReport` 反向依赖 `pinduoduo`,与 T-262 已记录的架构决策相反)。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/procurement/CandidateSearchDiagnostics.kt`: + 末尾追加 `specEntryAbsent: Int = 0`、`payPage: String = "NONE"` 两个字段 + (既有字段顺序/含义未动)及对应 `require` 校验(含 `PAGE_NAME_PATTERN`); + `withPaymentBoundaryAttribution()` 追加 `payPage = attribution.page`(逐次 + 覆盖,与 `payCheckoutRoot` 等字段一致,不同于 `payStructureStreak` 的取大 + 合并);`auditSuffix()` 在既有 `payTextSuppressed=` 之后追加 + `specEntryAbsent=`、`payPage=` 两个键,此前所有键的名称/顺序/取值逐字节 + 未动。 +- 测试: + - `PinduoduoPageClassifierTest.kt` 新增 9 个用例:真机取证的原生结算页 + 样本(掩码手机号 `199****5018` + `已选:` 摘要 + 数量/`已包邮` + 微信支付 + 促销横幅)被识别为 `ORDER_CONFIRMATION` 且 + `paymentBoundaryAttribution.page == "ORDER_CONFIRMATION"`;三个条件单独 + 缺失任一个都不触发(掩码手机号单独存在、已选摘要+数量但无掩码手机号); + 商品详情页、规格弹层、搜索结果页在混入「已选:」摘要与纯数字文本但没有 + 掩码手机号时**不被**新判据误判为订单确认页(三个回归用例);归因 + `toString()` 不含收件人姓名/掩码手机号/地址等测试用合成占位文本。 + - `PinduoduoSpecificationParserTest.kt` 新增 1 个用例:穷举「免拼购买」 + 「立即购买」「确认订单」「提交订单」「去支付」「立即支付」「下单」 + 「加入购物车」「去结算」等购买/下单/拼单/支付语义文本,逐一断言 + `isSafeEntryText()` 均返回 `false`(`SAFE_ENTRY_TEXTS`/ + `SAFE_ENTRY_PREFIXES` 是纯 allowlist,不含任何交易语义词,这是移除 + 兜底分支后唯一的安全入口判据来源)。 + - `PinduoduoCandidateAutomationTest.kt`:`FakeCandidateDriver` 的 + `openSpecifications()` 改为返回枚举,新增可选构造参数 + `openSpecificationsEntryAbsent`;既有「unsupported specification entry + skips candidate safely」用例追加断言 `specEntryAbsent == 0`(证明通用 + 不可用不会误计入该键);新增 1 个用例覆盖 `ENTRY_ABSENT` 路径——断言 + `specificationOpenFailedCount`/`specEntryAbsent` 均为 1,且驱动侧唯一 + 发生的调用只有 `openSpecifications` 与 `returnToResults`(` + PinduoduoCandidateDriver` 接口本身没有任何购买/下单/拼单/支付语义方法可 + 供调用,找不到安全入口时不可能触发此类点击),`reset()` 后归零。 + - `CandidateSearchDiagnosticsTest.kt`:更新 8 处既有精确匹配/`endsWith` + 断言以包含 `specEntryAbsent=0 payPage=NONE`(或触发场景下的实际值)尾部 + 键;新增 4 个用例覆盖 `specEntryAbsent` 默认值与非负校验、`payPage` + 默认值与逐次覆盖(非取大合并)语义、`payPage` 格式非法值(小写/空串) + 拒绝。 + - `WorkflowRunnerTest.kt`:既有「payment boundary attribution is carried + through to the report」用例追加 `page = "ORDER_CONFIRMATION"` 构造参数与 + 对应断言;新增 2 个用例覆盖 `PaymentBoundaryAttribution.page` 默认值 + `"NONE"` 与格式非法值(小写/空串)拒绝。 + +#### 命令与结果 + +``` +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testDebugUnitTest --no-daemon --console=plain" +BUILD SUCCESSFUL;Debug 单测 329 项,0 失败、0 错误(较 T-263 的 313 项新增 16 项)。 + +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testReleaseUnitTest --no-daemon --console=plain" +BUILD SUCCESSFUL;Release 单测 329 项,0 失败、0 错误。 + +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat lintDebug --no-daemon --console=plain" +BUILD SUCCESSFUL。 + +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat assembleDebug --no-daemon --console=plain" +BUILD SUCCESSFUL。 + +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat assembleRelease --no-daemon --console=plain" +BUILD SUCCESSFUL。 +``` + +五个目标全部通过。 + +调试过程中曾出现一次编译错误:`return@withPinduoduoRoot` 与其返回值分写在 +两行时,Kotlin 把裸 `return@label`(换行后另起一句)解析为返回 `Unit`,导致 +`openPinduoduoSpecificationsDetailed()` 类型不匹配。修正为将返回值与 +`return@withPinduoduoRoot` 放在同一行后编译通过,不影响任何行为逻辑。 + +#### 偏离任务文档之处及理由 + +1. **`specEntryAbsent` 通过修改 `PinduoduoCandidateDriver.openSpecifications()` + 的返回类型(`Boolean` → 三态枚举)实现,而非在 `BuyerAccessibilityService` + 内部用一个额外的可变字段传递「上一次是否因缺入口失败」。** 原本考虑用 + `BuyerAccessibilityBridge.openSpecifications(): Boolean` 不变、另开一个 + 只读的 `isPinduoduoSpecificationEntryAbsent()` 供候选驱动在失败后追加查询 + 的方案,但那需要第二次节点树查询,且存在「两次查询之间页面已变化」的 + 理论竞态。改为让唯一的点击实现 `openPinduoduoSpecificationsDetailed()` + 一次性产出三态结果,`openPinduoduoSpecifications(): Boolean` 只是其布尔 + 投影,供下单 dry-run 驱动复用同一份点击逻辑而不重复点击——这是改动面 + 略大但更安全、无竞态的方案。`PinduoduoOrderDryRunDriver` 是完全独立的 + 接口(不同文件、不同签名),未受此次接口改动影响。 +2. **验收要点要求「必须写单测证明这条路径不点击任何此类控件」,但项目的 + 单测环境是纯 JVM(无 Robolectric、无 mockito,`build.gradle.kts` 里 + `testImplementation` 只有 junit/coroutines-test/mockwebserver),无法在 + 单测里构造真实的 `AccessibilityNodeInfo` 来直接驱动 + `BuyerAccessibilityService.openPinduoduoSpecifications()` 本身。** 因此 + 证明分两层:(a) `PinduoduoSpecificationParser.isSafeEntryText()` + 是纯 Kotlin 函数、可直接单测,新增用例穷举购买/下单/拼单/支付语义文本 + 证明它们永远不会被判定为安全入口——这是移除兜底分支后唯一决定点击目标 + 的判据来源;(b) `PinduoduoCandidateAutomation` 经 `FakeCandidateDriver` + 完全可测,新增用例证明 `ENTRY_ABSENT` 时自动化只调用 + `openSpecifications`/`returnToResults`,而 `PinduoduoCandidateDriver` + 接口本身没有任何购买/下单/拼单/支付语义的方法可供调用。二者结合,从 + 「判据永不选中购买控件」与「驱动接口没有购买类方法可调」两个角度共同 + 构成完整的回归证明,弥补了无法直接对 `AccessibilityNodeInfo` 做单测的 + 环境限制。这与 T-262 记录中「`PinduoduoUiElement` 是扁平模型,无父节点 + 引用」的既有架构限制是同一类工程判断的延续。 +3. **`hasNativeOrderConfirmation` 触发时不强制 `safetyStop = PAYMENT_BOUNDARY` + ——它只影响 `page` 的分类,不进入 `structureHit || hasCheckoutRoot || + (textMarkerHit && !textMarkerSuppressed)` 表达式。** 这是刻意设计而非 + 遗漏:任务文档第二节的目标是「让既有 `awaitSpecificationEntry()` 的 + `ORDER_CONFIRMATION` 分支生效」,该分支在检查 `safetyStopReason` **之前** + 就已按 `page == ORDER_CONFIRMATION` 匹配并调用 + `returnFromOrderConfirmation()` 安全退出——即便 `safetyStopReason` 为 + `null` 也会生效。真机取证的样本本身还含有「微信支付」促销文案,会命中 + 既有 `paymentMarkers`(`page=ORDER_CONFIRMATION` 不在可抑制页面集合内), + 因此实际场景下 `safetyStopReason` 依旧会是 `PAYMENT_BOUNDARY`——新判据 + 解决的是「`page` 分类不准导致 `awaitSpecificationEntry()` 走不到优雅退出 + 分支、只能被全局 `Blocked` 卡住」,而不是「补一条新的强制停止判据」。 + `isSafeForCleanup()` 已经把 `page == ORDER_CONFIRMATION` 排除在安全清理 + 之外(不论 `safetyStopReason` 是否为空),因此新判据不会让清理逻辑在 + 该页面上意外点击任何控件;其余轮询函数(`awaitPage()`/ + `awaitResultsPage()`/`validateResultsPage()`)在页面不符合预期时只做 + 计数与延迟,不发生点击,因此该设计不构成「放松支付边界判据」。 +4. **`specEntryAbsent` 与 `payPage` 两个新键的相对顺序**:任务文档分别在 + 第一节(`specEntryAbsent`)与第三节(`payPage`)描述,未规定二者互相的 + 先后。按方案小节的编号顺序(`specEntryAbsent` 对应方案第 2 条,`payPage` + 对应方案第 4 条)追加,`specEntryAbsent` 在前、`payPage` 在后。 + +#### 真机验证(未完成,需人工执行) + +以下需人工在真机上执行,任务 status 保持 `DOING`: + +- 重跑此前触发原生结算页故障的同一采购任务,确认遇到无颜色/尺码规格的 + 商品(如纸巾)不再点击「免拼购买」、不再进入订单确认页;若仍进入,应能 + 被识别为 `ORDER_CONFIRMATION` 并经 `returnFromOrderConfirmation()` 自动 + 退出,而非像 T-263 记录的那样卡死。 +- 从 Admin 读取诊断 message 中的 `specEntryAbsent`,评估移除兜底后的候选 + 覆盖损失是否偏高;若偏高需另开任务以更安全的方式补回(不得点击购买按 + 钮)。 +- 从 Admin 读取 `payPage`,确认支付边界触发时能看到页面分类名称,未触发时 + 为 `NONE`。