Files
goauto/android
QiuSWandClaude Opus 5 e82762e6b7 fix(android): pick submit leaf only from the bottom-most row, not the whole panel (#335)
Reviewer cross-check of 2ce59eb on 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 from 2bc624f), 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 from 2ce59eb: 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
2026-09-23 10:57:52 +08:00
..

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 页面执行由后续工单实现。