155 Commits
Author SHA1 Message Date
QiuSWandClaude Opus 5 4caed7d2dd chore(android): bump agent to 0.9.63 for #335
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:54 +08:00
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
QiuSWandClaude Opus 5 edc956c430 fix(android): aim submit target at labelled leaf so clickFresh can re-find it (#335)
Real-device result on build 0.9.62 (commit 34d09cd, task 594): the bottom
container was selected correctly but the click never happened.
GoAutoAccessibilityService.clickFreshDetailed re-finds its target live by
(preferredOrDescendantLabel() == target.label, className, center ±32) and
only THEN climbs to the nearest clickable ancestor and clicks it. The
previous finalSubmitTargets returned the clickable FrameLayout container
itself, whose SnapshotNode.label is "" (its text lives only in children),
while the live preferredOrDescendantLabel() digs into a child and returns
real text ("大促价,") - so the re-find always missed -> TARGET_NOT_FOUND ->
PURCHASE_ORDER_RESULT_UNKNOWN right after the irreversible boundary. No
order was created (user confirmed).

Per the user's restated strategy, finalSubmitTargets now aims at the
bottom-right-most VISIBLE, non-zero-size node that has a NON-BLANK OWN
LABEL within the recognized panel container, requiring it to have a
clickable ancestor (or be clickable itself) of non-zero size; ties go to
the rightmost. This is the same "aim at the labelled descendant, let
clickFresh climb to the clickable ancestor" convention already used by
ImageSearchCandidatePolicy.clickTargetInside for PDD image-search result
cards. The nearest clickable ancestor's subtree (not just the chosen
leaf's own subtree) must not contain SUBMIT_TARGET_BLOCKED_MARKERS, since
a payment word can live in a sibling leaf under the same clickable row.
hasFinalSavedAddressEvidence, finalConfirmation and submitOrderOnce still
all consume this one finalSubmitTargets, so the address-save evidence
check and the final click stay consistent.

Tests: added ReFindingDriver, a fake driver that emulates
clickFreshDetailed's real re-find + climb-to-clickable-ancestor semantics
against a fixed live node list, with a dedicated test on the task 570/594
structure that asserts the click actually lands on the clickable
FrameLayout container, not the labelled leaf finalSubmitTargets aimed at -
this is the class of test that would have caught the task 594 regression;
prior StaticDriver-only tests could not, since StaticDriver's clickFresh
just records target.label directly. Updated the existing 570 test and the
zero-size test to assert on the now-correct leaf-label target. Verified
every other #335 safety test (legacy 提交订单, tie/rightmost, payment-word
blocked, outside-panel-container, no-determinable-container) still passes
unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:50 +08:00
QiuSWandClaude Opus 5 1d302a9144 chore(android): bump agent to 0.9.62 for #335
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:48 +08:00
QiuSWandClaude Opus 5 18353b2381 fix(android): scope submit target to panel container, widen blocked words (#335)
Review follow-up on 2bc624f:

1. finalSubmitTargets' payment-word block only covered PAYMENT_MARKERS
   (立即支付/确认支付/输入支付密码). The ticket also requires blocking 去支付
   and 付款. These cannot join the shared PAYMENT_MARKERS list: it also
   gates the global pageProblem() payment guard, and UNPAID_MARKERS' "待付款"
   contains "付款", so adding it there would misclassify every ordinary
   unpaid-order page as a forbidden payment page. Added a separate
   SUBMIT_TARGET_BLOCKED_MARKERS list (立即支付/确认支付/去支付/付款/输入支付密码)
   used only by finalSubmitTargets' bottom-node subtree check.

2. Candidates were previously scanned across the whole snapshot, so an
   unrelated clickable node outside the recognized panel (e.g. an
   underlying goods-detail-page bottom bar still in the accessibility tree
   behind the sheet) could out-rank the real button by sitting lower on
   screen. Added panelContainerBounds(): climbs from the panel's unique
   quantity input to the largest ancestor that still does not cover the
   whole screen (the same "does not cover the whole screen" bounded notion
   #331 already uses for sharesBoundedPanelContainer/boundedScrollables),
   giving the whole bottom-sheet container. ParsedPddScreen.specPanelContainer
   (PddScreenParser's `panelScrollable`) was considered but is the wrong
   notion here: it is only the inner *scrollable dimension list* used for
   heading/option parsing, and in a real PDD sheet the address/payment/
   submit rows sit outside it as structural siblings, not descendants -
   using it would incorrectly exclude the real submit row in most panels.
   A candidate belongs to the panel when its center point falls inside the
   container's bounds (geometric containment, matching the existing
   `inside()` convention in this file, not path prefix, since PDD's own
   tree can place the submit bar as a structural sibling that is still
   visually part of the sheet). When the quantity input is missing or not
   unique, the container cannot be determined and finalSubmitTargets fails
   explicitly (no target) instead of guessing.

Tests added to SpecPanelRecognitionTest: bottom node labeled "去支付" and
"付款" are blocked (distinct from the existing PAYMENT_MARKERS-triggered
PURCHASE_PAYMENT_FORBIDDEN cases, since neither word reaches the global
guard); a clickable node outside the recognized panel container is never
chosen even though it is bottom-most on screen; a recognized panel with no
determinable quantity input/container fails explicitly. All existing
readOrderResult tests (which legitimately show "待付款"/"去支付" on the
order-result page, unrelated to finalSubmitTargets) stay green unchanged,
confirming SUBMIT_TARGET_BLOCKED_MARKERS did not leak into that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:46 +08:00
QiuSWandClaude Opus 5 fc35d144c8 fix(android): submit target = bottom-most clickable node in spec panel (#335)
Task 570 (goods 8580) timed out because the order button's text kept
changing (促销价文案 instead of "提交订单"), so FINAL_SUBMIT_MARKERS never
matched and hasFinalSavedAddressEvidence stayed false forever. Per the
user's confirmed strategy, finalSubmitTargets now ignores button text and
instead picks the bottom-most clickable/enabled/non-zero-size node of an
already-recognized spec panel (reusing #331's PddScreenParser recognition,
including the REQUIRED_EVIDENCE fallback). Ties on the bottom edge pick the
rightmost node and are flagged in diagnostics. A node whose own label or
subtree contains a payment word (PAYMENT_MARKERS) is never a click target.
A candidate taller than 30% of screen height is excluded so a full-sheet/
full-body wrapper container can never win the tie against the real bottom
bar. The old FINAL_SUBMIT_MARKERS text match is kept only as non-required
diagnostic evidence. hasFinalSavedAddressEvidence, finalConfirmation and
submitOrderOnce all consume the same finalSubmitTargets, so the address-save
evidence check and the final click use one consistent rule.

Tests: PurchaseLiveAutomationTest's LiveDriver confirmation-page fixture now
carries a full #331 REQUIRED_EVIDENCE structure (address/payment rows nested
under the shared panel container) since finalSubmitTargets depends on panel
recognition; one assertion that encoded the old bare-tap address activation
is updated to reflect the now-wrapped clickable row. SpecPanelRecognitionTest
replaces the old text-matching submit-target test with cases for the 570
promo-price bottom row, legacy "提交订单" panels, side-by-side tie/rightmost,
payment-word blocking (visible and invisible-subtree), zero-size exclusion,
and unrecognized pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:44 +08:00
QiuSWandClaude Opus 5 47f6377095 chore(android): bump agent to 0.9.61 for #334
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:42 +08:00
QiuSWandClaude Opus 5 faad596d32 fix(collection): narrow spec-panel collapse detection, surface explicit probe failures (#334)
Review follow-up on 56f24e1:

1. moveSpecPanelToTop's collapse detection was too sensitive: a normal
   scroll-to-top on a multi-dimension panel can legitimately drop the
   heading count (a lower heading scrolls out of view) without losing
   any spec values. SPEC_PANEL_TOP_COLLAPSED is now only raised when
   every collected spec dimension value vanishes (count goes from >0
   to 0) or the spec panel is no longer recognized as open
   (!specPanelOpen or specPanelType == UNKNOWN). Heading count alone
   no longer triggers it.

2. A collector failure during the purchase spec probe (e.g.
   SPEC_PANEL_TOP_COLLAPSED) previously vanished into
   collectPurchaseProbe returning null, so probeOutcome() reported
   the generic PURCHASE_SPEC_NOT_MATCHED "商品规格探测失败" — the same
   as an ordinary spec mismatch. collectPurchaseProbe now encodes a
   failed collect() result (code + message) into the opaque probe
   JSON via PurchaseSpecProbePolicy.encodeCollectorFailure, and
   PurchaseSpecProbePolicy.demote (replacing demoteIfEmpty, kept as a
   deprecated alias) surfaces it as an explicit
   PURCHASE_SPEC_PROBE_FAILED failure with a purchaser-readable
   message ("规格探测时规格面板被拖动,规格标题消失" for
   SPEC_PANEL_TOP_COLLAPSED, a generic message naming the code
   otherwise). This keeps PurchaseRehearsalExecutor's probeSpecs
   callback contract opaque, so its existing unit tests are untouched.

Tests: PddProductDetailCollectorTest (heading count dropping while
dimensions remain present must not fail collection, still collects
colors/sizes), PurchaseSpecProbePolicyTest (encode/extract collector
failure, demote surfaces SPEC_PANEL_TOP_COLLAPSED and unrecognized
codes explicitly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:40 +08:00
QiuSWandClaude Opus 5 4f02e4dcfa fix(collection): skip needless spec-panel top swipes and fail explicit empty probes (#334)
PddProductDetailCollector.moveSpecPanelToTop always swiped DOWN at
least once even when the panel already showed its topmost color
heading, and required two identical viewport signatures to stop. On
a real device that extra swipe could drag the bottom sheet and make
the color/size headings disappear, after which the purchase spec
probe silently reported spec_probe_completed with zero dimensions
and the server reported the generic PURCHASE_SPEC_NOT_MATCHED,
hiding the real cause (goods 8580, tasks 551/552).

- moveSpecPanelToTop now skips the restore swipe when the panel is
  already at top (the first parsed dimension is "color" with visible
  values), and stops and fails explicitly (SPEC_PANEL_TOP_COLLAPSED)
  if a restore swipe makes headings/dimensions vanish, instead of
  swiping further or returning an empty success.
- New AgentDiagnosticReason.SPEC_PANEL_TOP_ALREADY /
  SPEC_PANEL_TOP_COLLAPSED record swipe count and heading/dimension
  counts before/after (booleans/counts only, no page text).
- New PurchaseSpecProbePolicy demotes an Agent spec_probe_completed
  outcome with zero collected dimensions into an explicit
  PURCHASE_SPEC_PROBE_EMPTY failure ("规格探测未读取到任何颜色或尺码")
  before it is persisted/reported, instead of reaching the server as
  a normal empty probe.
- Server resolveProbedSpecs uses the same explicit
  PURCHASE_SPEC_PROBE_EMPTY code/message when a probe result has zero
  colors and zero sizes, as defense in depth for older Agent builds.

Tests: PddProductDetailCollectorTest (already-at-top skips the
restore swipe; not-at-top restores and still collects; vanishing
headings stop swiping and fail), PurchaseSpecProbePolicyTest, and
service_test.go TestLiveProbeWithNoDimensionsFailsWithExplicitEmptyProbeCode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 10:57:38 +08:00
QiuSWandClaude Opus 5 36810f3a71 chore(android): bump agent to 0.9.60 for #331/#332
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-22 10:51:05 +08:00
QiuSWandClaude Opus 5 d8382e6518 fix(android): recognize spec panel by address, payment and quantity input (#331)
- PddScreenParser: add REQUIRED_EVIDENCE fallback type after all existing
  branches; requires address entry (masked phone in a nearby clickable row),
  payment entry (specPanel.paymentAreaAliases in a nearby clickable row) and
  one enabled quantity EditText, plus at least one auxiliary signal
  (options, summary, close, +/- or submit action). +/- no longer required
  on this path; existing branches unchanged.
- The three required items must share one panel container: their lowest
  common ancestor must not be a window root and must not cover the whole
  screen. Items from an address dialog, a payment dialog and an unrelated
  input box are not recognized.
- Purchaser hints on SPEC_PANEL_EVIDENCE_NOT_MATCHED, the spec-panel wait
  diagnostic and the address-save timeout when a required item is missing
  (default WeChat Pay / default address / quantity input). No Alipay alias.
- Purchase spec-entry wait and post address-save wait: bounded 5000 ms,
  fallback panels need two identical structure samples, no Back while
  waiting, explicit failure on timeout; boolean/count diagnostics only.
- Final submit target additionally rejects zero-size labels/containers.
- Diagnostics: evidence flags (incl. same-container) in panel evidence and
  spec-panel entry event.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-22 10:14:03 +08:00
QiuSWandClaude Opus 5 0edc78a943 fix(android): dedupe spec option nodes and diagnose selection mismatch (#332)
- PddScreenParser: nested clickable nodes of one labelled option block
  (outer block, image, inner text with a badge such as "零差评") are
  collapsed into one spec value named by the outer block (task 535). The
  click node is still chosen by safeOptionRank; selected/checked flags are
  merged from the block members; no badge-text stripping.
- PurchaseRehearsalExecutor: on SPEC_SELECTION_UNCONFIRMED emit a
  specSelectionUnconfirmed diagnostic with the dimension, the target value
  and the parsed option texts (each truncated to 40 chars) with selected
  flags. Spec values are product attributes only.
- Tests: SpecOptionDedupTest with a sanitized task 535 fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-22 10:08:25 +08:00
QiuSW b76fb73e12 fix: wait for unpaid order evidence and report payable total (#325) 2026-09-19 11:52:53 +08:00
QiuSW 8c01329f97 feat: capture optional PDD order amount in manual backfill (#306) 2026-09-18 09:22:12 +08:00
QiuSWandClaude Opus 5 c2426e2671 merge: #242 Agent 采购记录页回填入口与 PDD 订单扫描
合并 feat/242-agent-order-backfill(edd1cb1)。

Android 代码自动合并无冲突,与主线此后的 Agent 改动(#276/#277 图搜、#292
归位、#294 失败原因、#302 支付页标记)共存;全量单测 390 通过(含本单新增
16 个),assembleDebug 成功。

docs/03 镜像冲突取主线版本(2026-09-11 Wiki 同步版,已含 #242 规则)。

注意:本次合入的是 2026-09-08 的完整版(扫描 + 调用 #241 上传),不是
2026-09-10 讨论的「第一阶段只写本地」版本。页面识别判据仍未经真机验证。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 15:54:59 +08:00
QiuSWandClaude Sonnet 5 f634996dad fix: order_result_unknown 补充"是否见过支付/待付款页"诊断标记 (#302)
order_result_unknown 目前是全有或全无:parseOrderEvidence 要求订单号、下单
时间、待付款/支付文案同时命中才算 order_created,任何一项缺失就落进同一个
order_result_unknown,无法区分"确实到过支付页只是没读全证据"和"根本没到
那一步"——前者大概率已在 PDD 建了真实订单。

Agent:readOrderResult 采样循环中,只要命中过支付页 Activity 或
unpaidContextVisible(待付款/待支付/去支付文案),记 paymentPageObserved,
与订单号是否解析成功无关,随 order_result_unknown 一起上报。

服务端:PurchaseTask 新增 PaymentPageObservedAt,仅在请求带
paymentPageObserved=true 时写入服务端当前时间;不回填既有 107 笔历史记录,
无法从历史数据反推当时是否见过支付页。

不改判定结果本身,order_created 的四项条件、批量重试逻辑均未动。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 14:25:47 +08:00
QiuSWandClaude Opus 5 b2b8c0d1e0 fix(ops): 记录图搜规格匹配的逐条结果,保留 Agent 原始失败原因 (#294)
排查时两次卡在缺记录上,只能靠手工重放接口和按失败耗时反推。

服务端:BatchSpecMatch 的返回值此前用 `_` 丢弃,23 条明细因 AI 服务 503 全部
失败时日志一个字都没有。改为记录四个计数并附前几条阻塞原因。

Agent:IMAGE_SEARCH_ENTRY_NOT_FOUND 有「找不到入口」和「归位失败」两个来源,
统一文案把两者抹平。改为在用户文案后括注内部原因,写法与候选点击失败一致。

两处都只记录错误码、计数与规格层面的原因,不含凭据、账号、订单和个人数据。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 11:53:43 +08:00
QiuSWandClaude Opus 5 bb097a6a15 fix(android): 图搜归位的返回动作等页面稳定,推不动时清栈回首页 (#292)
BACK 分支按完就走、固定 300ms 盲等,不够 PDD 完成一次页面切换:抓到的是切换
动画中间态,判据全不命中,被归为 OTHER_PDD 后继续按。50 次预算在 15 秒内空转
耗尽,App 可能一次完整返回都没做完(真机任务 156)。

CLICK_CAMERA 分支早有 awaitPage,BACK 是唯一一个动作后不等待的分支,同一个坑
只修了一半。

- BACK 后等页面形态真的变化再判下一步。
- 连续 3 次推不动,改用 FLAG_ACTIVITY_CLEAR_TOP 清栈回首页。
- resetPddToHome 与 launchPddToForeground 分开:后者要保留深层页面,采集当前页
  的流程正需要那个行为。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 11:46:04 +08:00
QiuSWandClaude Opus 5 2193762cbd fix(agent): 相册最新项判定改用经典 sortOrder,修复 Android 12 (#277)
三星 SM-G9700(Android 12 / SDK 31)上图搜任务持续以
IMAGE_SEARCH_ASSET_NOT_LATEST 失败,而设备时钟正常、READ_EXTERNAL_STORAGE
已授予、相册里最新的照片还是两天前的——我们刚写入的图确实是最新的。

根因是查询参数形式。isMostRecent 用的是 Bundle 的
QUERY_ARG_SORT_COLUMNS / QUERY_ARG_LIMIT,老版本 MediaProvider 不认:
排序被忽略后 LIMIT=5 取回任意 5 行,我们刚写入的图不在其中,maxWith
自然不等于 ownId。同一台设备上用经典 sortOrder 字符串查询排序正常
(content query --sort 验证过),OnePlus PLY110(Android 16 / SDK 36)
两种形式都认,所以此前没暴露。

改用经典 sortOrder "date_added DESC, _id DESC",按游标读前
RECENCY_QUERY_LIMIT 行。LIMIT 不能写进 sortOrder:Android 11 起
MediaProvider 会以 Invalid token LIMIT 拒绝。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 17:51:27 +08:00
QiuSWandClaude Opus 5 95b5f05ae8 fix(#277): 图搜候选点击改瞄有文案子节点,关联不再被跨币种判据挡死
真机连续排查出三处缺陷,均已实测确认:

1) 候选卡片点不动。快照里 SnapshotNode.label 只取节点自身的 text/
   contentDescription,而 clickFreshDetailed 重新定位时用
   preferredOrDescendantLabel()(会往子孙里挖)。候选卡片是自身无文案的
   FrameLayout,两者算出的 label 一个是空串一个是标题文字,永远对不上,
   重找直接 TARGET_NOT_FOUND。改为瞄准卡片内最靠上的有文案子节点,由
   clickFreshDetailed 既有的向上走到可点祖先逻辑落到卡片容器上。
   卡片内文案的归属同时从 path 前缀改为几何包含——path 父子语义从未核对,
   几何包含已用真机 dump 直接验证。

2) 错误码拆分。IMAGE_SEARCH_NO_CANDIDATES 此前同时表示“结果页没出现”和
   “卡片点不动”,定位问题时完全分不清。拆为 RESULTS_NOT_READY /
   NO_CANDIDATES / CANDIDATE_CLICK_FAILED,并把 clickFreshDetailed 的
   result 与 reason 带进失败消息——这两个值本来就有,此前被丢弃。

3) 自动关联从上线起一次都没执行过。snapshot.PriceGuardSkipped 与
   shopee_product.currency != "CNY" 两处各自 return nil,而 priceSkipped
   的唯一来源就是 referenceCurrency != "CNY",两者是同一个条件;线上 26902
   个虾皮商品全部是 TWD。表现为图搜任务 completed、PDD 商品采集完整,但
   关联停在旧值,采购因此无法创建(订单 26091536MJHXG8 即如此)。
   币种不同只说明价格没法比,不说明关联不该建立,两件事已解耦。

   补偿控制是 image_search_linked 标记,已显示在商品页与采购视图。
   ImageSearchPriceAllowed 零调用方,加 [未接线] 标注,避免再被当成生效的
   防线;待引入可配汇率后再接。

测试夹具默认币种由 CNY 改为 TWD(线上真实币种),并新增跨币种必须关联、
同币种行为不变两个用例。此前夹具用 CNY,所以整套用例全绿而线上从未成功。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 16:27:15 +08:00
QiuSWandClaude Opus 5 c1ed3750d2 fix(agent): 声明 pdd.image-search.v1 能力 (#277)
服务端 taskCompatible 对 source='image_search' 的任务按该能力过滤,
但 AgentCapabilities.supported 从未包含它。设备不声明时图搜任务
永远不会被领取,而且是静默的——任务停在 pending、不失败也不报错。

线上设备 #7「采购2」的 capabilities_json 实测确实没有这一项,装机前
核对时发现。

新增 AgentCapabilitiesTest 锁住与服务端 task.ImageSearchCapability
的一致性,并检查清单无重复。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 14:50:59 +08:00
QiuSWandClaude Opus 5 c42e32d49d fix(#277): 首页判定必须由可点相机入口证明
真机核对(2026-09-15)发现个人中心 tab 的根节点同样带
content-desc='拍照搜索',覆盖全屏 [0,0][1080,2354] 且 clickable=false;
而 GoAutoAccessibilityService.visibleTexts() 会把 contentDescription 当
文本收集。原实现只按文案判 HOME,于是个人中心被判成首页,找不到可点入口
后走几何兜底,点到该页顶部最靠右的可点节点——实测那是「设置」按钮。

两处收紧:

- classify 增加 hasClickableCameraEntry,HOME 必须由可点入口证明。
- 几何兜底增加搜索栏证据(顶部区域 content-desc='搜索' 的节点),
  没有搜索栏就返回 null 而不是交出"最靠右的可点节点"。

补充回归测试:文案存在但无可点节点时必须判为 OTHER_PDD。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 11:04:03 +08:00
QiuSWandClaude Opus 5 95be6e7569 feat(agent): 接入 PDD 图搜自动化状态机 (#277)
- 新增 PinduoduoImageSearchAutomation:归位状态机(首页/图搜入口/结果页/
  重试弹窗/订单确认页/其它页面 -> 拍照搜索/返回/拉起应用),候选卡片挑选
  (跳过广告、容忍图片未加载),均以纯函数实现并覆盖单测;订单确认页只
  允许按返回归位,不点击任何支付/下单控件。
- 修复 PinduoduoImageSearchAssetStore.isMostRecent 只比较 jpeg 的问题:
  相册"最近项目"网格实测同时含图片和视频,现在查询 MediaStore.Files
  (图片+视频)交给 ImageSearchMediaRecencyPolicy 判定,若有更新的媒体
  排在前面则明确失败,不猜测、不往后找。
- AgentForegroundService 接入完整执行链路:下载参考图 -> 校验/放大 ->
  写入相册 -> 复核最新一项 -> 归位入口 -> 选相册第一张 -> 等结果页 ->
  跳过广告打开候选 -> 复用现有 CurrentPageIdentityRunner 分享链接识别 +
  PddProductDetailCollector 采集;任务结束(成功/失败)都清理写入的图片。
- 新增错误码 IMAGE_SEARCH_ASSET_NOT_LATEST 及对应人话文案。

真机点击序列、重试弹窗分支、结果页广告识别未经真机验证,仅纯判定逻辑
(ImageSearchRecoveryPolicy / ImageSearchCandidatePolicy /
ImageSearchMediaRecencyPolicy)有 JVM 单测覆盖。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 11:01:00 +08:00
QiuSWandClaude Opus 5 c30b8c4624 fix(#277): 调度垫底改由服务端排序实现,并记录真机判据核对结果
客户端 TaskDispatchPolicy 无法实现「采集 > 图搜」:图搜任务就是
source='image_search' 的普通采集任务,由同一个 /tasks/next 端点下发,
Agent 调用时服务端已经选好了行。原先新增的 hasImageSearchWaiting 参数
和 CHECK_IMAGE_SEARCH 分支从未被传入 true,是不可达的死代码,且注释
声称的优先级与代码顺序相反。

改为在 task.Service.Next 的两处 ORDER BY 加入来源降级:

- 设备专属队列:来源降级作为主键。
- 共享候选池:来源降级排在 device_id 之后——指派设备是人的显式选择,
  必须继续优先于来源降级,否则未指派的采集任务会抢在运营明确路由到
  本机的图搜任务前面。

另外:

- #280 的批次上限去掉 env 覆盖。确认对话框要在提交前禁用按钮,前端
  必然持有同一个数字,env 覆盖只会让两侧静默不一致。
- #277 判据常量按 2026-09-15 真机 dump 核对:首页入口、图搜页四段文案、
  最近项目网格几何、结果页判据全部命中;已记录「唯一可点」过滤为必需
  (个人中心根节点带同一 content-desc 且覆盖全屏)、取景提示文案与移植
  来源不符、相册网格含视频、以及重试弹窗未验证。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 10:51:10 +08:00
QiuSWandClaude Opus 5 51265681c2 feat(android): 图搜任务资产准备、页面判据与调度垫底 #277
- PinduoduoImageSearchAssetStore 补全 sha256/大小/mediaType 校验之外的四点:
  写入前清理自身历史图片、最小边不足 960 时放大、写入后等待媒体库索引并做
  JPEG 头尾标记校验、isMostRecent() 判定自己是否为相册最新一张(不识别图片
  内容,只保证相册第一张即是本次图搜参考图)。纯逻辑部分拆到
  ImageSearchAssetPolicy,JVM 单测覆盖校验/放大边界。
- 新增 PinduoduoImageSearchCriteria:图搜入口页/结果页/"再试一次"弹窗/四列
  最近项目网格的纯文本判据,从同作者已在真机走通的旧项目移植常量,注释标注
  "待真机核对",集中存放便于后续按 dump 结果调整。
- TaskDispatchPolicy.decide 新增可选参数 hasImageSearchWaiting 与
  CHECK_IMAGE_SEARCH 分支,采购 > 采集 > 图搜;默认值保证既有四条分支行为
  逐字节不变,回归测试见 TaskDispatchPolicyTest。
- AgentForegroundService 的 image_search 分支:真实校验相册权限并给出
  IMAGE_SEARCH_PERMISSION_REQUIRED 等人话失败文案;点击入口/选图/等待结果页/
  打开候选的真实自动化状态机本次未接入执行路径,原因见工单——真机 dump 无
  障碍树前置门禁未执行,移植的选择器常量未经核对,不贸然接线。
- TaskHistoryFragment 采集列表行为 source == "image_search" 的任务加一行
  "来源:图搜自动匹配" 标记(#279),不改动既有 collection 布尔分叉结构。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 10:39:41 +08:00
QiuSWandClaude Opus 5 c1eb23e9b1 fix(android): 补充相册权限重新授权入口 #276
MainActivity.onCreate 里的相册权限申请是唯一入口,用户拒绝(尤其"不再询问")后
没有任何补救路径。新增 MediaPermissionPolicy(状态查询 + 跳转应用详情页),并
在"状态"页加相册权限卡片,显示当前状态并提供重新授权/跳转系统设置的入口,供
#277 图搜执行器复用同一查询。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 10:39:27 +08:00
QiuSW def0a86f63 fix(#277): fail image search tasks explicitly until automation is available 2026-09-15 09:59:56 +08:00
QiuSW a4874091a8 feat: prepare validated image search media asset (#277) 2026-09-15 09:42:25 +08:00
QiuSW 9e0dcc254e feat: parse image search task metadata in agent (#277) 2026-09-15 09:40:51 +08:00
QiuSW e01fcd9860 feat: request media permission for image search (#276) 2026-09-14 16:03:21 +08:00
QiuSW edd1cb15df feat(android): add read-only order backfill with bounded scanning (#242) 2026-09-08 15:45:06 +08:00
QiuSW 0cb36b1e73 fix(agent): retain purchase panel recognition after heading scroll (#231) 2026-09-07 11:05:24 +08:00
QiuSW 486dff29fd fix(agent): launch purchase deep links with fresh PDD task (#232) 2026-09-07 10:51:24 +08:00
QiuSW c8e5b99b0c fix(agent): recognize selected spec panels without summary prefix (#231) 2026-09-07 10:40:12 +08:00
QiuSW 666d19ad66 fix(agent): distinguish nested purchase scroll containers (#230) 2026-09-07 10:21:36 +08:00
QiuSW c2c1044dbb fix(agent): recover vertical size grids after horizontal search failure (#230) 2026-09-07 10:07:39 +08:00
QiuSW 4e6afc2d25 fix(agent): search horizontally for exact purchase size (#230) 2026-09-07 09:55:07 +08:00
QiuSW 7b9fcfb8c1 fix(agent): search horizontally for exact purchase color (#230) 2026-09-07 09:41:54 +08:00
QiuSW 1f40a7fcb1 fix(agent): resolve duplicated semantic address cards (#229) 2026-09-05 18:28:47 +08:00
QiuSW c9aaade6e9 fix(agent): deduplicate address entry nodes (#229) 2026-09-05 18:17:54 +08:00
QiuSW 5ee3b62906 fix(agent): retain exact specs across panel transitions (#228) 2026-09-05 17:58:52 +08:00
QiuSW b829a203dc fix(agent): retain purchase panel after address save (#227) 2026-09-05 17:30:40 +08:00
QiuSW 41fe461f94 fix(android): reopen PDD for manual purchase retries (#226) 2026-09-05 17:00:25 +08:00
QiuSW 9124e92ed6 fix(agent): retry purchases in place (#225) 2026-09-05 16:45:52 +08:00
QiuSW 095dbfacbf fix(android): retain verified spec selections (#224) 2026-09-05 16:33:24 +08:00
QiuSW cdcb6930d0 fix(android): revalidate offscreen selected specs (#224) 2026-09-05 16:21:01 +08:00
QiuSW f40331189b fix(android): wait for stable PDD product page (#222) 2026-09-05 12:40:19 +08:00
QiuSW 2895f3d72d fix(purchase): advance past spec confirmation (#220) 2026-09-05 10:54:02 +08:00
QiuSW e4051ed8df feat(purchase): reuse probed PDD page (#219) 2026-09-05 10:34:35 +08:00
QiuSW 5859a819c8 feat(agent): 显示采购结果气泡 (#218) 2026-09-05 10:01:21 +08:00