diff --git a/client/src/pdd_u2_purchase_adapter.py b/client/src/pdd_u2_purchase_adapter.py index 7d26512..f35770d 100644 --- a/client/src/pdd_u2_purchase_adapter.py +++ b/client/src/pdd_u2_purchase_adapter.py @@ -547,17 +547,70 @@ def _pdd_title_back_targets(root: ET.Element) -> list[Bounds]: ] +def _same_address_target(first: Bounds, second: Bounds) -> bool: + """判断两个边界是否是同一地址卡片的重复无障碍节点。""" + + # 使用两倍中心坐标计算,避免浮点数;阈值等价于中心距离每轴 3 像素。 + if ( + abs((first[0] + first[2]) - (second[0] + second[2])) > 6 + or abs((first[1] + first[3]) - (second[1] + second[3])) > 6 + ): + return False + intersection_width = max( + 0, min(first[2], second[2]) - max(first[0], second[0]) + ) + intersection_height = max( + 0, min(first[3], second[3]) - max(first[1], second[1]) + ) + intersection_area = intersection_width * intersection_height + first_area = (first[2] - first[0]) * (first[3] - first[1]) + second_area = (second[2] - second[0]) * (second[3] - second[1]) + smaller_area = min(first_area, second_area) + return smaller_area > 0 and intersection_area * 100 >= smaller_area * 98 + + +def _merge_address_targets(targets: list[Bounds]) -> list[Bounds]: + """严格合并近乎完全重叠的地址候选,并返回共同安全区域。""" + + clusters: list[list[Bounds]] = [] + for target in sorted(set(targets)): + cluster = next( + ( + item + for item in clusters + if all(_same_address_target(target, other) for other in item) + ), + None, + ) + if cluster is None: + clusters.append([target]) + else: + cluster.append(target) + + merged: list[Bounds] = [] + for cluster in clusters: + intersection = ( + max(item[0] for item in cluster), + max(item[1] for item in cluster), + min(item[2] for item in cluster), + min(item[3] for item in cluster), + ) + if intersection[2] > intersection[0] and intersection[3] > intersection[1]: + merged.append(intersection) + return sorted(merged) + + def _address_entry_targets(root: ET.Element) -> list[Bounds]: """通过脱敏手机号结构定位规格面板顶部的收货地址卡片。""" parents = _parent_nodes(root) - targets = { + targets = [ bounds for node in root.iter("node") if _MASKED_PHONE_PATTERN.search(_label(node)) if (bounds := _nearest_clickable_bounds(node, parents)) is not None - } - return sorted(targets) + ] + return _merge_address_targets(targets) def _shipping_address_editor(root: ET.Element) -> Optional[tuple[str, Bounds]]: diff --git a/client/test/test_pdd_u2_purchase_adapter.py b/client/test/test_pdd_u2_purchase_adapter.py index aa275ef..872dba9 100644 --- a/client/test/test_pdd_u2_purchase_adapter.py +++ b/client/test/test_pdd_u2_purchase_adapter.py @@ -18,6 +18,7 @@ from src.performance_timing import TaskPerformanceTrace from src.pdd_u2_purchase_adapter import ( U2PddLivePurchaseAdapter, U2PddPurchaseAdapter, + _address_entry_targets, _final_submit_targets, _needs_color_region_restore, _page_kind, @@ -544,6 +545,29 @@ def address_confirmation_xml(address: str) -> str: """ +def duplicate_address_entry_xml(*, separated: bool = False) -> str: + """模拟同一卡片重复节点,或两个位置明显不同的真实候选。""" + + second_top, second_bottom = ((620, 727) if separated else (366, 472)) + return f""" + + + + + + + + + """ + + def clipped_address_confirmation_xml(address: str) -> str: """首次地址卡片被顶部裁剪时的脱敏控件树。""" @@ -1075,6 +1099,16 @@ class AmbiguousAfterModifyAddressDevice(AddressFlowDevice): class U2PddPurchaseAdapterTest(unittest.TestCase): + def test_address_entry_merges_one_pixel_duplicate_nodes(self): + root = ET.fromstring(duplicate_address_entry_xml()) + + self.assertEqual(_address_entry_targets(root), [(0, 366, 1080, 472)]) + + def test_address_entry_keeps_separate_cards_ambiguous(self): + root = ET.fromstring(duplicate_address_entry_xml(separated=True)) + + self.assertEqual(len(_address_entry_targets(root)), 2) + def _adapter(self, device, calls): def select_color_fn(_device, _xml, target, **_kwargs): calls.append(("color", target))