--- id: T-102 title: 浏览并采集最多 5 个候选 phase: 1 deps: - T-101 status: DONE created: 2026-07-25 context_ref: f226d0f work_branch: main write_paths: - android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt - android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/** - android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/** - android-buyer/app/src/main/java/com/roubao/autopilot/ui/screens/SearchProbeScreen.kt - android-buyer/app/src/main/java/com/roubao/autopilot/workflow/WorkflowModels.kt - android-buyer/app/src/main/res/xml/buyer_accessibility_service.xml - android-buyer/app/src/test/java/com/roubao/autopilot/pinduoduo/** - docs/00-ai-start-here.md - docs/02-requirements.md - docs/03-tech-stack.md - docs/04-architecture.md - docs/05-coding-rules.md - docs/current-state.md - docs/tasks/T-102.md - progress.md --- ## 问题 / 背景 T-101 已能稳定进入固定词结果页,但尚未证明可以识别独立商品卡、进入详情、保存证据 并返回同一结果页。T-102 只验证有界候选浏览,不进行匹配判断、下单或支付。 ## 关联需求与交互 - 功能:F-005 的最多 5 个候选采集部分、F-007 安全失败。 - 用户故事:US-004、US-006。 - 交互:Android“探针”页展示候选计数和当前步骤。 - 架构:`WorkflowRunner` + `PinduoduoCandidateAutomation` + 无障碍 driver + App 内部缓存证据。 ## 方案 1. 只从已确认的搜索结果页识别主 RecyclerView 的直属商品卡。 2. 商品卡必须可见、启用、可点击,包含图片和有限文本;用文本 SHA-256 去重,不在 日志或 UI 展示标题。 3. 每次只打开一个候选;详情页必须由稳定语义组合确认,保存全屏 PNG、文本指纹和 节点计数后立即返回结果页。 4. 最多采集 5 个不同候选,最多滚动结果列表 2 次;没有无界循环或翻页。 5. 登录、验证码、风控、未知页、订单和支付边界立即阻塞;绝不点击详情页购买控件。 ## 验收要点 - [x] 从 T-101 结果页依次进入候选详情并返回。 - [x] 采集数量始终不超过 5,结果滚动次数始终不超过 2。 - [x] 每个候选有内部缓存 PNG、SHA-256、尺寸和语义节点证据。 - [x] 证据文件名匿名,普通日志和 UI 不输出商品标题。 - [x] 全流程不使用坐标,不点击购买、订单或支付控件。 - [x] 页面未知或安全边界出现时产生可区分的 `BLOCKED`。 - [x] Fake driver 自动化测试覆盖成功、去重、上限和安全停止。 - [x] 拼多多 8.17.0 真机 smoke 采集 5 个候选并返回结果页。 - [x] `lintDebug test assembleDebug` 通过,默认 APK 不含私有 fixture。 ## 边界 - 不评估候选是否匹配;T-104 才接入候选评估。 - 不把候选证据上传网络;T-202 以后再接后端证据接口。 - 不点击购买、拼单、加入购物车、订单或支付相关节点。 - 不保存真实蝦皮输入或把商品详情文本写入普通日志。 ## 执行记录 ### 2026-07-25:任务开始 - 基于 T-101 提交 `f226d0f` 开始。 - 脱敏结构审计确认拼多多 8.17.0 结果页主 RecyclerView 和双列商品卡;详情页有 可点击返回语义以及联系客服、收藏、店铺语义,当前未出现订单或支付边界。 ### 2026-07-25:实现 - 新增 `PinduoduoCandidateAutomation` 和复合探针 workflow。一次 session 最多尝试 5 个不同卡片、最多发出 2 次结果滚动;预算在动作前消耗,runner retry 不会重置。 - 主结果 RecyclerView 必须可滚动、覆盖至少 90% 宽度且拥有足够节点。候选卡必须 至少 80% 可见、宽度在单列范围、含图片/价格/至少 2 个语义文本;根不可点击时 只接受唯一覆盖卡片至少 85% 面积的点击目标,小型嵌套按钮不会被选中。 - 详情页同时要求可点击返回、联系客服/收藏/店铺至少两类语义、宽 ViewPager 和宽 可滚动主列表。详情页动作白名单只有截图和一次全局返回,返回后必须重新确认固定 查询结果页。 - Android 11/API 30+ 使用声明了 `canTakeScreenshot` 的无障碍截图 API。Hardware buffer 在独立执行器转换和压缩并始终关闭;8 秒超时后结构化失败,不绕过安全窗口。 - 每个候选只保存匿名 PNG、卡片/详情语义 SHA-256、节点计数、文件 SHA-256、字节数 和尺寸到 App cache;manifest 不保存标题或页面原文,文件写入在 IO dispatcher。 - 探针页增加候选 `0/5` 计数和第五步;仅记录 workflow/候选阶段枚举与截图错误码。 ### 2026-07-25:自动化验证 - `gradlew.bat lintDebug test assembleDebug --no-daemon` 成功。 - App Debug/Release、task contract 和导入器共 14 份报告、78 次测试,0 failure、 0 error、0 skipped。 - Fake driver 覆盖 5 个去重候选、候选硬上限、旧页面有界过渡、两次滚动上限、 空列表、安全阻塞、截图失败以及从已验证详情页安全恢复。 - 普通 Debug APK 中 `assets/probe-fixtures/` 条目数为 0。 ### 2026-07-25:真机 smoke - OnePlus PKG110、Android 16/API 36、拼多多 8.17.0 上,从肉包按钮启动后 20 秒内 依次采集 5 个候选并返回固定词结果页;肉包显示 `5 / 5` 和五步完成。 - cache manifest 为 5 条,匿名文件名合法;5 个 PNG 均为 `1080x2376`,字节数为 正数,文件 SHA-256 与 manifest 全部一致。 - 最终结果页订单/支付边界计数为 0;logcat 没有肉包崩溃或 ANR,阶段日志不含商品 标题或页面原文。 - 真机审计 XML/PNG 位于被忽略的 `.local/`,候选证据位于 App 内部 cache,均未纳入 Git。 ### 2026-07-25:修正记录 - 首次运行在候选点击后把短暂保留的旧结果页误判为未知。改为只在固定 20 次观测 预算内等待结果页和详情页过渡;任何安全阻塞仍立即停止。 - 调试期间旧 Activity 造成终态读取混淆;最终 smoke 先清理肉包任务栈,再用 `FLAG_ACTIVITY_REORDER_TO_FRONT` 返回原实例核验,未创建第二个 workflow。 ## 后续 - T-103 使用 T-004 的私有 ProbeTask 接入结构化需求提取;不能把订单号或店铺名发给 模型。 - T-104 才把候选截图交给评估适配器;T-102 不声明任何候选匹配结论。