diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b1c9bad..6a81d34 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "cn.ilapage.goauto.agent" minSdk = 23 targetSdk = 34 - versionCode = 85 - versionName = "0.9.72" + versionCode = 86 + versionName = "0.9.73" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt index 867c878..c660fb9 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt @@ -186,7 +186,7 @@ object PddScreenParser { // purchase/order/payment controls must never become collection click targets. private val nonConfigurableClickDenylist = listOf("提交订单", "确认订单", "支付", "付款") - fun parse(snapshot: UiSnapshot, config: PddCollectorConfig, goodsId: String, evidence: PageEvidence?, purchaseContext: PurchasePanelContext? = null): ParsedPddScreen { + fun parse(snapshot: UiSnapshot, config: PddCollectorConfig, goodsId: String, evidence: PageEvidence?, purchaseContext: PurchasePanelContext? = null, collectionContext: Boolean = false): ParsedPddScreen { val visibleNodes = snapshot.nodes.filter { it.visible } val visible = visibleNodes.mapNotNull { node -> val descendants = descendants(node, visibleNodes) @@ -323,12 +323,23 @@ object PddScreenParser { (headings.size >= 2 || (continuedPurchasePanel && headings.isNotEmpty())) && dimensions.any { it.values.isNotEmpty() } && hasClose && hasQuantityControls && hasPaymentArea && hasOrderSubmitAction + // Collection can start before any selection summary exists and with only + // one dimension visible. Keep this exception out of purchase parsing. + val singleDimensionCollectionPanel = collectionContext && purchaseContext == null && + snapshot.packageName == PDD_PACKAGE && problem == null && !hasSelectionSummary && + evidence != null && snapshot.packageName == evidence.packageName && + snapshot.activityName == evidence.activityName && + snapshot.nodes.any { it.visible && it.matches(evidence.selector) } && + boundedScrollables.size == 1 && headedPanelScrollable != null && headings.size == 1 && + dimensions.size == 1 && dimensions.single().values.isNotEmpty() && + hasClose && hasQuantityControls && hasPaymentArea && hasOrderSubmitAction // Some selected-spec panels omit both the "已选" prefix and a confirm // button. Keep the same structural evidence required for checkout. val specPanelType = when { quickConfirmationEvidence -> SpecPanelType.QUICK_CONFIRMATION orderConfirmationEvidence -> SpecPanelType.ORDER_CONFIRMATION structuredSelectionPanel -> SpecPanelType.NORMAL_SCROLLABLE + singleDimensionCollectionPanel -> SpecPanelType.NORMAL_SCROLLABLE panelScrollable != null && (hasSelectionSummary || hasSubmitHint || (hasPanelTitle && hasPanelAction)) -> SpecPanelType.NORMAL_SCROLLABLE hasSelectionSummary && hasPanelTitle && hasPanelAction -> SpecPanelType.NON_SCROLLABLE_CONFIRMATION // Some PDD builds expose the complete selector as non-scrollable @@ -1765,7 +1776,7 @@ class PddProductDetailCollector( } private fun parse(goodsId: String, config: PddCollectorConfig, evidence: PageEvidence) = - PddScreenParser.parse(driver.capture(), config, goodsId, evidence) + PddScreenParser.parse(driver.capture(), config, goodsId, evidence, collectionContext = true) private fun screenSignature(screen: ParsedPddScreen): List = screen.sourceNodes.asSequence() .filter(SnapshotNode::visible) diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt index a087ba0..3ca9d93 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt @@ -27,6 +27,52 @@ import org.junit.Assert.assertTrue import org.junit.Test class PddProductDetailCollectorTest { + private fun singleDimensionPanel(): UiSnapshot { + val snapshot = prefixlessPanel() + return snapshot.copy(nodes = snapshot.nodes.filterNot { + it.path.startsWith("scroll/size") || it.path == "info/summary" + }.map { + if (it.path == "root") it.copy(resourceId = "android:id/content", className = "android.widget.FrameLayout") else it + }) + } + + @Test fun `single dimension without summary is recognized only during collection`() { + for (heading in listOf("颜色分类", "尺码")) { + val snapshot = singleDimensionPanel().let { original -> + original.copy(nodes = original.nodes.map { + if (it.path == "scroll/color-title") it.copy(text = heading) else it + }) + } + assertFalse(PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence()).specPanelOpen) + val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence(), collectionContext = true) + assertEquals(SpecPanelType.NORMAL_SCROLLABLE, parsed.specPanelType) + assertEquals("scroll", parsed.specPanelContainer?.path) + assertEquals(1, parsed.dimensions.size) + assertEquals(null, parsed.selectedSummary) + assertFalse(parsed.hasSelectionSummary) + } + } + + @Test fun `single dimension collection requires all structural and page evidence`() { + val snapshot = singleDimensionPanel() + for (missing in listOf("close", "plus", "minus", "info/quantity", "payment", "submit", "scroll/color-title", "scroll/color", "root")) { + val changed = snapshot.copy(nodes = snapshot.nodes.filterNot { it.path == missing }) + assertFalse(missing, PddScreenParser.parse(changed, config(), GOODS_ID, evidence(), collectionContext = true).specPanelOpen) + } + val secondScroll = node("other-scroll", "", 0, 500, 1080, 700, scrollable = true) + val secondSubmit = node("other-submit", "提交订单", 10, 2200, 300, 2300, clickable = true) + for (changed in listOf( + snapshot.copy(nodes = snapshot.nodes + secondScroll), + snapshot.copy(nodes = snapshot.nodes + secondSubmit), + snapshot.copy(packageName = "example.other"), + snapshot.copy(activityName = "example.OtherActivity"), + snapshot.copy(nodes = snapshot.nodes + node("captcha", "请完成安全验证", 0, 0, 600, 100)), + )) { + assertFalse(PddScreenParser.parse(changed, config(), GOODS_ID, evidence(), collectionContext = true).specPanelOpen) + } + assertFalse(PddScreenParser.parse(snapshot, config(), GOODS_ID, null, collectionContext = true).specPanelOpen) + } + private fun prefixlessPanel(): UiSnapshot = UiSnapshot(PDD_PACKAGE, ACTIVITY, listOf( node("root", "", 0, 0, 1080, 2376), node("close", "关闭", 970, 270, 1050, 350, clickable = true),