Reviewer cross-check of2ce59ebon real dumps found a Samsung sample where the true bottom bar is a label-less 31px FrameLayout whose only text node is zero-size ([0,0][0,0]). A whole-panel leaf scan fell through to the next lowest labelled leaf, which lives in the PAYMENT-METHOD row directly above the bottom bar ("使用#微信支付,更换先用后付可0元下单"), and the climb-to-clickable-ancestor landed the click on "change payment method" instead of the order button. That must never happen. finalSubmitTargets is now a strict two-step "row, then leaf" pick instead of a single whole-panel leaf scan: 1. Row: among visible/enabled/non-zero-size CLICKABLE nodes inside the recognized panel's container with height <= 30% of screen height (the guard from2bc624f), pick the one with the lowest bottom edge; ties go to the rightmost. This row, and only this row, may hold the order button. If its own subtree matches a payment-method alias (textAliases.specPanel.paymentAreaAliases: 微信支付/先用后付/支付方式) or SUBMIT_TARGET_BLOCKED_MARKERS, fail explicitly (outcome=row_blocked) - never fall back to a higher row. 2. Leaf: within that row's own subtree (or the row itself), among visible/non-zero-size nodes with a non-blank own label whose nearest clickable ancestor is EXACTLY that row, pick the bottom-right-most one (unchanged rationale from2ce59eb: PurchaseUiDriver.clickFresh re-finds by preferredOrDescendantLabel()+className+center±32 before climbing to the nearest clickable ancestor, so the target must carry a real label). If the row has no such leaf, fail explicitly (outcome=bottom_row_unlabelled) - never fall back to a higher row either. hasFinalSavedAddressEvidence, finalConfirmation and submitOrderOnce still all consume this one finalSubmitTargets. Tests: SpecPanelFixtures.sheet(Sheet(submit = "hidden")) already models the Samsung shape (31px hidden bottom bar above a real payment-method row) - added a test asserting it now fails explicitly with no click, verifying the payment row is never touched. Added a second test where the payment row is itself the bottom-most clickable row (no submit region at all) and must be rejected by its own alias match. Fixed readySheet() (used by the address-save-wait test) to stop compounding liveShapedSheet()'s own hidden 31px placeholder with a separate real submit button - the two used to tie and could flip which one the rightmost tie-break picked; it now builds the same shape directly with submit="none" so "r/final" is the sole, unambiguous bottom row. Updated one diagnostic key assertion (tie= -> rowTie=) for the row-level tie-break. Every other #335 test (legacy 提交订单, 570 structure + ReFindingDriver re-find/climb, payment-word blocked, outside-panel-container, no-determinable-container) passed unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Android Agent
一期 Android 执行端,包名为 cn.ilapage.goauto.agent。
当前已经实现:
- 首次安装生成并持久化随机 UUID
installId,不读取硬件唯一标识。 - 自动注册和已认证的版本信息更新,无注册码输入页。
- Android Keystore AES-GCM 加密保存 Device Token,界面和日志不显示明文。
- 前台服务、15 秒心跳、网络恢复触发重连。
- Agent 0.3.1 的状态、采集、采购、设置四 Tab 外壳;默认进入状态页。
- 状态页区分无障碍未开启、系统已开启但服务未绑定、已绑定就绪三态。
- 设置页可测试服务器连接、保存服务器地址和设备名称并重新连接;任务执行中禁止修改。
- 设置页只显示 Device Token 是否配置,不显示 Token 明文;采集和采购 Tab 在 #90 接入当前设备任务历史。
- 清除 App 数据后按新的安装实例注册;Token 无效时明确报错,不降级为无认证注册。
服务端地址可在设置页保存,也可在构建时通过 GOAUTO_SERVER_URL 环境变量或同名 Gradle 属性写入默认值。Debug 和 Release 均接受 HTTP 或 HTTPS Origin;HTTP 会明文传输 Device Token、任务内容和执行结果,仅应连接到管理员明确启用明文 Agent 流量的受控服务。设置页的“测试连接”只访问 /api/v1/health,不会注册设备或创建任务;“保存并重新连接”沿用当前 installId 和 Device Token。
设备执行任务时,状态页显示当前任务,设置页会禁用服务器地址、设备名称、测试和保存操作。返回系统无障碍设置后,状态页和设置页会自动刷新真实就绪状态。
任务结果成功提交(采购结果也可以先安全写入本地 Outbox)后,Agent 进入 15 秒自动返回窗口。它沿用现有任务轮询依次确认采购、采集队列都为空,并确认用户没有离开本次自动化打开的 PDD/浏览器页面,才返回 Agent 状态页。新任务、网络异常、待恢复的不可逆采购边界或前台应用变化都会取消自动返回。
采集结果被服务端安全接收后还会按设置页的“采集任务执行间隔”等待下一次采集;起止值均为 0~600 秒整数,默认 15~15,每次在闭区间内随机一次。采购仍可优先执行,手动检查和重新采集不能绕过;本次抽中的秒数、开始时间和截止时间会一起持久化,重启或修改设置不会重新随机。该间隔与自动返回窗口相互独立。任务执行、自动返回窗口和采集间隔期间有界保持亮屏,结束后释放;不会自动解锁 PIN、图案或密码。
T05 真机验证记录
- 一加 PKG110 / Android 16:首次注册、15 秒心跳、前台通知和在线空闲状态已验证。
- 杀死进程后重新打开:installId 和 deviceId 保持不变,设备表仍只有一条记录,心跳恢复。
- PDD 8.17.0 版本信息已随注册提交。
- ColorOS 首次 ADB 安装需要在“安装增强防护”页执行“更多 → 开始深度扫描 → 继续安装”。
- 当前 MVP 只完成并维护一加/ColorOS 真机兼容,不包含华为 ROM。
T23~T24 v2 真机验证
- 一加 PKG110 / Android 16 已验证 v2 能力上报、正式任务领取、浏览器到 PDD
NewPageActivity、规格面板打开、颜色文字点击和逐颜色稳定价格。 - 图片型颜色卡片只点击文字节点,不点击整卡或“打开大图/查看大图”。
- 规格值会排除“#一次选多款#”等面板功能入口,避免把操作按钮伪装成尺码和 SKU。
- 商品
719834019024已采到 4 个颜色、4 个 2690 分颜色价格和 4 个结构化 SKU,缺少尺码、店铺和评价时正确提交completed_partial。 - 商品
236231603269已验证规格面板归顶、锚定横纵容器、逐行蛇形颜色遍历和尺码续页:14 个颜色、14 个颜色价格、8 个尺码和 112 个完整可用 SKU,任务最终为completed。 - 滚动优先使用目标容器的无障碍
ACTION_SCROLL_FORWARD/BACKWARD;回退坐标手势时等待系统完成回调后才读取新树,避免把未结束手势误判成列表边缘。 - 商品标题回退只接受与销量节点空间关联的文本,不再把页面中任意最长文本当成标题。
- 规格入口只接受带明确选择语义的规格摘要,或屏幕底部真实“购买/拼单”文字按钮;评价、评论、晒单和问答区域及其父容器不能成为入口。
- 商品评价页与商品详情页共用
NewPageActivity时,Agent 通过顶部评价标题和评价内容识别误触,安全返回后最多重新定位一次。点击无页面变化、重复误入评价页和面板结构无法确认使用不同错误码;日志只记录入口语义、控件类型、坐标和动作结果,不保存控件树或截图。 - 逐颜色价格必须先取得目标选中、已选摘要或规格面板变化证据,再等待价格刷新并连续稳定读取;其他颜色残留的选中标记不会阻塞新价格,相同价格仍允许保存。动作无效果时不会沿用旧价格,结构化日志区分未确认选择、未找到价格和价格不稳定。
.\gradlew.bat test
.\gradlew.bat assembleDebug
APK 输出:app/build/outputs/apk/debug/app-debug.apk。AccessibilityService、任务领取和 PDD 页面执行由后续工单实现。