diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b396214..891447d 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 = 36 - versionName = "0.9.23" + versionCode = 37 + versionName = "0.9.24" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt index 1d04859..28e8566 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseLiveAutomation.kt @@ -73,7 +73,7 @@ class PurchaseLiveAutomation( val snapshot = driver.capture() pageProblem(snapshot) if (snapshot.packageName != PDD_PACKAGE) fail("PURCHASE_CONFIRMATION_LOST", "最终提交前页面已经变化,禁止创建订单") - if (!hasSavedAddressEvidence(snapshot, address.expectedAddress, address.suffix)) { + if (!hasFinalSavedAddressEvidence(snapshot, address.expectedAddress, address.suffix)) { fail("PURCHASE_ADDRESS_UPDATE_FAILED", "收货地址保存后复核失败,未创建订单") } val submit = finalSubmitTargets(snapshot) @@ -171,12 +171,12 @@ class PurchaseLiveAutomation( val save = uniqueClickable(stable, stable.nodes.filter { it.visible && it.enabled && it.label == "保存" }) if (save.size != 1) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存按钮不唯一,未创建订单") click(save.single(), "保存地址") - val savedEvidence = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存超时,未创建订单") { snapshot -> - hasSavedAddressEvidence(snapshot, expected, suffix) - } - if (finalSubmitTargets(savedEvidence).size != 1) { + val savedEvidence = waitForStableSavedAddress(expected, suffix) + if (!hasFinalSavedAddressEvidence(savedEvidence, expected, suffix)) { if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单") - waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { finalSubmitTargets(it).size == 1 } + waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { + hasFinalSavedAddressEvidence(it, expected, suffix) + } } return ShippingAddressProof(expected, suffix) } @@ -184,11 +184,6 @@ class PurchaseLiveAutomation( private fun hasSavedAddressEvidence(snapshot: UiSnapshot, expected: String, suffix: String): Boolean { if (snapshot.packageName != PDD_PACKAGE || shippingAddressEditors(snapshot).isNotEmpty()) return false val visible = snapshot.nodes.filter { it.visible } - val addressContext = visible.count { normalizeAddressText(it.label) == "收货地址" } == 1 || - (mergedDirect(visible.filter { it.enabled && MASKED_PHONE.containsMatchIn(it.label) }).size == 1 && - finalSubmitTargets(snapshot).size == 1) - if (!addressContext) return false - val normalizedExpected = normalizeAddressText(expected) val fullMatches = mergedDirect(visible.filter { normalizeAddressText(it.label).contains(normalizedExpected) && hasExactTaskSuffix(it.label, suffix) @@ -200,6 +195,25 @@ class PurchaseLiveAutomation( return suffixMatches.size == 1 } + private fun hasFinalSavedAddressEvidence(snapshot: UiSnapshot, expected: String, suffix: String): Boolean = + hasSavedAddressEvidence(snapshot, expected, suffix) && finalSubmitTargets(snapshot).size == 1 + + private fun waitForStableSavedAddress(expected: String, suffix: String): UiSnapshot { + var consecutiveMatches = 0 + repeat(50) { + val snapshot = driver.capture() + pageProblem(snapshot) + if (hasSavedAddressEvidence(snapshot, expected, suffix)) { + consecutiveMatches++ + if (consecutiveMatches >= 2) return snapshot + } else { + consecutiveMatches = 0 + } + pause(200) + } + fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存超时,未创建订单") + } + private fun normalizeAddressText(value: String): String = value.filterNot(Char::isWhitespace) private fun hasExactTaskSuffix(value: String, suffix: String): Boolean = diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt index 3b829d1..6fd92f5 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseLiveAutomationTest.kt @@ -51,6 +51,19 @@ class PurchaseLiveAutomationTest { assertEquals(0, driver.submitClicks) } + @Test + fun `saved address transition without legacy title or phone is accepted before final confirmation`() { + val driver = LiveDriver(savedTransitionWithoutLegacyContext = true) + val automation = PurchaseLiveAutomation(driver, pause = {}) + + val address = automation.updateShippingAddress("_cg21") + val final = automation.finalConfirmation(input().copy(addressSuffix = "_cg21"), address) + + assertEquals("_cg21", final.addressSuffix) + assertEquals(1, driver.backCount) + assertEquals(0, driver.submitClicks) + } + @Test fun `edit field containing suffix cannot impersonate post save evidence`() { val driver = LiveDriver(saveStaysInEdit = true) @@ -64,24 +77,20 @@ class PurchaseLiveAutomationTest { @Test fun `missing confirmation suffix fails before order creation`() { val driver = LiveDriver(hideConfirmationSuffix = true) - val automation = PurchaseLiveAutomation(driver, pause = {}) - val address = automation.updateShippingAddress("_cg19") - val error = runCatching { automation.finalConfirmation(input().copy(addressSuffix = "_cg19"), address) } + val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg19") } .exceptionOrNull() as PurchaseLiveException - assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code) + assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code) assertEquals(0, driver.submitClicks) } @Test fun `duplicate independent confirmation suffixes fail before order creation`() { val driver = LiveDriver(splitConfirmationAddress = true, duplicateConfirmationSuffix = true) - val automation = PurchaseLiveAutomation(driver, pause = {}) - val address = automation.updateShippingAddress("_cg20") - val error = runCatching { automation.finalConfirmation(input().copy(addressSuffix = "_cg20"), address) } + val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg20") } .exceptionOrNull() as PurchaseLiveException - assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code) + assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code) assertEquals(0, driver.submitClicks) } @@ -166,6 +175,7 @@ class PurchaseLiveAutomationTest { private val hideConfirmationSuffix: Boolean = false, private val duplicateConfirmationSuffix: Boolean = false, private val saveStaysInEdit: Boolean = false, + private val savedTransitionWithoutLegacyContext: Boolean = false, ) : PurchaseUiDriver { private var page = "confirmation" private var addressVisible = !addressClipped @@ -196,6 +206,10 @@ class PurchaseLiveAutomationTest { node("editor-address", address, className = "android.widget.EditText", bounds = NodeBounds(180, 390, 900, 480)), node("save-parent", "", clickable = true), node("save", "保存", parentPath = "save-parent"), )) + "saved-transition" -> snapshot(listOf( + node("transition-title", "选择收货信息"), + node("address-suffix", address.substring(address.lastIndexOf("_cg"))), + )) "order" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("time", "下单时间:2026-08-21 10:30:00"), node("pay", "立即支付", clickable = true))) "chooser" -> UiSnapshot(if (trustedChooser) "android" else "example.untrusted", "com.android.internal.app.ChooserActivity", listOf( node("chooser-title", "选择要使用的应用"), node("wechat-1", "微信"), node("wechat-2", "微信分身"), @@ -236,7 +250,7 @@ class PurchaseLiveAutomationTest { when (target.label) { "138****5678" -> page = "panel" "修改" -> page = "edit" - "保存" -> if (!saveStaysInEdit) page = "panel" + "保存" -> if (!saveStaysInEdit) page = if (savedTransitionWithoutLegacyContext) "saved-transition" else "panel" "提交订单" -> { submitClicks++; page = if (chooserAfterSubmit) "chooser" else "order" } } return FreshActionResult.SUCCESS