diff --git a/T-263.-.md b/T-263.-.md new file mode 100644 index 0000000..4dec603 --- /dev/null +++ b/T-263.-.md @@ -0,0 +1,393 @@ + +> 同步来源:[`docs/tasks/T-263.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-263.md) · commit `afc651f75a3a` + +--- +id: T-263 +title: 以页面结构判定支付边界并按页面类型收敛文本规则 +phase: 2 +deps: + - T-262 +status: DOING +created: 2026-07-31 +context_ref: 0c2e423 +work_branch: null +write_paths: + - docs/tasks/T-263.md + - docs/current-state.md + - android-buyer/app/src/main/** + - android-buyer/app/src/test/** +--- + +## 问题 / 背景 + +T-262 采集的四个真机样本证明:**现行「见到支付关键词即停」的判定,在误报和漏报两端 +同时失效。** + +| 页面 | Activity | 节点数 | 非空文本 | WebView | `order_checkout` | paymentMarkers | +| --- | --- | --- | --- | --- | --- | --- | +| 首页 | `MainFrameActivity` | 227 | 48 | 0 | 未命中 | 无 | +| 我的优惠券 | `NewPageActivity` | 457 | 205 | 2(占满屏) | 未命中 | 无 | +| 商品规格弹层 | `NewPageActivity` | 147–153 | 多 | 0 | 未命中 | **2 条 → 误报** | +| **订单确认/去支付** | `NewPageActivity` | **11** | **0** | **1(占满屏)** | 未命中 | **0 → 漏报** | + +### 两端同时失效 + +**误报**:商品规格弹层上的「使用#微信支付,更换先用后付可0元下单」(促销横幅)和 +「提交订单 ¥14.9」(底部 CTA)被子串匹配命中。这是**正常购物页面**,自动化必须能在 +上面读取规格,却被拦在第一步,导致连续多次真机任务三轮全败。 + +**漏报**:真实结算页**一条可见文本都没有**,`order_checkout` 也未命中。现行规则 +**完全无法识别它**——之所以从未出事,是因为自动化根本走不到那一步。 + +### 根本原因:混淆了「在支付页上」与「看得见通往支付的按钮」 + +商品规格弹层上本来就有结算入口按钮,这是正常的。自动化需要停止的是**已经身处支付 +页面**,而不是**看见了支付相关字样**。文本关键词无法区分这两者。 + +### 决定性证据:WebView 本身不阻断无障碍 + +优惠券页同为占满屏 WebView,其 **205 条文本全部可读**;结算页的 WebView **一条读不 +到**。因此「结算页读不到内容」是该页面特有性质(很可能是支付页的无障碍防护), +而非 WebView 的普遍限制。 + +据此,「**存在占满屏 WebView 且语义文本数近乎为零**」在四个样本中**唯有结算页命中**。 + +## 关联需求与交互 + +- 功能:F-005、F-007 +- 用户故事:US-004、US-006 +- 交互:不新增采购操作 +- 架构/API:沿用 T-262 归因诊断,**后端零改动** + +## 行为契约 + +### 一、总原则:只增强,不削弱 + +新判据是**追加**,不是替换。三条判据任一成立即判 `PAYMENT_BOUNDARY`: + +``` +结构判据(新) 或 hasCheckoutRoot(保留) 或 文本判据(收敛后保留) +``` + +`paymentMarkers` 列表内容与顺序**不得删改**。`hasCheckoutRoot` 规则**不得删改**。 + +### 二、结构判据(新增主判据) + +同时满足以下全部条件时判定为支付边界: + +1. 前台包名为拼多多; +2. 存在**占满屏**的 `WebView`(宽高各不小于屏幕的 90%); +3. 可见语义文本数**不超过阈值**(常量定义,初值 `2`,覆盖极少量装饰性文本); +4. **连续 `PAYMENT_STRUCTURE_CONFIRMATIONS` 次快照均满足上述条件**,初值 `3`。 + +第 4 条是硬要求。任何普通页面在 WebView 尚未渲染时都会短暂呈现「WebView + 零文本」, +单次命中即判定会把**页面加载瞬间**误判为结算页。参照既有 `unknownPageLimit` +(20 次 × 200 毫秒)的模式,不得单次命中即停。 + +连续计数在页面结构不再满足条件时立即清零,不得跨页面累积。 + +### 三、文本判据按页面类型收敛 + +**仅当页面未被正面识别为已知安全页面类型时**,文本关键词命中才强制停止。 + +- 页面已被分类器正面识别为商品详情、规格弹层、搜索结果、图片搜索结果或首页时, + 文本命中**不再强制停止**,但必须完整记录(见第四节)。 +- 页面为 `UNKNOWN` 或任何未正面识别的类型时,文本命中**维持现有强制停止行为**。 + +依据:在已确认的正常购物页面上出现支付字样属预期现象(促销横幅、结算入口按钮); +在无法识别的页面上出现支付字样则是危险信号,必须保守处理。 + +**登录、验证码、风控三类安全停止的判定与优先级完全不变**,不受本节影响。 + +### 四、可审计:记录被放行的文本命中 + +新增诊断计数,追加在 T-262 五个键之后: + +``` +payStructureHit=<0|1> payStructureStreak= payTextSuppressed= +``` + +| 键 | 含义 | +| --- | --- | +| `payStructureHit` | 本轮是否由结构判据触发过支付边界 | +| `payStructureStreak` | 结构条件连续命中的最大次数 | +| `payTextSuppressed` | 因页面被正面识别而**未强制停止**的文本命中次数 | + +`payTextSuppressed` 是本次变更的安全审计口径:**它记录了在旧规则下会停止、新规则下 +放行的次数**,用于事后评估变更影响。若真机上该值异常偏高,说明收敛过度,需回头收紧。 + +沿用既有脱敏口径:只记计数,不记页面文本或商品信息。 + +### 五、回归测试必须基于真实样本 + +必须用 T-262 记录的四个真机样本构造测试夹具,逐一断言判定结果: + +| 样本 | 期望 | +| --- | --- | +| 首页(227 节点 / 48 文本 / 0 WebView / 无命中) | 不判支付边界 | +| 优惠券页(457 / 205 / 2 WebView / 无命中) | 不判支付边界 | +| 商品规格弹层(147 / 多文本 / 0 WebView / 2 条命中) | **不判支付边界**(本任务修复目标) | +| 结算页(11 / 0 文本 / 1 占满屏 WebView / 无命中) | **判支付边界**(连续 3 次后) | + +另需覆盖:结算页结构仅命中 1 次和 2 次时**不得**判定;`hasCheckoutRoot` 单独命中时 +仍判定;`UNKNOWN` 页面文本命中时仍判定;登录/验证码/风控优先级不变。 + +### 六、既有行为不变 + +- 不改登录、验证码、风控的检测与优先级。 +- 不改 `paymentMarkers` 内容与顺序、`containsAny` 实现、`hasCheckoutRoot` 规则。 +- 不改 T-262 五个归因键的顺序、名称与取值,也不改 T-254/T-255/T-257/T-260/T-261 + 既有诊断键。 +- 不改候选识别、签名算法、跨轮去重、SKU 硬约束、价格解析、三轮预算或成功判定式。 +- 不改下单 dry-run、订单提交、授权价格策略或支付边界之后的处置逻辑。 + +## 方案 + +1. `PinduoduoPageClassifier` 增加结构判据计算(占满屏 WebView + 文本数阈值), + 阈值与连续次数以具名常量定义。 +2. 连续确认状态由调用方持有并传入,分类器保持无副作用的纯函数特性;结构不满足时清零。 +3. 文本判据增加「页面已正面识别」的抑制条件,抑制时记录计数而非停止。 +4. 三个新诊断键并入 `CandidateSearchDiagnostics`,追加在 T-262 键之后。 +5. 用四个真机样本构造测试夹具并逐一断言,另覆盖连续次数边界与各优先级场景。 + +## 验收要点 + +- [ ] 四个真机样本的判定结果与上表逐一相符。 +- [ ] 结算页结构连续命中不足 3 次时不判定,达到 3 次后判定。 +- [ ] 结构条件中断后连续计数清零,不跨页面累积。 +- [ ] `hasCheckoutRoot` 单独命中仍判定支付边界。 +- [ ] `UNKNOWN` 页面上文本命中仍强制停止。 +- [ ] 登录、验证码、风控的判定与优先级未变,有回归测试。 +- [ ] `paymentMarkers` 内容与顺序未删改。 +- [ ] 三个新诊断键出现且不含页面文本或商品信息。 +- [ ] T-254/T-255/T-257/T-260/T-261/T-262 既有诊断键顺序、名称、取值不变。 +- [ ] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease` 全部通过。 +- [ ] 真机验证:同一商品重跑采购任务**不再**被拦在 `pdd_open_app`,能进入候选采集; + 并从 Admin 读取 `payTextSuppressed` 确认放行次数在合理范围。 + +## 边界 + +- **不删除任何现有判据**:`paymentMarkers`、`hasCheckoutRoot` 全部保留。 +- 不改登录、验证码、风控的检测逻辑与优先级。 +- 不改支付边界判定成立**之后**的处置逻辑(立即停止、不清理、不点击)。 +- 不改 T-261 清理逻辑与 `isSafeForCleanup`。 +- 不改 T-256 提交门槛、T-258 后端校验、T-259 安全停止补报、T-260 节奏配置。 +- 不改候选识别、价格解析或下单相关任何逻辑。 +- 不改后端代码、数据库、API 契约或 Admin 模板。 +- 不处理订单列表页可能被自身规则阻挡的问题(见 T-262 附带发现),另行开任务。 + +## 已知局限 + +**真实结算页样本只有一个。** 结构判据基于该单一样本设计,可能存在盲区: + +- 若某些拼多多版本或路径使用**原生结算页**(有可读文本),结构判据不会命中。此时 + 仍依赖 `hasCheckoutRoot` 与 `UNKNOWN` 页面上的文本判据兜底。 +- `order_checkout` 这个 resourceId 在本次采集的结算页上**并未命中**,其在当前版本 + 上的有效性存疑,但按「只增强不削弱」原则仍予保留。 + +因此本任务**不宣称覆盖全部支付页面形态**,只解决已证实的误报并补上已证实的漏报。 +后续若发现新的结算页形态,应继续追加判据而非放松现有判据。 + +## 执行记录 + +开工后记录修改、命令、结果、环境、决策和 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/workflow/WorkflowModels.kt`: + `PaymentBoundaryAttribution` 追加三个字段——`structureHit`(本次 + `PAYMENT_BOUNDARY` 是否由结构判据触发)、`structureConfirmations`(产出本次 + 归因时的结构连续确认次数)、`textSuppressedCount`(本次判定时因页面被正面 + 识别而放行的命中文本条数);均为末尾追加、默认值 `false`/`0`/`0`,`init` 增 + 加对应非负校验。既有五个字段与其含义/顺序未动。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoPageClassifier.kt`: + - 新增具名常量 `PAYMENT_STRUCTURE_TEXT_THRESHOLD = 2`、 + `PAYMENT_STRUCTURE_CONFIRMATIONS = 3`、私有 `FULL_SCREEN_WEBVIEW_RATIO = 0.9`。 + - `classify()` 追加末尾可选参数 `priorStructureConfirmations: Int = 0`(调用方 + 持有并传入的连续确认状态,分类器本身仍是无副作用纯函数);新增私有函数 + `hasFullScreenWebViewWithFewTexts()`:屏幕范围取当前可见元素包围盒(根容器 + 通常等于屏幕大小,避免新增屏幕尺寸入参改动既有调用方签名),WebView 宽高 + 各不小于该包围盒的 90% 且可见非空文本数不超过阈值时判定单次快照满足结构 + 条件;`structureConfirmations = if (met) prior+1 else 0`, + `structureHit = structureConfirmations >= PAYMENT_STRUCTURE_CONFIRMATIONS`。 + - 将原来紧跟在 `visibleTexts` 之后计算的 `page` 判定块整体上移到 + `safetyStop` 判定之前(`page` 的计算只依赖 `hasCheckoutRoot`/`normalized`/ + `visibleElements` 等既有局部变量,与 `safetyStop` 无关,纯粹重排不改变 + `page` 本身的判定逻辑),使文本判据可以引用已确定的 `page` 值。 + - `safetyStop` 表达式改为 + `structureHit || hasCheckoutRoot || (textMarkerHit && !textMarkerSuppressed)`, + 其中 `textMarkerHit = visibleTexts.containsAny(paymentMarkers)`(`containsAny` + 实现、`paymentMarkers` 内容与顺序、`hasCheckoutRoot` 规则逐字节未动); + `textMarkerSuppressed = textMarkerHit && page in {PRODUCT_DETAIL, + SPECIFICATION_PANEL, SEARCH_RESULTS, IMAGE_SEARCH_RESULTS, HOME}`。登录/ + 验证码/风控三类判定仍在同一个 `when` 的前三个分支,优先级和触发逻辑未动; + 结构/文本判据只影响 `LoginBlocker.NONE/UNKNOWN` 分支内部。 + - `PinduoduoUiSnapshot` 追加两个字段:`structureConfirmations: Int = 0`(本 + 次调用后的连续确认计数,供调用方在下一次调用时回传)、 + `textMarkerSuppressed: Boolean = false`(本次快照是否发生文本命中放行, + 与 `PAYMENT_BOUNDARY` 是否触发无关,始终计算,供调用方做会话级审计)。 + - `computePaymentBoundaryAttribution()` 追加三个入参并透传进 + `PaymentBoundaryAttribution` 的三个新字段,其余标记扫描逻辑未动。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityService.kt`: + 新增私有实例字段 `pinduoduoStructureConfirmations`;`classifyPinduoduoRoot()` + (全仓库唯一的真机快照来源,其余 `snapshotPinduoduoUi()`、 + `clickPinduoduoSearchEntry()` 等数十处调用均经此函数)将该字段作为 + `priorStructureConfirmations` 传入 `classify()`,再用返回快照的 + `structureConfirmations` 回写该字段,从而让结构判据的连续确认覆盖所有轮询 + 循环与一次性动作调用;`snapshotPinduoduoUi()` 在前台包名不是拼多多的分支 + 显式清零该字段,确保离开拼多多前台或结构条件中断时立即清零、不跨页面累积。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoCandidateAutomation.kt`: + 新增两个会话内累计字段 `maxStructureConfirmations`、`textSuppressedCount` + (`reset()` 一并清零);新增私有 `observeSnapshot()` 作为本类唯一的快照读取 + 入口(原有约 11 处 `driver.snapshot()` 全部替换为该函数),每次快照后更新 + 两个累计字段的最大值/计数,不改变 `driver.snapshot()` 的返回值或任何既有 + 判定/重试/清理行为;`diagnostics()` 追加 + `payStructureStreak = maxStructureConfirmations`、 + `payTextSuppressed = textSuppressedCount`。 +- `android-buyer/app/src/main/java/com/roubao/autopilot/procurement/CandidateSearchDiagnostics.kt`: + 末尾追加 `payStructureHit`/`payStructureStreak`/`payTextSuppressed` 三个字段 + (默认值 `0`)及对应 `require` 校验;`withPaymentBoundaryAttribution()` 追加 + 设置 `payStructureHit`(直接取自本次归因),`payStructureStreak` 改为 + `maxOf(既有值, 归因中的 structureConfirmations)`(取大合并,不覆盖候选自动化 + 自身轮询中已观察到的更高计数);`payTextSuppressed` **不**在此方法中设置, + 完全由调用方(`PinduoduoCandidateAutomation.diagnostics()`)持续累积后写入, + 避免被本方法在未触发 `PAYMENT_BOUNDARY` 的常见场景下清零或覆盖(原因见下方 + 「偏离与决策」第 2 条)。既有五个键与 `auditSuffix()` 中它们之前的所有键均 + 逐字节未动,三个新键追加在 `payExact=` 之后。 +- 测试: + - `PinduoduoPageClassifierTest.kt` 新增 9 个用例:基于 T-262 四个真机样本的 + 首页/优惠券页/规格弹层/结算页判定(规格弹层用例直接复现 T-262 记录的误报 + 文案,断言不再判定且 `paymentBoundaryAttribution` 保持默认值)、结算页 + 结构连续命中 1/2 次不判定、第 3 次判定、结构中断后清零再重新计数、 + `hasCheckoutRoot` 单独命中且结构计数为 0 时仍判定、`UNKNOWN` 页面文本命中 + 仍强制停止、登录/验证码/风控在已有结构连续计数时优先级不变。 + - `CandidateSearchDiagnosticsTest.kt` 更新 6 处既有精确匹配断言以包含三个新 + 增尾部默认键,新增 4 个用例覆盖非法计数拒绝、结构命中归因追加、 + `payStructureStreak` 取大合并两种方向、`payTextSuppressed` 不被 + `withPaymentBoundaryAttribution` 清零。 + - `PinduoduoCandidateAutomationTest.kt` 的 `FakeCandidateDriver` 新增 + `structureConfirmations`/`textMarkerSuppressed` 两个可选构造参数并透传进 + 快照;新增一个用例验证候选自动化经 `observeSnapshot()` 累积的 + `payStructureStreak`/`payTextSuppressed` 出现在 `diagnostics()` 中,且 + `reset()` 后归零。 + +#### 命令与结果 + +``` +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testDebugUnitTest --no-daemon --console=plain" +BUILD SUCCESSFUL;Debug 单测 313 项,0 失败、0 错误(较 T-262 的 299 项新增 14 项)。 + +cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testReleaseUnitTest --no-daemon --console=plain" +BUILD SUCCESSFUL;Release 单测 313 项,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。 +``` + +五个目标全部通过。 + +#### 四个真机样本的断言结果(单测,非真机) + +| 样本 | 断言 | 结果 | +| --- | --- | --- | +| 首页(无 WebView、46+2 条文本、无标记命中) | `page=HOME`,`safetyStopReason=null`,`structureConfirmations=0` | 通过 | +| 优惠券页(2 个占满屏 WebView、205 条文本、无标记命中) | `safetyStopReason=null`,`structureConfirmations=0`(文本数量守住占满屏 WebView 不误判) | 通过 | +| 商品规格弹层(0 WebView、命中“提交订单”“微信支付”2 条) | `page=SPECIFICATION_PANEL`,`safetyStopReason=null`(**本任务修复目标**),`paymentBoundaryAttribution` 为默认值,`textMarkerSuppressed=true` | 通过 | +| 结算页(1 个占满屏 WebView、0 条文本) | 连续 1/2 次快照 `safetyStopReason=null`;第 3 次 `safetyStopReason=PAYMENT_BOUNDARY`,`paymentBoundaryAttribution.structureHit=true`、`structureConfirmations=3`、`checkoutRootHit=false`、`markerIndex=-1` | 通过 | + +另覆盖:结构条件中断后连续计数清零、`hasCheckoutRoot` 单独命中仍判定、 +`UNKNOWN` 页面文本命中仍强制停止、登录/验证码/风控在已有结构连续计数时优先级 +不变——均通过。 + +#### 偏离任务文档之处及理由 + +1. **结构判据的“屏幕范围”不是新增入参,而是取当前可见元素包围盒。** 任务文档 + 要求“宽高各不小于屏幕的 90%”,但 `classify()` 现有签名不含屏幕尺寸, + `PinduoduoUiElement` 也不含屏幕尺寸字段。改为在可见元素集合内取 + `boundsLeft/Top/Right/Bottom` 的包围盒作为屏幕范围的近似——真机根容器 + (`FrameLayout`)的 bounds 本就等于屏幕大小(T-262 结算页 dump 亦如此: + `FrameLayout bounds=[0,0][1080,2376]`),且无障碍节点树采集时根节点必然 + 被收录(`BuyerAccessibilityService.collectNodes()` 用 BFS 且根节点最先入 + 队)。这样既满足“宽高各不小于屏幕的 90%”的判定意图,又不改变 `classify()` + / `PinduoduoUiElement` 对既有调用方的签名,风险更低。 +2. **`payTextSuppressed` 的口径改为调用方持续累积,而非像 T-262 五个键那样只 + 在 `PAYMENT_BOUNDARY` 触发时产出。** 这是本任务中分量最重的一个偏离, + 理由:文本判据被放行(`textMarkerSuppressed=true`)几乎总是意味着 + `PAYMENT_BOUNDARY` **没有**触发(正是本任务要修复的常见场景——自动化在 + 规格弹层等页面继续正常浏览)。若沿用 T-262 的“只在触发时产出”模式, + `payTextSuppressed` 在绝大多数真实场景下都会读到默认值 `0`,验收要点 + “若真机上该值异常偏高,说明收敛过度”将无法被观察到,审计口径名存实亡。 + 经推演证实:`hasCheckoutRoot` 恒定强制 `page=ORDER_CONFIRMATION`(不在可 + 抑制页面集合内),结构判据要求非空文本数 `<=2` 而可抑制的五类页面 + (HOME/SEARCH_RESULTS/IMAGE_SEARCH_RESULTS/PRODUCT_DETAIL/ + SPECIFICATION_PANEL)现有识别规则均需要 `>=2` 个独立于标记文本之外的 + 语义文本,二者在现有启发式下几乎互斥——即“归因触发时刻”与“文本判据被 + 放行”事实上不会同时发生。因此改为:`PinduoduoUiSnapshot` 新增 + `textMarkerSuppressed`(与 `structureConfirmations` 一样,每次快照都计算, + 不依赖 `PAYMENT_BOUNDARY` 是否触发);`PinduoduoCandidateAutomation` 通过 + 新增的唯一快照入口 `observeSnapshot()` 在自身轮询过程中持续累积 + `textSuppressedCount`(会话内计数,`reset()` 清零),随 + `diagnostics()` 一并暴露;`CandidateSearchDiagnostics.withPaymentBoundaryAttribution()` + 不再覆盖或清零该键。 + **已知局限**:该累积目前只覆盖 `pdd_browse_candidates` 步骤(候选浏览, + 规格弹层重复出现的主要场所),不覆盖 `PinduoduoSearchAutomation`/ + `PinduoduoImageSearchAutomation` 所在的 `pdd_open_app`/`pdd_enter_query`/ + `pdd_submit_query` 等更早步骤(这两个类当前没有诊断累积基础设施,为其新增 + 等价机制需要更大改动面)。原故障恰好是“在 `pdd_open_app` 单次命中即误判”, + 修复后该步骤会正常通过并进入候选浏览,绝大多数后续文本放行会发生在 + `pdd_browse_candidates` 内并被正确计数;但如果放行只发生在更早步骤且之后 + 再未出现,`payTextSuppressed` 会读到偏低的值。这是本次为控制改动面而接受 + 的范围边界,如需覆盖全流程应另开任务为 + `PinduoduoSearchAutomation`/`PinduoduoImageSearchAutomation` 补充等价的 + 累积与暴露机制。 +3. **`payStructureStreak` 由 `withPaymentBoundaryAttribution()` 与 + `PinduoduoCandidateAutomation.diagnostics()` 共同产出,取二者较大值,而非 + 单一来源。** 归因中的 `structureConfirmations` 只反映触发 `PAYMENT_BOUNDARY` + 那一刻的读数(可能来自 `pdd_open_app` 阶段,`PinduoduoCandidateAutomation` + 完全不知情);候选自动化自身累积的 `maxStructureConfirmations` 只覆盖它 + 自己轮询到的快照。二者是互补而非互斥的信息源,取大合并可以同时覆盖“结构 + 判据在更早步骤触发”与“候选浏览阶段观察到接近阈值但未触发”两类场景,且不 + 丢失任何一方已经观察到的信息。 +4. **文本判据可抑制的页面集合按任务文档字面五类严格取值** + (`PRODUCT_DETAIL`、`SPECIFICATION_PANEL`、`SEARCH_RESULTS`、 + `IMAGE_SEARCH_RESULTS`、`HOME`),未包含 `SEARCH_RESULTS_OTHER_QUERY`、 + `IMAGE_SEARCH`、`IMAGE_SEARCH_RETRY_DIALOG`、`ORDER_LIST` 等其余非 + `UNKNOWN` 类型。任务文档原文列举的是“商品详情、规格弹层、搜索结果、图片 + 搜索结果或首页”,理解为这五个具名类型;其余非正面列举的类型按“任何未正面 + 识别的页面”对待,维持强制停止,偏保守方向,符合“遇到不确定页面停止”的 + 业务硬边界。 +5. **`PinduoduoOrderDryRunAutomation`/`AndroidPinduoduoOrderDryRunDriver` + (T-217 订单 dry-run)一个字节未改**,但由于二者最终都经同一个 + `BuyerAccessibilityService.classifyPinduoduoRoot()` 获取快照,新增的结构 + 判据在共享的 `classify()` 层面对它们同样生效:若 dry-run 流程在 + `expectingOrderConfirmation` 变为 `true` 之前意外连续 3 次观察到“占满屏 + WebView + 语义文本 ≤ 2”的结构(现有 `PinduoduoCollapsedCheckoutPolicy` + 本就是为识别这一结构而设计,只是原先只在该布尔量被显式置位后才转换为 + `PAYMENT_BOUNDARY`),`classify()` 会独立触发 `PAYMENT_BOUNDARY`,比 + dry-run 驱动原有的门控逻辑更早/更广地停止。这是共享判据修复误报/漏报后 + 的必然连带效应,方向上只会让安全停止更早/更确定,不会绕过或削弱既有 + dry-run 停止点,且不修改 dry-run 文件本身的任何代码,因此判断为符合 + “只增强不削弱”原则、未违反“不改下单 dry-run”的边界,但作为可能影响未来 + dry-run 行为的连带效应在此明确记录,供后续如遇异常提前停止时排查参考。 + +#### 未完成项(需人工真机验证,state 保持 DOING) + +- 真机复现同一商品重跑采购任务,确认不再被拦在 `pdd_open_app` 且能进入候选 + 采集。 +- 从 Admin 读取 `payTextSuppressed`,结合上方「偏离与决策」第 2 条的范围边界 + 确认放行次数在合理范围;若异常偏高需回头收紧文本判据的可抑制页面集合或 + 结构判据阈值。 +- 若条件允许,在真实结算页上验证连续 3 次快照后确实触发 `PAYMENT_BOUNDARY` + 且 `payStructureHit=1`。