fix(agent): stabilize post-order WeChat recovery (#163)

This commit is contained in:
QiuSW
2026-08-31 10:01:10 +08:00
parent 67f3a74956
commit d8e13da83c
4 changed files with 105 additions and 9 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "cn.ilapage.goauto.agent"
minSdk = 23
targetSdk = 34
versionCode = 41
versionName = "0.9.28"
versionCode = 42
versionName = "0.9.29"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -106,14 +106,22 @@ class PurchaseLiveAutomation(
}
}
/** Only reads the current PDD order/payment flow; the sole navigation is one safe Back. */
/** Reads the order flow with bounded chooser/payment Back actions and one verified PDD foreground request. */
fun readOrderResult(): PurchaseOrderEvidence? {
val labels = linkedSetOf<String>()
var backedOutOfPayment = false
var backedOutOfChooser = false
var restoredFromWechat = false
repeat(60) { index ->
var consecutiveEmptySnapshots = 0
repeat(ORDER_RESULT_MAX_SAMPLES) { index ->
val snapshot = driver.capture()
if (snapshot.packageName.isNullOrBlank()) {
consecutiveEmptySnapshots++
if (consecutiveEmptySnapshots >= ORDER_RESULT_MAX_EMPTY_SAMPLES) return null
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
return@repeat
}
consecutiveEmptySnapshots = 0
val currentLabels = snapshot.nodes.filter { it.visible }.map { it.label }
if (isKnownAndroidWechatChooser(snapshot, currentLabels)) {
if (backedOutOfChooser || !driver.backPurchase()) return null
@@ -128,10 +136,15 @@ class PurchaseLiveAutomation(
return@repeat
}
if (snapshot.packageName != PDD_PACKAGE) return null
currentLabels.forEach(labels::add)
parseOrderEvidence(labels)?.let { return it }
val paymentVisible = isKnownPddPaymentActivity(snapshot) ||
currentLabels.any { label -> PAYMENT_MARKERS.any(label::contains) }
val orderContextVisible = currentLabels.any { label -> ORDER_CONTEXT_MARKERS.any(label::contains) }
if (!paymentVisible && !orderContextVisible) {
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
return@repeat
}
currentLabels.forEach(labels::add)
parseOrderEvidence(labels)?.let { return it }
if (paymentVisible && !backedOutOfPayment) {
backedOutOfPayment = true
if (!driver.backPurchase()) return null
@@ -141,7 +154,7 @@ class PurchaseLiveAutomation(
} else if (index > 0 && index % 15 == 0) {
driver.swipePurchase(SwipeDirection.UP, 400)
}
pause(200)
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
}
return null
}
@@ -357,6 +370,7 @@ class PurchaseLiveAutomation(
val FINAL_SUBMIT_MARKERS = listOf("提交订单", "现在买,仅", "确认购买")
val PAYMENT_MARKERS = listOf("立即支付", "确认支付", "输入支付密码")
val UNPAID_MARKERS = listOf("待付款", "待支付", "去支付")
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})?)")
val ANDROID_CHOOSER_PACKAGES = setOf("android", "com.android.intentresolver")
@@ -367,5 +381,8 @@ class PurchaseLiveAutomation(
val ANDROID_CHOOSER_ACTIVITY_SUFFIXES = listOf(".ChooserActivity", ".ResolverActivity")
val ANDROID_CHOOSER_TITLES = setOf("选择要使用的应用", "使用以下应用打开", "完成操作时使用")
val PDD_PAYMENT_ACTIVITIES = setOf("com.xunmeng.pinduoduo.app_pay.core.PayActivity")
const val ORDER_RESULT_MAX_SAMPLES = 60
const val ORDER_RESULT_MAX_EMPTY_SAMPLES = 15
const val ORDER_RESULT_SAMPLE_INTERVAL_MS = 200L
}
}
@@ -16,6 +16,7 @@ interface PurchaseUiDriver {
fun swipePurchaseIn(target: SnapshotNode, direction: SwipeDirection, durationMs: Long): Boolean
fun pullDownGoodsPage(): Boolean = swipePurchase(SwipeDirection.DOWN, 550)
fun backPurchase(): Boolean
/** Requests the existing PDD task stack in the foreground; callers must verify the observed package afterwards. */
fun bringPddToForeground(): Boolean = false
}
@@ -138,6 +138,69 @@ class PurchaseLiveAutomationTest {
assertFalse(driver.clicked.any { it.contains("微信") || it.contains("登录") || it.contains("支付") })
}
@Test
fun `empty accessibility transitions wait for WeChat and verify delayed PDD foreground`() {
val driver = LiveDriver(
postSubmitCaptureSequence = listOf("empty", "wechat-login", "empty", "payment", "order"),
)
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg29")
automation.finalConfirmation(input().copy(addressSuffix = "_cg29"), address)
automation.submitOrderOnce()
val order = automation.readOrderResult()
assertEquals("PDD-202608210001", order?.orderNo)
assertEquals(1, driver.pddRestoreCount)
assertEquals(1, driver.postSubmitBackCount)
assertEquals("order", driver.currentPage)
assertFalse(driver.clicked.any { it.contains("微信") || it.contains("登录") || it.contains("支付") })
}
@Test
fun `continuous empty accessibility transition has a fixed sample limit`() {
val driver = LiveDriver(postSubmitCaptureSequence = List(15) { "empty" })
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg30")
automation.finalConfirmation(input().copy(addressSuffix = "_cg30"), address)
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals(15, driver.postSubmitCaptureCount)
assertEquals(0, driver.pddRestoreCount)
assertEquals(0, driver.postSubmitBackCount)
assertEquals(0, driver.genericSwipes)
}
@Test
fun `stable unknown application still stops reconciliation immediately`() {
val driver = LiveDriver(postSubmitCaptureSequence = listOf("unknown"))
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg31")
automation.finalConfirmation(input().copy(addressSuffix = "_cg31"), address)
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals(1, driver.postSubmitCaptureCount)
assertEquals(0, driver.pddRestoreCount)
assertEquals(0, driver.postSubmitBackCount)
assertEquals(0, driver.genericSwipes)
}
@Test
fun `PDD home after WeChat restore is never scrolled as order context`() {
val driver = LiveDriver(postSubmitCaptureSequence = listOf("wechat-login", "pdd-home"))
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg32")
automation.finalConfirmation(input().copy(addressSuffix = "_cg32"), address)
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals(1, driver.pddRestoreCount)
assertEquals(0, driver.postSubmitBackCount)
assertEquals(0, driver.genericSwipes)
}
@Test
fun `WeChat still foreground after one restore stays result unknown without gestures`() {
val driver = LiveDriver(wechatLoginAfterSubmit = true, wechatRestoreStuck = true)
@@ -212,8 +275,11 @@ class PurchaseLiveAutomationTest {
private val savedTransitionHidesSuffix: Boolean = false,
private val wechatLoginAfterSubmit: Boolean = false,
private val wechatRestoreStuck: Boolean = false,
postSubmitCaptureSequence: List<String> = emptyList(),
) : PurchaseUiDriver {
private var page = "confirmation"
private val sequencedPostSubmit = postSubmitCaptureSequence.isNotEmpty()
private val pendingPostSubmitPages = postSubmitCaptureSequence.toMutableList()
private var addressVisible = !addressClipped
private var address = "广东省广州市天园街道骏景花园骏晖轩1202-old"
val clicked = mutableListOf<String>()
@@ -225,9 +291,18 @@ class PurchaseLiveAutomationTest {
var backCount = 0
var postSubmitBackCount = 0
var pddRestoreCount = 0
var postSubmitCaptureCount = 0
val currentPage: String get() = page
override fun capture(): UiSnapshot = when (page) {
override fun capture(): UiSnapshot {
if (submitClicks > 0) {
postSubmitCaptureCount++
if (pendingPostSubmitPages.isNotEmpty()) page = pendingPostSubmitPages.removeAt(0)
}
return currentSnapshot()
}
private fun currentSnapshot(): UiSnapshot = when (page) {
"panel" -> snapshot(listOf(
node("title", "收货地址"),
node("modify-parent", "", clickable = true),
@@ -254,6 +329,9 @@ class PurchaseLiveAutomationTest {
))
"payment" -> UiSnapshot(PDD, "com.xunmeng.pinduoduo.app_pay.core.PayActivity", listOf(node("pay", "立即支付", clickable = true)))
"wechat-login" -> UiSnapshot("com.tencent.mm", "com.tencent.mm.plugin.account.ui.SimpleLoginUI", listOf(node("wechat-login", "微信登录")))
"empty" -> UiSnapshot(null, null, emptyList())
"unknown" -> UiSnapshot("example.untrusted", "example.untrusted.UnknownActivity", emptyList())
"pdd-home" -> UiSnapshot(PDD, "com.xunmeng.pinduoduo.ui.activity.MainFrameActivity", listOf(node("home", "拼多多首页")))
else -> {
val nodes = mutableListOf(
node("root", "", bounds = NodeBounds(0, 0, 1080, 2200)),
@@ -333,7 +411,7 @@ class PurchaseLiveAutomationTest {
override fun bringPddToForeground(): Boolean {
pddRestoreCount++
if (!wechatRestoreStuck) page = "payment"
if (!wechatRestoreStuck && !sequencedPostSubmit) page = "payment"
return true
}