From ff925286b94aecf7bd3003fc3f5d68f56c759d36 Mon Sep 17 00:00:00 2001 From: chengma Date: Tue, 18 Aug 2026 09:35:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E5=A4=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E9=80=89=E6=8B=A9=E5=99=A8=E6=A0=B8=E5=8D=95=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=20(#261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pdd_u2_purchase_reconcile_adapter.py | 50 +++++++ .../test_pdd_u2_purchase_reconcile_adapter.py | 122 +++++++++++++++++- 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/client/src/pdd_u2_purchase_reconcile_adapter.py b/client/src/pdd_u2_purchase_reconcile_adapter.py index dd21e11..9c4ff0d 100644 --- a/client/src/pdd_u2_purchase_reconcile_adapter.py +++ b/client/src/pdd_u2_purchase_reconcile_adapter.py @@ -53,6 +53,12 @@ _NON_UNPAID_MARKERS = ("已付款", "交易成功", "交易完成", "已取消", _SAFE_NAVIGATION_LABELS = ("个人中心", "我的订单", "待付款") _ORDER_DETAIL_PAY_LABELS = ("去支付",) _ORDER_DETAIL_CANCEL_LABELS = ("取消订单",) +_ANDROID_CHOOSER_PACKAGE = "android" +_ANDROID_CHOOSER_TITLES = ("选择要使用的应用",) +_ANDROID_CHOOSER_ACTIVITIES = ( + "com.android.internal.app.ChooserActivity", + "com.android.internal.app.ResolverActivity", +) class PddPurchaseReconcileError(RuntimeError): @@ -138,6 +144,26 @@ def _is_single_order_detail(root: ET.Element) -> bool: return pay_count == 1 and cancel_count <= 1 and len(order_numbers) <= 1 +def _is_android_wechat_chooser(root: ET.Element) -> bool: + """严格识别覆盖在 PDD 上方的 Android 微信应用选择器。""" + + if _foreground_package_from_tree(root) != _ANDROID_CHOOSER_PACKAGE: + return False + labels = tuple(_label(node) for node in root.iter("node")) + has_title = any(label in _ANDROID_CHOOSER_TITLES for label in labels) + has_wechat_candidate = any( + label == "微信" or label.startswith("微信分身") for label in labels + ) + return has_title and has_wechat_candidate + + +def _is_known_android_chooser_activity(activity: str) -> bool: + """前台 Activity 只接受 Android 标准选择器,避免误退未知系统页。""" + + normalized = str(activity or "").strip() + return not normalized or normalized in _ANDROID_CHOOSER_ACTIVITIES + + def _parse_bounds(value: str) -> Optional[Bounds]: match = _BOUNDS_PATTERN.fullmatch(str(value or "").strip()) if match is None: @@ -465,6 +491,7 @@ class U2PddPurchaseReconcileAdapter(PddPurchaseReconcileAdapter): checked_foreground = False started_pdd = False pressed_back = False + pressed_chooser_back = False clicked_navigation: set[str] = set() last_reason = "等待 PDD 待付款页稳定" while self._monotonic() <= deadline: @@ -474,6 +501,29 @@ class U2PddPurchaseReconcileAdapter(PddPurchaseReconcileAdapter): last_reason = "读取手机控件树超时" break if not _package_hint_from_tree(root): + if _is_android_wechat_chooser(root): + if pressed_chooser_back: + raise PddPurchaseReconcileError( + "RECONCILE_ANDROID_CHOOSER_STUCK", + "Android 微信应用选择器在一次安全返回后仍未关闭,请人工返回 PDD 核单", + ) + current = device.app_current() + chooser_activity = str(current.get("activity") or "") + if not _is_known_android_chooser_activity( + chooser_activity + ): + raise PddPurchaseReconcileError( + "RECONCILE_ANDROID_CHOOSER_UNRECOGNIZED", + "检测到疑似微信应用选择页,但前台 Activity 不在安全白名单,请人工处理", + ) + if self._monotonic() > deadline: + last_reason = "确认 Android 微信应用选择器超时" + break + device.press("back") + pressed_chooser_back = True + last_reason = "已关闭 Android 微信应用选择器,等待 PDD 待付款详情加载" + self._settle() + continue if not checked_foreground: current_package = _foreground_package_from_tree(root) if not current_package: diff --git a/client/test/test_pdd_u2_purchase_reconcile_adapter.py b/client/test/test_pdd_u2_purchase_reconcile_adapter.py index 0279911..1923a57 100644 --- a/client/test/test_pdd_u2_purchase_reconcile_adapter.py +++ b/client/test/test_pdd_u2_purchase_reconcile_adapter.py @@ -119,10 +119,26 @@ def xml_package_page(package: str, text: str = "") -> str: ) +def xml_android_wechat_chooser() -> str: + return ( + '' + '' + '' + '' + '' + ) + + class TransitionDevice(DetailDevice): - def __init__(self, pages, current_package="com.tencent.mm"): + def __init__( + self, + pages, + current_package="com.tencent.mm", + current_activity="", + ): super().__init__(pages) self.current_package = current_package + self.current_activity = current_activity self.dump_calls = 0 self.back_calls = 0 @@ -133,7 +149,10 @@ class TransitionDevice(DetailDevice): def app_current(self): self.app_current_calls += 1 - return {"package": self.current_package} + return { + "package": self.current_package, + "activity": self.current_activity, + } def press(self, key): if key == "back": @@ -304,6 +323,105 @@ class PddU2PurchaseReconcileAdapterTest(unittest.TestCase): self.assertEqual(device.app_current_calls, 0) self.assertLess(clock.now, 30) + def test_android_wechat_chooser_is_closed_once_before_reading_detail(self): + device = TransitionDevice( + [ + xml_android_wechat_chooser(), + xml_package_page("com.xunmeng.pinduoduo", "页面加载中"), + xml_detail("待付款"), + ], + current_package="android", + current_activity="com.android.internal.app.ChooserActivity", + ) + clock = FakeClock() + adapter = U2PddPurchaseReconcileAdapter( + "serial", + device_service=FakeDeviceService(device), + settle_seconds=1, + transition_timeout_seconds=30, + monotonic=clock.monotonic, + sleep=clock.sleep, + ) + + adapter._open_unpaid_orders(device) + + self.assertEqual(device.back_calls, 1) + self.assertEqual(device.app_start_calls, 0) + self.assertEqual(device.app_current_calls, 1) + + def test_android_wechat_chooser_stops_when_one_back_does_not_close_it(self): + device = TransitionDevice( + [xml_android_wechat_chooser(), xml_android_wechat_chooser()], + current_package="android", + current_activity="com.android.internal.app.ResolverActivity", + ) + clock = FakeClock() + adapter = U2PddPurchaseReconcileAdapter( + "serial", + device_service=FakeDeviceService(device), + settle_seconds=1, + transition_timeout_seconds=30, + monotonic=clock.monotonic, + sleep=clock.sleep, + ) + + with self.assertRaises(PddPurchaseReconcileError) as captured: + adapter._open_unpaid_orders(device) + + self.assertEqual( + captured.exception.code, "RECONCILE_ANDROID_CHOOSER_STUCK" + ) + self.assertEqual(device.back_calls, 1) + self.assertEqual(device.app_start_calls, 0) + + def test_android_wechat_chooser_with_unknown_activity_stops_safely(self): + device = TransitionDevice( + [xml_android_wechat_chooser()], + current_package="android", + current_activity="com.example.UnknownSystemActivity", + ) + clock = FakeClock() + adapter = U2PddPurchaseReconcileAdapter( + "serial", + device_service=FakeDeviceService(device), + settle_seconds=1, + transition_timeout_seconds=30, + monotonic=clock.monotonic, + sleep=clock.sleep, + ) + + with self.assertRaises(PddPurchaseReconcileError) as captured: + adapter._open_unpaid_orders(device) + + self.assertEqual( + captured.exception.code, + "RECONCILE_ANDROID_CHOOSER_UNRECOGNIZED", + ) + self.assertEqual(device.back_calls, 0) + self.assertEqual(device.app_start_calls, 0) + + def test_unknown_android_page_is_not_treated_as_wechat_chooser(self): + unknown_system_page = xml_package_page("android", "系统设置") + device = TransitionDevice( + [unknown_system_page, xml_detail("待付款")], + current_package="android", + current_activity="com.android.settings.Settings", + ) + clock = FakeClock() + adapter = U2PddPurchaseReconcileAdapter( + "serial", + device_service=FakeDeviceService(device), + settle_seconds=1, + transition_timeout_seconds=30, + monotonic=clock.monotonic, + sleep=clock.sleep, + ) + + adapter._open_unpaid_orders(device) + + self.assertEqual(device.back_calls, 0) + self.assertEqual(device.app_start_calls, 1) + def test_payment_back_is_pressed_at_most_once_while_page_changes(self): payment_page = xml_package_page( "com.xunmeng.pinduoduo", "立即支付"