docs: import wiki at afc651f75a3a

ila
2026-08-07 16:37:12 +08:00
parent af36ee0400
commit 325d089273
+92
@@ -0,0 +1,92 @@
<!-- docs-wiki-sync:docs/tasks/T-274.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
> 同步来源:[`docs/tasks/T-274.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-274.md) · commit `afc651f75a3a`
---
id: T-274
title: 修复重复候选签名导致商品节点无法打开
phase: 2
deps:
- T-255
- T-271
status: DONE
created: 2026-08-01
context_ref: c8c4914
work_branch: null
write_paths:
- docs/tasks/T-274.md
- docs/current-state.md
- docs/04-architecture.md
- docs/05-coding-rules.md
- android-buyer/app/src/main/**
- android-buyer/app/src/test/**
---
## 问题 / 背景
真机任务三轮都在 `pdd_browse_candidates` 首次打开候选时返回
`TRANSIENT_AUTOMATION`。每轮诊断均为 `cards=4 distinct=2 attempts=1 scrolls=0`,且
规格、身份和 VLM 计数全部为 0。
代码确认读取阶段按标题语义生成 `signature`,打开阶段重新扫描后使用
`filter(signature).singleOrNull()` 选择节点。4 张卡片只有 2 个签名时,重复签名使
`singleOrNull()` 返回空,未执行 `ACTION_CLICK`。此外自动化用相同 signature 记录已尝试
候选,即使打开成功,同标题但不同商品的后续卡片也会被跳过。
## 行为契约
1. 候选读取除语义 signature 外,为每张已接受卡片生成仅限当前结果视口使用的 locator:
recycler child index、接受顺序、容器 bounds、点击目标相对 path,以及对完整语义、节点
class/resource/path 的 SHA-256 指纹。locator 不包含原始文本,也不进入证据、outbox、
Admin 或持久化进度。
2. 打开候选时重新读取安全结果页,先同时复核 signature、完整内容指纹和原容器位置;
原位置变化时只允许通过唯一内容指纹和高比例 bounds 重叠重定位。零命中或多命中均
失败关闭,不能凭标题任取第一个节点。
3. bounds 只用于验证同一节点,动作仍对唯一、可见、enabled 的无障碍节点执行一次
`ACTION_CLICK`;禁止坐标点击、VLM 选择或保留 `AccessibilityNodeInfo` 跨快照引用。
4. 本轮尝试去重使用不含位置的完整卡片 attempt key;语义 signature 继续用于观测和证据。
同标题但完整卡片不同的商品可分别尝试,同一完整卡片在滚动后不能反复尝试。
5. 打开结果必须区分 `PAGE_CHANGED/LOCATOR_MISSING/LOCATOR_NOT_FOUND/LOCATOR_AMBIGUOUS/
ACTION_REJECTED`;重复 signature 被 locator 成功消歧单独计数。所有结果只映射既有安全
失败边界,不把失败伪装成 SKU、身份或 VLM 错误。
6. 初次候选就绪至少需要连续两次非空读取产生相同 locator stability keys,避免使用仍在
渲染的临时结构;等待受既有 `candidateReadyPollLimit` 和轮次总预算约束。
## 方案
1. 扩展 `PinduoduoCandidateCard` 的可空临时 locator,并增加纯 Kotlin locator 生成、
重定位和 bounds 重叠策略。
2. `BuyerAccessibilityService.candidateNodes()` 在同次树遍历中计算容器/点击目标 path 和
指纹;打开接口返回结构化 outcome,不再以 signature 的 `singleOrNull()` 点击。
3. `PinduoduoCandidateAutomation` 使用卡片 attempt key 去重、等待两帧稳定,并将结构化
outcome 聚合到追加诊断键,外层失败码继续保持兼容。
4. 用纯策略测试覆盖 `cards=4 distinct=2`、位置稳定、位置变化唯一重定位、重复定位、
失配和 bounds 不重叠;用自动化 fake 覆盖每种打开 outcome 和同标题不同卡片。
## 验收要点
- [x] 4 张卡片只有 2 个语义 signature 时,指定卡片可由 locator 唯一打开。
- [x] 同标题但完整语义不同的两张卡片可在同一轮分别尝试,不因 signature 去重跳过。
- [x] locator 缺失、失配、多命中、页面变化和 `ACTION_CLICK=false` 分别形成诊断计数。
- [x] 页面重排或指纹变化时不按标题任点,也不使用坐标、VLM 或旧节点引用兜底。
- [x] 旧 fake/fixture 可通过显式 locator 构造迁移,候选证据/API/数据库契约不变。
- [x] Android Debug/Release 单测、lint 和两种 APK 构建通过。
## 边界
- 不重新启用图片加载硬过滤,不修改八项候选容器判定。
- 不改变三轮数量、浏览预算、SKU/价格硬约束、goods_id 或分享链接流程。
- 不修复订单 dry-run 的独立候选选择契约,不增加 VLM 调用或下单权限。
## 执行记录
- 2026-08-01:根据真机三轮完全相同的 `cards=4 distinct=2 attempts=1` 诊断创建任务,
状态 `TODO`。直接失败点为 `openCandidate=false`;重复 signature 已被诊断证明,现有代码
尚不能区分重复签名、节点不存在和 `ACTION_CLICK=false`。
- 2026-08-01:完成临时 locator、纯 Kotlin 重定位策略、两帧稳定读取和完整内容 attempt
key。无障碍服务重新扫描后只点击唯一节点;重复 signature 可成功消歧,位置变化仅允许
唯一指纹加 80% bounds 重叠。外层失败码保持兼容,事件追加 `openPageChanged`、
`openLocatorMissing`、`openLocatorNotFound`、`openLocatorAmbiguous`、
`openActionRejected` 和 `openDisambiguated` 六个非秘密计数。
- 2026-08-01:Debug/Release 各 457 项单测、`lintDebug`、`assembleDebug`、
`assembleRelease` 全部通过,状态改为 `DONE`。未修改容器八项判定、三轮预算、VLM、
evidence/API/数据库或订单 dry-run;真实失败任务仍需安装本 APK 后复跑确认真机计数。