From 73cb94e0739198ce928ae26111b3b15957eb7d5a Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 3 Sep 2026 12:01:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E8=BF=9B=E5=85=A5=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E8=AF=A6=E6=83=85=E8=AF=BB=E5=8F=96=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=20(#203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../automation/PurchaseLiveAutomation.kt | 27 ++++++++ .../agent/PurchaseLiveAutomationTest.kt | 64 +++++++++++++++++++ 2 files changed, 91 insertions(+) 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 b55e9ea..b13d1dd 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 @@ -121,6 +121,7 @@ class PurchaseLiveAutomation( var backedOutOfChooser = false var restoredFromWechat = false var pddObservedAfterWechatRestore = false + var orderDetailEntryOpened = false var wechatRestorePendingSamples = 0 var consecutiveEmptySnapshots = 0 repeat(ORDER_RESULT_MAX_SAMPLES) { index -> @@ -178,6 +179,23 @@ class PurchaseLiveAutomation( return@repeat } if (!orderContextVisible && !unpaidContextVisible) { + val entries = orderDetailEntryTargets(snapshot) + if (entries.size > 1) { + return unknown("PURCHASE_ORDER_DETAIL_ENTRY_AMBIGUOUS", "订单详情入口不唯一,已停止只读核单") + } + if (entries.size == 1) { + if (orderDetailEntryOpened) { + return unknown("PURCHASE_ORDER_DETAIL_ENTRY_TIMEOUT", "进入订单详情后页面未出现可验证证据") + } + when (driver.clickFresh(entries.single())) { + FreshActionResult.SUCCESS -> Unit + FreshActionResult.AMBIGUOUS -> return unknown("PURCHASE_ORDER_DETAIL_ENTRY_AMBIGUOUS", "订单详情入口不唯一,已停止只读核单") + else -> return unknown("PURCHASE_ORDER_DETAIL_ENTRY_FAILED", "订单详情入口点击失败,已停止只读核单") + } + orderDetailEntryOpened = true + pause(500) + return@repeat + } pause(ORDER_RESULT_SAMPLE_INTERVAL_MS) return@repeat } @@ -375,6 +393,14 @@ class PurchaseLiveAutomation( snapshot.nodes.filter { node -> node.visible && node.enabled && FINAL_SUBMIT_MARKERS.any { node.label == it || node.label.startsWith(it) } }, ) + /** A post-submit navigation target is allowed only when it is one exact, non-payment PDD order-detail entry. */ + private fun orderDetailEntryTargets(snapshot: UiSnapshot): List = uniqueClickable( + snapshot, + snapshot.nodes.filter { node -> + node.visible && node.enabled && node.label in ORDER_DETAIL_ENTRY_MARKERS + }, + ) + private fun pageProblem(snapshot: UiSnapshot) { val labels = snapshot.nodes.filter { it.visible }.map { it.label } PddPageClassifier.classify(snapshot.packageName, snapshot.activityName, labels)?.let { fail(it.code, it.message) } @@ -425,6 +451,7 @@ class PurchaseLiveAutomation( val FINAL_SUBMIT_MARKERS = listOf("提交订单", "现在买,仅", "确认购买") val PAYMENT_MARKERS = listOf("立即支付", "确认支付", "输入支付密码") val UNPAID_MARKERS = listOf("待付款", "待支付", "去支付") + val ORDER_DETAIL_ENTRY_MARKERS = setOf("查看订单", "订单详情") val ORDER_CONTEXT_MARKERS = listOf("订单编号", "订单号", "下单时间", "创建时间") val ORDER_NO = Regex("(?:订单编号|订单号)\\s*[::]?\\s*([A-Za-z0-9-]{6,64})") val ORDER_TIME = Regex("(?:下单时间|创建时间)\\s*[::]?\\s*(20[0-9]{2}[-/.年][0-9]{1,2}[-/.月][0-9]{1,2}日?\\s+[0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)") 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 1f21c1a..fe4a2f8 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 @@ -136,6 +136,52 @@ class PurchaseLiveAutomationTest { assertFalse(driver.clicked.any { it.contains("支付") }) } + @Test + fun `successful order page enters the unique order detail before reading the result`() { + val driver = LiveDriver(orderDetailEntryAfterSubmit = true) + val automation = PurchaseLiveAutomation(driver, pause = {}) + val address = automation.updateShippingAddress("_cg56") + automation.finalConfirmation(input().copy(addressSuffix = "_cg56"), address) + automation.submitOrderOnce() + + val order = automation.readOrderResult() + + assertEquals("PDD-202608210001", order?.orderNo) + assertEquals(1, driver.orderDetailEntryClicks) + assertEquals("order", driver.currentPage) + assertFalse(driver.clicked.any { it.contains("支付") }) + } + + @Test + fun `order detail entered from success page scrolls only to reveal folded result evidence`() { + val driver = LiveDriver(orderDetailEntryAfterSubmit = true, orderDetailEvidenceBelowFold = true) + val automation = PurchaseLiveAutomation(driver, pause = {}) + val address = automation.updateShippingAddress("_cg58") + automation.finalConfirmation(input().copy(addressSuffix = "_cg58"), address) + automation.submitOrderOnce() + + val order = automation.readOrderResult() + + assertEquals("PDD-202608210001", order?.orderNo) + assertEquals(1, driver.orderDetailEntryClicks) + assertEquals(1, driver.genericSwipes) + assertFalse(driver.clicked.any { it.contains("支付") }) + } + + @Test + fun `ambiguous order detail entries stop without navigating`() { + val driver = LiveDriver(orderDetailEntryAfterSubmit = true, duplicateOrderDetailEntry = true) + val automation = PurchaseLiveAutomation(driver, pause = {}) + val address = automation.updateShippingAddress("_cg57") + automation.finalConfirmation(input().copy(addressSuffix = "_cg57"), address) + automation.submitOrderOnce() + + assertEquals(null, automation.readOrderResult()) + assertEquals("PURCHASE_ORDER_DETAIL_ENTRY_AMBIGUOUS", automation.lastOrderReadFailure?.code) + assertEquals(0, driver.orderDetailEntryClicks) + assertFalse(driver.clicked.any { it.contains("支付") }) + } + @Test fun `repeated known payment activity still stops without scrolling or payment clicks`() { val driver = LiveDriver(postSubmitCaptureSequence = listOf("payment", "payment")) @@ -342,6 +388,9 @@ class PurchaseLiveAutomationTest { private val wechatLoginAfterSubmit: Boolean = false, private val wechatRestoreStuck: Boolean = false, private val orderEvidenceBelowFold: Boolean = false, + private val orderDetailEntryAfterSubmit: Boolean = false, + private val orderDetailEvidenceBelowFold: Boolean = false, + private val duplicateOrderDetailEntry: Boolean = false, postSubmitCaptureSequence: List = emptyList(), ) : PurchaseUiDriver { private var page = "confirmation" @@ -358,6 +407,7 @@ class PurchaseLiveAutomationTest { var backCount = 0 var postSubmitBackCount = 0 var pddRestoreCount = 0 + var orderDetailEntryClicks = 0 var postSubmitCaptureCount = 0 val currentPage: String get() = page @@ -393,6 +443,15 @@ class PurchaseLiveAutomationTest { "order" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("time", "下单时间:2026-08-21 10:30:00"), node("pay", "立即支付", clickable = true))) "order-folded" -> snapshot(listOf(node("status", "待付款"), node("pay", "立即支付", clickable = true))) "order-no-time" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("pay", "去支付", clickable = true))) + "success" -> snapshot(buildList { + add(node("success-title", "购买成功")) + add(node("detail-parent", "", clickable = true)) + add(node("detail", "查看订单", parentPath = "detail-parent")) + if (duplicateOrderDetailEntry) { + add(node("detail-parent-2", "", clickable = true)) + add(node("detail-2", "订单详情", parentPath = "detail-parent-2")) + } + }) "chooser" -> UiSnapshot(if (trustedChooser) "android" else "example.untrusted", "com.android.internal.app.ChooserActivity", listOf( node("chooser-title", "选择要使用的应用"), node("wechat-1", "微信"), node("wechat-2", "微信分身"), )) @@ -442,9 +501,14 @@ class PurchaseLiveAutomationTest { page = when { chooserAfterSubmit -> "chooser" wechatLoginAfterSubmit -> "wechat-login" + orderDetailEntryAfterSubmit -> "success" else -> "order" } } + "查看订单", "订单详情" -> { + orderDetailEntryClicks++ + page = if (orderDetailEvidenceBelowFold) "order-folded" else "order" + } } return FreshActionResult.SUCCESS }