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
This commit is contained in:
+26
-3
@@ -44,10 +44,17 @@ object ImageSearchRecoveryPolicy {
|
||||
/** 归位动作上限,参考 cmroubao_old 的经验值,防止死循环。 */
|
||||
const val MAX_RECOVERY_ACTIONS = 50
|
||||
|
||||
/**
|
||||
* 首页搜索栏的 content-desc,真机实测 `[381,166][765,223]` TextView 携带。
|
||||
* 只用于证明"当前确实在带搜索栏的首页",不作为点击目标。
|
||||
*/
|
||||
const val SEARCH_BAR_CONTENT_DESCRIPTION = "搜索"
|
||||
|
||||
fun classify(
|
||||
packageName: String?,
|
||||
visibleTexts: Collection<String>,
|
||||
sortControlCount: Int,
|
||||
hasClickableCameraEntry: Boolean,
|
||||
): ImageSearchPageKind {
|
||||
if (packageName != PDD_PACKAGE) return ImageSearchPageKind.NON_PDD
|
||||
if (PinduoduoImageSearchCriteria.isRetryDialog(visibleTexts)) return ImageSearchPageKind.RETRY_DIALOG
|
||||
@@ -56,7 +63,12 @@ object ImageSearchRecoveryPolicy {
|
||||
}
|
||||
if (PinduoduoImageSearchCriteria.isImageSearchEntryPage(visibleTexts)) return ImageSearchPageKind.IMAGE_SEARCH_ENTRY
|
||||
if (visibleTexts.any { text -> ORDER_CONFIRM_MARKERS.any(text::contains) }) return ImageSearchPageKind.ORDER_CONFIRM
|
||||
if (visibleTexts.any { it == PinduoduoImageSearchCriteria.CAMERA_ENTRY_CONTENT_DESCRIPTION }) return ImageSearchPageKind.HOME
|
||||
// `[必须]` HOME 只能由**可点**的相机入口证明,不能只看文案。真机核对
|
||||
// (2026-09-15)发现个人中心 tab 的根节点同样带 content-desc='拍照搜索',
|
||||
// 覆盖全屏且 clickable=false,而 visibleTexts() 会把 contentDescription
|
||||
// 当文本收集。只看文案会把个人中心判成首页,随后几何兜底就会点到该页
|
||||
// 顶部最靠右的可点节点——实测那是「设置」按钮。
|
||||
if (hasClickableCameraEntry) return ImageSearchPageKind.HOME
|
||||
return ImageSearchPageKind.OTHER_PDD
|
||||
}
|
||||
|
||||
@@ -152,11 +164,14 @@ class PinduoduoImageSearchAutomation(
|
||||
return ImageSearchAutomationResult(false, it.code, it.message)
|
||||
}
|
||||
val sortControlCount = visible.count { it.label in SORT_CONTROL_LABELS }
|
||||
val kind = ImageSearchRecoveryPolicy.classify(currentPackage(), texts, sortControlCount)
|
||||
val cameraEntry = uniqueCameraEntry(visible)
|
||||
val kind = ImageSearchRecoveryPolicy.classify(
|
||||
currentPackage(), texts, sortControlCount, hasClickableCameraEntry = cameraEntry != null,
|
||||
)
|
||||
when (ImageSearchRecoveryPolicy.nextAction(kind)) {
|
||||
ImageSearchRecoveryAction.NONE -> return ImageSearchAutomationResult(true, "OK", "已在图搜入口")
|
||||
ImageSearchRecoveryAction.CLICK_CAMERA -> {
|
||||
val target = uniqueCameraEntry(visible)
|
||||
val target = cameraEntry
|
||||
?: return ImageSearchAutomationResult(false, "IMAGE_SEARCH_ENTRY_NOT_FOUND", "找不到唯一可点的拍照搜索入口")
|
||||
driver.clickFreshDetailed(target)
|
||||
}
|
||||
@@ -184,6 +199,14 @@ class PinduoduoImageSearchAutomation(
|
||||
}
|
||||
byText.singleOrNull()?.let { return it }
|
||||
if (byText.isNotEmpty()) return null
|
||||
// `[必须]` 几何兜底只在确实看得到顶部搜索栏时才允许。否则任何 PDD 页面都会
|
||||
// 交出"顶部最靠右的可点节点",在个人中心那是「设置」。实测首页搜索栏含
|
||||
// content-desc='搜索' 的节点,个人中心没有。
|
||||
val hasSearchBar = visible.any {
|
||||
it.visible && it.bounds.top <= PinduoduoImageSearchCriteria.CAMERA_ENTRY_FALLBACK_MAX_TOP_PX &&
|
||||
it.contentDescription?.trim() == ImageSearchRecoveryPolicy.SEARCH_BAR_CONTENT_DESCRIPTION
|
||||
}
|
||||
if (!hasSearchBar) return null
|
||||
val fallback = visible.filter {
|
||||
it.enabled && it.clickable && it.bounds.top <= PinduoduoImageSearchCriteria.CAMERA_ENTRY_FALLBACK_MAX_TOP_PX
|
||||
}
|
||||
|
||||
+21
-8
@@ -15,45 +15,58 @@ class ImageSearchRecoveryPolicyTest {
|
||||
|
||||
@Test
|
||||
fun `classify recognizes non-pdd package regardless of texts`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.android.launcher", entryTexts, 0)
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.android.launcher", entryTexts, 0, hasClickableCameraEntry = false)
|
||||
assertEquals(ImageSearchPageKind.NON_PDD, kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classify recognizes image search entry page`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", entryTexts, 0)
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", entryTexts, 0, hasClickableCameraEntry = false)
|
||||
assertEquals(ImageSearchPageKind.IMAGE_SEARCH_ENTRY, kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classify recognizes results page over other markers`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", resultsTexts, 4)
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", resultsTexts, 4, hasClickableCameraEntry = false)
|
||||
assertEquals(ImageSearchPageKind.IMAGE_SEARCH_RESULTS, kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classify recognizes retry dialog`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", retryTexts, 0)
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", retryTexts, 0, hasClickableCameraEntry = false)
|
||||
assertEquals(ImageSearchPageKind.RETRY_DIALOG, kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classify recognizes order confirm page by read-only markers without treating them as click targets`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", listOf("提交订单", "收货地址"), 0)
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", listOf("提交订单", "收货地址"), 0, hasClickableCameraEntry = false)
|
||||
assertEquals(ImageSearchPageKind.ORDER_CONFIRM, kind)
|
||||
// order confirm only ever maps to BACK -- never a click action.
|
||||
assertEquals(ImageSearchRecoveryAction.BACK, ImageSearchRecoveryPolicy.nextAction(kind))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classify recognizes home page via unique camera content description`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", homeTexts, 0)
|
||||
fun `classify recognizes home page only when a clickable camera entry exists`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify(
|
||||
"com.xunmeng.pinduoduo", homeTexts, 0, hasClickableCameraEntry = true,
|
||||
)
|
||||
assertEquals(ImageSearchPageKind.HOME, kind)
|
||||
}
|
||||
|
||||
// 真机核对(2026-09-15):个人中心 tab 的根节点也带 content-desc='拍照搜索',
|
||||
// 覆盖全屏且不可点,而 visibleTexts() 会把 contentDescription 当文本收集。
|
||||
// 只看文案会把个人中心判成首页,几何兜底随后会点到「设置」。
|
||||
@Test
|
||||
fun `classify refuses home when camera text is present but nothing clickable carries it`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify(
|
||||
"com.xunmeng.pinduoduo", homeTexts, 0, hasClickableCameraEntry = false,
|
||||
)
|
||||
assertEquals(ImageSearchPageKind.OTHER_PDD, kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classify falls back to other pdd page`() {
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", listOf("商品详情", "加入购物车"), 0)
|
||||
val kind = ImageSearchRecoveryPolicy.classify("com.xunmeng.pinduoduo", listOf("商品详情", "加入购物车"), 0, hasClickableCameraEntry = false)
|
||||
assertEquals(ImageSearchPageKind.OTHER_PDD, kind)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user