fix(#154): return safely from WeChat to PDD
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 38
|
||||
versionName = "0.9.25"
|
||||
versionCode = 39
|
||||
versionName = "0.9.26"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+9
@@ -409,6 +409,15 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto
|
||||
|
||||
override fun backPurchase(): Boolean = performGlobalAction(GLOBAL_ACTION_BACK)
|
||||
|
||||
override fun bringPddToForeground(): Boolean {
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(PDD_PACKAGE) ?: return false
|
||||
launchIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
return runCatching {
|
||||
startActivity(launchIntent)
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
override fun swipeSpec(direction: SwipeDirection, anchor: SnapshotNode?): Boolean {
|
||||
if (anchor == null) return swipe(SemanticTarget.SPEC_PANEL, direction)
|
||||
val root = rootInActiveWindow ?: return false
|
||||
|
||||
+10
-1
@@ -111,16 +111,24 @@ class PurchaseLiveAutomation(
|
||||
val labels = linkedSetOf<String>()
|
||||
var backedOutOfPayment = false
|
||||
var backedOutOfChooser = false
|
||||
var restoredFromWechat = false
|
||||
repeat(60) { index ->
|
||||
val snapshot = driver.capture()
|
||||
val currentLabels = snapshot.nodes.filter { it.visible }.map { it.label }
|
||||
currentLabels.forEach(labels::add)
|
||||
if (isKnownAndroidWechatChooser(snapshot, currentLabels)) {
|
||||
if (backedOutOfChooser || !driver.backPurchase()) return null
|
||||
backedOutOfChooser = true
|
||||
pause(500)
|
||||
return@repeat
|
||||
}
|
||||
if (snapshot.packageName == WECHAT_PACKAGE) {
|
||||
if (restoredFromWechat || !driver.bringPddToForeground()) return null
|
||||
restoredFromWechat = true
|
||||
pause(500)
|
||||
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) }
|
||||
@@ -344,6 +352,7 @@ class PurchaseLiveAutomation(
|
||||
|
||||
private companion object {
|
||||
const val PDD_PACKAGE = "com.xunmeng.pinduoduo"
|
||||
const val WECHAT_PACKAGE = "com.tencent.mm"
|
||||
val MASKED_PHONE = Regex("(?<![0-9])[0-9]{3}\\*{4}[0-9]{4}(?![0-9])")
|
||||
val FINAL_SUBMIT_MARKERS = listOf("提交订单", "现在买,仅", "确认购买")
|
||||
val PAYMENT_MARKERS = listOf("立即支付", "确认支付", "输入支付密码")
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ interface PurchaseUiDriver {
|
||||
fun swipePurchase(direction: SwipeDirection, durationMs: Long): Boolean
|
||||
fun swipePurchaseIn(target: SnapshotNode, direction: SwipeDirection, durationMs: Long): Boolean
|
||||
fun backPurchase(): Boolean
|
||||
fun bringPddToForeground(): Boolean = false
|
||||
}
|
||||
|
||||
data class PurchaseExecutionInput(
|
||||
|
||||
@@ -116,9 +116,42 @@ class PurchaseLiveAutomationTest {
|
||||
|
||||
assertEquals("PDD-202608210001", order?.orderNo)
|
||||
assertEquals(2, driver.postSubmitBackCount)
|
||||
assertEquals("order", driver.currentPage)
|
||||
assertFalse(driver.clicked.any { it.startsWith("微信") || it.contains("支付") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WeChat login is not touched and PDD is restored once before reading order detail`() {
|
||||
val driver = LiveDriver(wechatLoginAfterSubmit = true)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
val address = automation.updateShippingAddress("_cg22")
|
||||
automation.finalConfirmation(input().copy(addressSuffix = "_cg22"), address)
|
||||
automation.submitOrderOnce()
|
||||
|
||||
val order = automation.readOrderResult()
|
||||
|
||||
assertEquals("PDD-202608210001", order?.orderNo)
|
||||
assertEquals("2026-08-21T02:30:00Z", order?.submittedAt)
|
||||
assertEquals(1, driver.pddRestoreCount)
|
||||
assertEquals(1, driver.postSubmitBackCount)
|
||||
assertEquals("order", driver.currentPage)
|
||||
assertFalse(driver.clicked.any { it.contains("微信") || it.contains("登录") || it.contains("支付") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WeChat still foreground after one restore stays result unknown without gestures`() {
|
||||
val driver = LiveDriver(wechatLoginAfterSubmit = true, wechatRestoreStuck = true)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
val address = automation.updateShippingAddress("_cg23")
|
||||
automation.finalConfirmation(input().copy(addressSuffix = "_cg23"), address)
|
||||
automation.submitOrderOnce()
|
||||
|
||||
assertEquals(null, automation.readOrderResult())
|
||||
assertEquals(1, driver.pddRestoreCount)
|
||||
assertEquals(0, driver.genericSwipes)
|
||||
assertEquals(0, driver.postSubmitBackCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unrecognized chooser is never backed out automatically`() {
|
||||
val driver = LiveDriver(chooserAfterSubmit = true, trustedChooser = false)
|
||||
@@ -177,6 +210,8 @@ class PurchaseLiveAutomationTest {
|
||||
private val saveStaysInEdit: Boolean = false,
|
||||
private val savedTransitionWithoutLegacyContext: Boolean = false,
|
||||
private val savedTransitionHidesSuffix: Boolean = false,
|
||||
private val wechatLoginAfterSubmit: Boolean = false,
|
||||
private val wechatRestoreStuck: Boolean = false,
|
||||
) : PurchaseUiDriver {
|
||||
private var page = "confirmation"
|
||||
private var addressVisible = !addressClipped
|
||||
@@ -189,6 +224,8 @@ class PurchaseLiveAutomationTest {
|
||||
var lastInputTargetPath: String? = null
|
||||
var backCount = 0
|
||||
var postSubmitBackCount = 0
|
||||
var pddRestoreCount = 0
|
||||
val currentPage: String get() = page
|
||||
|
||||
override fun capture(): UiSnapshot = when (page) {
|
||||
"panel" -> snapshot(listOf(
|
||||
@@ -216,6 +253,7 @@ class PurchaseLiveAutomationTest {
|
||||
node("chooser-title", "选择要使用的应用"), node("wechat-1", "微信"), node("wechat-2", "微信分身"),
|
||||
))
|
||||
"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", "微信登录")))
|
||||
else -> {
|
||||
val nodes = mutableListOf(
|
||||
node("root", "", bounds = NodeBounds(0, 0, 1080, 2200)),
|
||||
@@ -252,7 +290,14 @@ class PurchaseLiveAutomationTest {
|
||||
"138****5678" -> page = "panel"
|
||||
"修改" -> page = "edit"
|
||||
"保存" -> if (!saveStaysInEdit) page = if (savedTransitionWithoutLegacyContext) "saved-transition" else "panel"
|
||||
"提交订单" -> { submitClicks++; page = if (chooserAfterSubmit) "chooser" else "order" }
|
||||
"提交订单" -> {
|
||||
submitClicks++
|
||||
page = when {
|
||||
chooserAfterSubmit -> "chooser"
|
||||
wechatLoginAfterSubmit -> "wechat-login"
|
||||
else -> "order"
|
||||
}
|
||||
}
|
||||
}
|
||||
return FreshActionResult.SUCCESS
|
||||
}
|
||||
@@ -286,6 +331,12 @@ class PurchaseLiveAutomationTest {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun bringPddToForeground(): Boolean {
|
||||
pddRestoreCount++
|
||||
if (!wechatRestoreStuck) page = "payment"
|
||||
return true
|
||||
}
|
||||
|
||||
private fun snapshot(nodes: List<SnapshotNode>) = UiSnapshot(PDD, ACTIVITY, nodes)
|
||||
private fun node(
|
||||
path: String,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: 71d8707aba33d07f329e1cc86a087383e555738d
|
||||
synchronized_at: 2026-08-29T03:47:02Z
|
||||
wiki_revision: d88d5d5aeabdb66bb67a2ec4b32da912fbf0629f
|
||||
synchronized_at: 2026-08-29T07:11:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
@@ -149,7 +149,7 @@ synchronized_at: 2026-08-29T01:39:44Z
|
||||
- 地址编辑页可以同时存在收货人、手机号和详细地址等多个输入框;Agent 只选择与“详细地址”标签纵向重叠且位于其右侧的唯一输入框,不能用页面输入框总数或顺序猜测。
|
||||
- 点击创建订单前,Agent 必须先在本地事务保存 `order_submit_started`、不可逆时间、稳定请求 ID 和不含地址全文的最终确认快照,再用同一请求 ID通知服务端;两侧成功后才允许精确点击唯一创建订单按钮一次。
|
||||
- 进入不可逆边界后,进程重启、断网、点击结果不明或无法取得唯一订单号/下单时间时只允许只读核单并进入 `order_result_unknown`,禁止再次点击;任务与订单正式关联仍以完整 PDD 订单号为准。
|
||||
- 创建订单后出现 Android 多微信应用选择器时,Agent 只有在系统包和 `ChooserActivity` / `ResolverActivity` 白名单、已知选择器标题、微信候选三类证据同时成立时,才允许按一次返回,绝不点击微信候选。回到 PDD 已知支付 Activity 或明确支付动作页面后最多再返回一次,再只读读取订单详情;任一页面不在白名单、一次返回后仍未离开或无法取得唯一订单号与下单时间时保持 `order_result_unknown`,禁止重下单或支付。
|
||||
- 创建订单后出现 Android 多微信应用选择器时,Agent 只有在系统包和 `ChooserActivity` / `ResolverActivity` 白名单、已知选择器标题、微信候选三类证据同时成立时,才允许按一次返回,绝不点击微信候选。若已进入真实微信包 `com.tencent.mm`,Agent 不点击、不输入、不登录、不支付、不强制停止微信,只允许用一次无参数 PDD 启动 Intent 把既有 PDD 任务栈拉回前台;第二次仍见微信或拉起失败即保持结果未知。回到 PDD 已知支付 Activity 或明确支付动作页面后最多再返回一次,再只读读取唯一订单号和下单时间;任一页面不在白名单、恢复动作重复、无法到达订单详情或结果不唯一时保持 `order_result_unknown`,禁止重下单或支付。
|
||||
- Agent 本地 SQLite/Outbox 负责断网和重启恢复,服务端以 `task_id + task_attempt_id` 幂等接收并保存最终事实。
|
||||
- 人工支付复核只记录 `paid` / `unpaid`;系统不执行或识别支付。快递单号与回填状态属于采购任务,后续物流工单实现。
|
||||
- 采购任务领取时同时占用设备租约和可选 PDD 账号租约;租约过期后才可释放并重新领取。设备还存在采集任务时不能领取采购任务。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Android-Agent-API-Contract
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.-
|
||||
wiki_revision: 9f04eb9bb7284e525a58309b9c47cdb593a1e09c
|
||||
synchronized_at: 2026-08-29T03:49:41Z
|
||||
wiki_revision: 0fbfd430daab3d703649dfab34100be107581b81
|
||||
synchronized_at: 2026-08-29T07:11:55Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
@@ -532,7 +532,7 @@ Android #42/#36 使用本地 SQLite 保存恢复与重传所需的任务、attem
|
||||
|
||||
创建订单前先在本地事务保存 `order_submit_started`、不可逆时间、稳定的 `orderSubmitRequestId` 和脱敏最终确认快照,再调用服务端同名接口;只有两侧标记完成并重新校验商品、规格、数量、单价、地址后缀、PDD 包/Activity 与唯一创建订单按钮后,才点击一次。重启时重放同一标记请求并只读核单;无法取得唯一未付款订单号和 PDD 下单时间时提交 `order_result_unknown`。支付文字仅用于识别未付款/离开支付页,永不点击。服务端数据库仍是最终事实来源;双方均不保存原始控件树、截图、PDD 凭据或完整收货地址。
|
||||
|
||||
创建订单后若出现 Android 多微信应用选择器,只读核单器必须同时确认前台包为 `android` / `com.android.intentresolver`、Activity 为白名单 `ChooserActivity` / `ResolverActivity`、页面出现已知系统选择器标题且至少一个候选以“微信”开头,才允许执行一次系统返回;不得点击任何微信候选。随后若前台为 PDD `com.xunmeng.pinduoduo.app_pay.core.PayActivity` 或当前页面出现支付动作文字,最多再返回一次。返回后只读解析唯一订单号和下单时间;选择器或支付页重复出现、白名单不成立、返回失败、结果不唯一或超时均返回未知结果,禁止再次点击创建订单、取消订单或支付。
|
||||
创建订单后若出现 Android 多微信应用选择器,只读核单器必须同时确认前台包为 `android` / `com.android.intentresolver`、Activity 为白名单 `ChooserActivity` / `ResolverActivity`、页面出现已知系统选择器标题且至少一个候选以“微信”开头,才允许执行一次系统返回;不得点击任何微信候选。若前台已经是精确微信包 `com.tencent.mm`,只读核单器不得点击、输入、登录、支付或强制停止微信,只允许执行一次无参数 PDD 启动 Intent 并等待既有 PDD 任务栈回到前台;拉起失败或第二次仍见微信立即返回未知结果。随后若前台为 PDD `com.xunmeng.pinduoduo.app_pay.core.PayActivity` 或当前页面出现支付动作文字,最多再返回一次。返回后只读解析唯一订单号和下单时间;选择器或支付页重复出现、白名单不成立、恢复动作重复、无法到达订单详情、结果不唯一或超时均返回未知结果,禁止再次点击创建订单、取消订单或支付。
|
||||
|
||||
| 错误码 | 普通提示 |
|
||||
|---|---|
|
||||
|
||||
Reference in New Issue
Block a user