From f15a390e2009d782b88025d75c3ecb52cfe1666c Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Fri, 18 Sep 2026 10:58:55 +0800 Subject: [PATCH] fix(android): stabilize order backfill list entry (#307) --- .../automation/GoAutoAccessibilityService.kt | 8 +-- .../agent/automation/OrderBackfillScanner.kt | 54 +++++++++++++++---- .../agent/service/AgentForegroundService.kt | 13 ++++- .../ilapage/goauto/agent/OrderBackfillTest.kt | 48 +++++++++++++++++ docs/03-business-rules-and-glossary.md | 10 +++- 5 files changed, 116 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt index 59c1168..b476286 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt @@ -194,15 +194,17 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto return swipeNode(node, direction) } - /** Backfill never uses ancestor clicks or coordinate/gesture fallbacks. */ - fun clickBackfill(target: SnapshotNode): Boolean { + /** Only the unique All navigation tab may use a gesture; order actions never do. */ + fun clickBackfill(target: SnapshotNode, allTabConfirmed: Boolean = false): Boolean { val page = capture() BackfillPagePolicy.validate(page) val fresh = page.nodes.singleOrNull { it.path == target.path && it.label == target.label && it.bounds == target.bounds && it.className == target.className } ?: return false - val allowed = BackfillPagePolicy.cards(page).any { it.path == fresh.path } || + val allTab = BackfillPagePolicy.allTab(page)?.path == fresh.path + val allowed = BackfillPagePolicy.cards(page, allTabConfirmed).any { it.path == fresh.path } || BackfillPagePolicy.expansion(page)?.path == fresh.path || (fresh.label == "全部" && page.nodes.any { it.label in setOf("我的订单", "全部订单") }) + if (allTab && !fresh.clickable) return dispatchCenterTap(Rect(fresh.bounds.left, fresh.bounds.top, fresh.bounds.right, fresh.bounds.bottom)) if (!allowed || !BackfillPagePolicy.safe(page, fresh)) return false val root = rootInActiveWindow ?: return false if (root.packageName?.toString() != BackfillPagePolicy.PDD) return false diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/OrderBackfillScanner.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/OrderBackfillScanner.kt index 7d6c42f..8ef762e 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/OrderBackfillScanner.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/OrderBackfillScanner.kt @@ -21,6 +21,19 @@ object BackfillPagePolicy { fun list(page: UiSnapshot): Boolean = page.nodes.any { it.visible && it.label == "全部" && it.selected } && page.nodes.any { it.visible && it.label in setOf("我的订单", "全部订单") } fun detail(page: UiSnapshot): Boolean = page.nodes.any { it.visible && (it.label == "订单详情" || it.label.contains("订单编号")) } + // Only the navigation tab may use a bounded gesture, never an order action. + fun allTab(page: UiSnapshot): SnapshotNode? { + if (detail(page) || page.nodes.none { it.visible && it.label in setOf("我的订单", "全部订单") }) return null + val tab = page.nodes.filter { it.visible && it.enabled && it.label == "全部" }.singleOrNull() ?: return null + val peers = page.nodes.filter { it.visible && it.label in setOf("待付款", "待分享", "待发货", "待收货", "待评价") && + it.bounds.top < tab.bounds.bottom && it.bounds.bottom > tab.bounds.top } + if (peers.map { it.label }.distinct().size < 2 || peers.any { it.bounds.left < tab.bounds.right }) return null + return tab.takeIf { safe(page, it.copy(clickable = true)) } + } + + fun confirmedList(page: UiSnapshot, tabConfirmed: Boolean): Boolean = list(page) || + (tabConfirmed && allTab(page) != null && page.nodes.none { it.visible && it.selected && + it.label in setOf("待付款", "待分享", "待发货", "待收货", "待评价") }) fun expansion(page: UiSnapshot): SnapshotNode? { if (!detail(page) || page.nodes.none { it.visible && it.label.contains("订单编号") }) return null val order = page.nodes.first { it.visible && it.label.contains("订单编号") } @@ -28,8 +41,8 @@ object BackfillPagePolicy { return page.nodes.filter { it.label == "展开" && safe(page, it) && it.bounds.top >= order.bounds.top && it.bounds.top < snapshot.bounds.bottom && it.bounds.bottom > snapshot.bounds.top }.singleOrNull() } - fun cards(page: UiSnapshot): List { - if (!list(page)) return emptyList() + fun cards(page: UiSnapshot, tabConfirmed: Boolean = false): List { + if (!confirmedList(page, tabConfirmed)) return emptyList() return page.nodes.filter { node -> if (!safe(page, node)) return@filter false if (node.label in setOf("订单详情", "查看详情")) return@filter true @@ -64,17 +77,38 @@ class OrderBackfillScanner( private val submit: (BackfillItem) -> Unit, private val progress: (Int) -> Unit, ) { + private var tabConfirmed = false + private fun listReady(page: UiSnapshot) = BackfillPagePolicy.confirmedList(page, tabConfirmed) + private fun awaitOrders(): UiSnapshot { + var previous: String? = null + repeat(8) { + checkActive() + val page = driver.capture() + if (page.packageName == BackfillPagePolicy.PDD && page.activityName?.startsWith(BackfillPagePolicy.PDD) == true) { + BackfillPagePolicy.validate(page) + if (BackfillPagePolicy.list(page) || BackfillPagePolicy.allTab(page) != null) { + val signature = page.nodes.filter { it.visible && it.label in setOf("我的订单", "全部订单", "全部", "待付款", "待发货", "待收货", "待评价") } + .joinToString { "${it.label}:${it.bounds}:${it.selected}" } + if (previous == signature) return page + previous = signature + } else previous = null + } else previous = null + driver.pause() + } + error("等待订单列表加载超时,未完整扫描") + } fun scan(): String { checkActive() + tabConfirmed = false driver.openOrders() - driver.pause() - var page = read() + var page = awaitOrders() if (!BackfillPagePolicy.list(page)) { - val tab = page.nodes.filter { it.label == "全部" && BackfillPagePolicy.safe(page, it) }.singleOrNull() - check(page.nodes.any { it.label in setOf("我的订单", "全部订单") } && tab != null) { "未识别我的订单-全部" } + val tab = BackfillPagePolicy.allTab(page) + check(tab != null) { "未识别我的订单-全部" } act { driver.click(tab) } - page = read() - check(BackfillPagePolicy.list(page)) { "无法确认全部订单标签" } + tabConfirmed = true + page = awaitOrders() + check(listReady(page)) { "无法确认全部订单标签" } } if (page.nodes.any { it.visible && it.label == "暂无订单" }) return "扫描完成,未发现订单" val seenCards = mutableSetOf() @@ -82,8 +116,8 @@ class OrderBackfillScanner( var noProgress = 0 while (window.checked < OrderBackfillWindow.MAX_ORDERS) { checkActive() - check(BackfillPagePolicy.list(page)) { "返回后未识别全部订单列表" } - val card = BackfillPagePolicy.cards(page).firstOrNull { BackfillPagePolicy.fingerprint(page, it) !in seenCards } + check(listReady(page)) { "返回后未识别全部订单列表" } + val card = BackfillPagePolicy.cards(page, tabConfirmed).firstOrNull { BackfillPagePolicy.fingerprint(page, it) !in seenCards } if (card == null) { if (++noProgress >= 3) return finish("列表无进展或卡片无法安全识别,未完整扫描") checkActive() diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt index ffae726..f9f20fe 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt @@ -323,7 +323,10 @@ class AgentForegroundService : Service() { private fun publishBackfill(state: OrderBackfillState) { backfillState = state sendBroadcast(Intent(ACTION_BACKFILL_STATE).setPackage(packageName)) - updateNotification(if (state.running) "订单回填 · 已检查 ${state.checked}" else "订单回填已停止,请查看采购页结果") + updateNotification(if (state.running) "订单回填 · 已检查 ${state.checked}" else "订单回填:${state.message}") + if (!state.running) android.os.Handler(android.os.Looper.getMainLooper()).post { + android.widget.Toast.makeText(this, "订单回填:${state.message}", android.widget.Toast.LENGTH_LONG).show() + } } private fun requestOrderBackfill(days: String, confirmedAt: Long) { @@ -358,13 +361,19 @@ class AgentForegroundService : Service() { checkActive = ::checkActive, ) val driver = object : cn.ilapage.goauto.agent.automation.BackfillDriver { + private var allTabConfirmed = false override fun openOrders() { checkActive() startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://mobile.yangkeduo.com/orders.html")) .setPackage(cn.ilapage.goauto.agent.automation.BackfillPagePolicy.PDD).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } override fun capture() = accessibility.capture() - override fun click(node: cn.ilapage.goauto.agent.automation.SnapshotNode): Boolean { checkActive(); return accessibility.clickBackfill(node) } + override fun click(node: cn.ilapage.goauto.agent.automation.SnapshotNode): Boolean { + checkActive() + val clicked = accessibility.clickBackfill(node, allTabConfirmed) + if (clicked && node.label == "全部") allTabConfirmed = true + return clicked + } override fun scroll(): Boolean { checkActive(); return accessibility.scrollBackfill() } override fun back(): Boolean { checkActive() diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/OrderBackfillTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/OrderBackfillTest.kt index e0d2e22..a73d7d1 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/OrderBackfillTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/OrderBackfillTest.kt @@ -259,6 +259,54 @@ class OrderBackfillTest { } private val item = BackfillItem("_cg7", "TEST-000007", null) + private fun missingSelectedPage() = page( + node("0/0", "我的订单").copy(bounds = NodeBounds(0, 0, 500, 50)), + node("0/1", "全部").copy(clickable = false, bounds = NodeBounds(0, 60, 100, 100)), + node("0/2", "待发货").copy(bounds = NodeBounds(100, 60, 200, 100)), + node("0/3", "待收货").copy(bounds = NodeBounds(200, 60, 300, 100))) + + @Test fun `missing selected requires unique safe tab and explicit confirmation`() { + val p = missingSelectedPage() + assertNotNull(BackfillPagePolicy.allTab(p)) + assertFalse(BackfillPagePolicy.confirmedList(p, false)) + assertTrue(BackfillPagePolicy.confirmedList(p, true)) + assertNull(BackfillPagePolicy.allTab(p.copy(nodes = p.nodes + p.nodes[1].copy(path = "0/9")))) + assertFalse(BackfillPagePolicy.confirmedList(p.copy(nodes = p.nodes.map { if (it.label == "待收货") it.copy(selected = true) else it }), true)) + assertNull(BackfillPagePolicy.allTab(p.copy(nodes = p.nodes + p.nodes[1].copy(path = "0/9", text = "去支付")))) + } + + @Test fun `scanner waits for stable list and taps only all when selected is missing`() { + var reads = 0 + var clicks = 0 + val driver = object : BackfillDriver { + override fun openOrders() = Unit + override fun pause() = Unit + override fun capture(): UiSnapshot = if (++reads < 3) UiSnapshot(null, null, emptyList()) else missingSelectedPage() + override fun click(node: SnapshotNode): Boolean { assertEquals("全部", node.label); clicks++; return true } + override fun scroll() = false + override fun back() = false + } + val reason = OrderBackfillScanner(driver, OrderBackfillWindow("2", now), {}, { fail("no upload") }, {}).scan() + assertEquals(1, clicks) + assertTrue(reads >= 6) + assertTrue(reason.contains("未完整扫描")) + } + + @Test fun `missing order page times out without any click`() { + var pauses = 0 + val driver = object : BackfillDriver { + override fun openOrders() = Unit + override fun pause() { pauses++ } + override fun capture() = UiSnapshot(null, null, emptyList()) + override fun click(node: SnapshotNode): Boolean { fail("unexpected click"); return false } + override fun scroll() = false + override fun back() = false + } + try { OrderBackfillScanner(driver, OrderBackfillWindow("2", now), {}, {}, {}).scan(); fail() } + catch (e: IllegalStateException) { assertTrue(e.message!!.contains("加载超时")) } + assertEquals(8, pauses) + } + private fun failure(code: String) = BackfillResult(0, 7, "failed", code, "", 0, null, null, "") private fun success() = BackfillResult(0, 7, "backfilled", "BACKFILLED", "order_created", 3, item.pddOrderNo, null, "page") private fun scanner(driver: FakeDriver, items: MutableList) = OrderBackfillScanner(driver, OrderBackfillWindow("2", now), {}, items::add, {}) diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index c23459a..953ba1a 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -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: f353b476add5081d4d7a1493e5454b6400518923 -synchronized_at: 2026-09-18T02:18:13Z +wiki_revision: 4b61d5dcc75c459831e6b1f0d556b09d6acd0310 +synchronized_at: 2026-09-18T02:58:16Z # 业务规则与术语 @@ -630,3 +630,9 @@ Web 唯一展示位置为“采集采购 → SYB 同步记录”:列表状态 订单回填状态独立于采购成功、支付复核及物流 writeback_status。实付金额只存 Admin,SYB cost=0 为接口固定参数,不以金额推断付款。SYB 接口会同时更新采购状态/平台/时间,不能视作纯展示修改。 采购管理增加独立状态列、批量回填和详情补偿;复用既有访问权限,不增支付确认或审批。批量受理与最终成功分开展示;重试采购和回填分别筛选勾选项。远端无原子CAS,对系统外人工并发修改/超长延迟请求不能承诺绝对互斥;有冲突应人工核对,禁止强制覆盖。 +## Agent 回填订单入口兼容(#307) + +- 打开我的订单后最多采样8次,每次间隔1秒,连续两次订单导航结构一致才继续;登录/风控仍立即停止,超时给出明确原因。 +- 保留全部标签selected=true的原路径。PDD不提供selected属性时,要求订单标题、唯一全部标签以及同排至少两个订单状态标签;仅该安全导航标签允许一次中心手势,不扩大到订单卡片、地址、支付或其他操作。 +- 本轮主动点击全部后复核订单导航结构;本轮上下文不能跨扫描复用,出现其他已选中订单状态时停止。卡片识别与支付禁令保持不变。 +- 回填结束/失败通过现有通知与Toast显示脱敏原因;详细结果仍在Agent采购页。本变更不调整服务端回填接口、采集或采购流程。