fix: 增强双微信选择器识别 (#263)
This commit is contained in:
@@ -53,12 +53,20 @@ _NON_UNPAID_MARKERS = ("已付款", "交易成功", "交易完成", "已取消",
|
|||||||
_SAFE_NAVIGATION_LABELS = ("个人中心", "我的订单", "待付款")
|
_SAFE_NAVIGATION_LABELS = ("个人中心", "我的订单", "待付款")
|
||||||
_ORDER_DETAIL_PAY_LABELS = ("去支付",)
|
_ORDER_DETAIL_PAY_LABELS = ("去支付",)
|
||||||
_ORDER_DETAIL_CANCEL_LABELS = ("取消订单",)
|
_ORDER_DETAIL_CANCEL_LABELS = ("取消订单",)
|
||||||
_ANDROID_CHOOSER_PACKAGE = "android"
|
_ANDROID_CHOOSER_PACKAGES = ("android", "com.android.intentresolver")
|
||||||
_ANDROID_CHOOSER_TITLES = ("选择要使用的应用",)
|
_ANDROID_CHOOSER_TITLES = (
|
||||||
|
"选择要使用的应用",
|
||||||
|
"使用以下应用打开",
|
||||||
|
"完成操作时使用",
|
||||||
|
)
|
||||||
_ANDROID_CHOOSER_ACTIVITIES = (
|
_ANDROID_CHOOSER_ACTIVITIES = (
|
||||||
"com.android.internal.app.ChooserActivity",
|
"com.android.internal.app.ChooserActivity",
|
||||||
"com.android.internal.app.ResolverActivity",
|
"com.android.internal.app.ResolverActivity",
|
||||||
)
|
)
|
||||||
|
_ANDROID_CHOOSER_ACTIVITY_SUFFIXES = (
|
||||||
|
".ChooserActivity",
|
||||||
|
".ResolverActivity",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PddPurchaseReconcileError(RuntimeError):
|
class PddPurchaseReconcileError(RuntimeError):
|
||||||
@@ -144,24 +152,50 @@ def _is_single_order_detail(root: ET.Element) -> bool:
|
|||||||
return pay_count == 1 and cancel_count <= 1 and len(order_numbers) <= 1
|
return pay_count == 1 and cancel_count <= 1 and len(order_numbers) <= 1
|
||||||
|
|
||||||
|
|
||||||
def _is_android_wechat_chooser(root: ET.Element) -> bool:
|
def _node_accessibility_values(node: ET.Element) -> tuple[str, ...]:
|
||||||
"""严格识别覆盖在 PDD 上方的 Android 微信应用选择器。"""
|
"""分别返回 text 和 content-desc,避免拼接后破坏精确识别。"""
|
||||||
|
|
||||||
if _foreground_package_from_tree(root) != _ANDROID_CHOOSER_PACKAGE:
|
return tuple(
|
||||||
return False
|
dict.fromkeys(
|
||||||
labels = tuple(_label(node) for node in root.iter("node"))
|
value.strip()
|
||||||
has_title = any(label in _ANDROID_CHOOSER_TITLES for label in labels)
|
for value in (
|
||||||
has_wechat_candidate = any(
|
node.get("text", ""),
|
||||||
label == "微信" or label.startswith("微信分身") for label in labels
|
node.get("content-desc", ""),
|
||||||
|
)
|
||||||
|
if value.strip()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_android_wechat_chooser_semantics(root: ET.Element) -> bool:
|
||||||
|
"""只判断选择器标题和微信候选,不在这里猜测前台应用。"""
|
||||||
|
|
||||||
|
has_title = False
|
||||||
|
has_wechat_candidate = False
|
||||||
|
for node in root.iter("node"):
|
||||||
|
values = _node_accessibility_values(node)
|
||||||
|
if any(value in _ANDROID_CHOOSER_TITLES for value in values):
|
||||||
|
has_title = True
|
||||||
|
if any(value.startswith("微信") for value in values):
|
||||||
|
has_wechat_candidate = True
|
||||||
return has_title and has_wechat_candidate
|
return has_title and has_wechat_candidate
|
||||||
|
|
||||||
|
|
||||||
def _is_known_android_chooser_activity(activity: str) -> bool:
|
def _is_known_android_chooser_foreground(
|
||||||
"""前台 Activity 只接受 Android 标准选择器,避免误退未知系统页。"""
|
package: str, activity: str
|
||||||
|
) -> bool:
|
||||||
|
"""前台包和 Activity 都属于 Android Resolver 白名单才可返回。"""
|
||||||
|
|
||||||
|
normalized_package = str(package or "").strip()
|
||||||
normalized = str(activity or "").strip()
|
normalized = str(activity or "").strip()
|
||||||
return not normalized or normalized in _ANDROID_CHOOSER_ACTIVITIES
|
activity_known = (
|
||||||
|
normalized in _ANDROID_CHOOSER_ACTIVITIES
|
||||||
|
or normalized.endswith(_ANDROID_CHOOSER_ACTIVITY_SUFFIXES)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
normalized_package in _ANDROID_CHOOSER_PACKAGES
|
||||||
|
and activity_known
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_bounds(value: str) -> Optional[Bounds]:
|
def _parse_bounds(value: str) -> Optional[Bounds]:
|
||||||
@@ -500,30 +534,31 @@ class U2PddPurchaseReconcileAdapter(PddPurchaseReconcileAdapter):
|
|||||||
if self._monotonic() > deadline:
|
if self._monotonic() > deadline:
|
||||||
last_reason = "读取手机控件树超时"
|
last_reason = "读取手机控件树超时"
|
||||||
break
|
break
|
||||||
|
if _has_android_wechat_chooser_semantics(root):
|
||||||
|
if pressed_chooser_back:
|
||||||
|
raise PddPurchaseReconcileError(
|
||||||
|
"RECONCILE_ANDROID_CHOOSER_STUCK",
|
||||||
|
"Android 微信应用选择器在一次安全返回后仍未关闭,请人工返回 PDD 核单",
|
||||||
|
)
|
||||||
|
current = device.app_current()
|
||||||
|
chooser_package = str(current.get("package") or "")
|
||||||
|
chooser_activity = str(current.get("activity") or "")
|
||||||
|
if not _is_known_android_chooser_foreground(
|
||||||
|
chooser_package, chooser_activity
|
||||||
|
):
|
||||||
|
raise PddPurchaseReconcileError(
|
||||||
|
"RECONCILE_ANDROID_CHOOSER_UNRECOGNIZED",
|
||||||
|
"检测到疑似微信应用选择页,但前台系统组件不在安全白名单,请人工处理",
|
||||||
|
)
|
||||||
|
if self._monotonic() > deadline:
|
||||||
|
last_reason = "确认 Android 微信应用选择器超时"
|
||||||
|
break
|
||||||
|
device.press("back")
|
||||||
|
pressed_chooser_back = True
|
||||||
|
last_reason = "已关闭 Android 微信应用选择器,等待 PDD 待付款详情加载"
|
||||||
|
self._settle()
|
||||||
|
continue
|
||||||
if not _package_hint_from_tree(root):
|
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:
|
if not checked_foreground:
|
||||||
current_package = _foreground_package_from_tree(root)
|
current_package = _foreground_package_from_tree(root)
|
||||||
if not current_package:
|
if not current_package:
|
||||||
|
|||||||
@@ -349,6 +349,69 @@ class PddU2PurchaseReconcileAdapterTest(unittest.TestCase):
|
|||||||
self.assertEqual(device.app_start_calls, 0)
|
self.assertEqual(device.app_start_calls, 0)
|
||||||
self.assertEqual(device.app_current_calls, 1)
|
self.assertEqual(device.app_current_calls, 1)
|
||||||
|
|
||||||
|
def test_mixed_package_chooser_with_duplicate_accessibility_values_closes(self):
|
||||||
|
chooser_over_pdd = (
|
||||||
|
'<hierarchy>'
|
||||||
|
'<node package="com.xunmeng.pinduoduo" text="页面加载中" />'
|
||||||
|
'<node package="com.xunmeng.pinduoduo" text="" />'
|
||||||
|
'<node package="com.xunmeng.pinduoduo" text="" />'
|
||||||
|
'<node package="android" text="选择要使用的应用" '
|
||||||
|
'content-desc="选择要使用的应用" />'
|
||||||
|
'<node package="com.tencent.mm" text="微信" '
|
||||||
|
'content-desc="微信" />'
|
||||||
|
'<node package="com.tencent.mm" text="微信 2" />'
|
||||||
|
'</hierarchy>'
|
||||||
|
)
|
||||||
|
device = TransitionDevice(
|
||||||
|
[chooser_over_pdd, 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_resolver_package_and_title_variant_are_supported(self):
|
||||||
|
resolver = (
|
||||||
|
'<hierarchy>'
|
||||||
|
'<node package="com.android.intentresolver" '
|
||||||
|
'text="使用以下应用打开" />'
|
||||||
|
'<node package="com.tencent.mm" text="微信" />'
|
||||||
|
'<node package="com.tencent.mm" text="微信分身" />'
|
||||||
|
'</hierarchy>'
|
||||||
|
)
|
||||||
|
device = TransitionDevice(
|
||||||
|
[resolver, xml_detail("待付款")],
|
||||||
|
current_package="com.android.intentresolver",
|
||||||
|
current_activity="com.android.intentresolver.ResolverActivity",
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
def test_android_wechat_chooser_stops_when_one_back_does_not_close_it(self):
|
def test_android_wechat_chooser_stops_when_one_back_does_not_close_it(self):
|
||||||
device = TransitionDevice(
|
device = TransitionDevice(
|
||||||
[xml_android_wechat_chooser(), xml_android_wechat_chooser()],
|
[xml_android_wechat_chooser(), xml_android_wechat_chooser()],
|
||||||
|
|||||||
@@ -147,6 +147,11 @@ Client 不保存或读取 `purchase.live_*` 手工授权设置。`purchase_mode`
|
|||||||
截止时间,也要先分析刚返回的最新控件树,再决定成功或超时。规格面板超时必须记录
|
截止时间,也要先分析刚返回的最新控件树,再决定成功或超时。规格面板超时必须记录
|
||||||
最后页面类型、控件树读取次数和脱敏 XML 引用,不保存商品、账号或收货相关原文。
|
最后页面类型、控件树读取次数和脱敏 XML 引用,不保存商品、账号或收货相关原文。
|
||||||
|
|
||||||
|
只读核单遇到多个微信的 Android 应用选择器时,分别读取节点的 `text` 和
|
||||||
|
`content-desc`,并同时校验白名单标题、微信候选、前台 Resolver 包和 Activity。
|
||||||
|
选择器可能与底层 PDD 节点同时出现在控件树中,因此必须先判断选择器;证据完整时
|
||||||
|
最多按一次返回键,绝不选择微信或点击支付。证据不完整或一次返回后仍存在时转人工。
|
||||||
|
|
||||||
> **运营提醒(不是代码规则):** Admin 超时重派可能导致同一任务被两台 Client 各下一单,
|
> **运营提醒(不是代码规则):** Admin 超时重派可能导致同一任务被两台 Client 各下一单,
|
||||||
> 产生重复的未付款订单。人工审核时取消多余订单即可。但未付款订单长期堆积可能触发平台风控,
|
> 产生重复的未付款订单。人工审核时取消多余订单即可。但未付款订单长期堆积可能触发平台风控,
|
||||||
> 需要操作人员留意,不要放着不管。
|
> 需要操作人员留意,不要放着不管。
|
||||||
|
|||||||
Reference in New Issue
Block a user