fix(agent): stabilize post-order WeChat recovery (#163)
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
+22
-5
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user