fix: 采购选色前归位规格面板 (#230)

This commit is contained in:
chengma
2026-08-14 18:10:56 +08:00
parent cbe8d0347b
commit fee40284b3
2 changed files with 401 additions and 0 deletions
+260
View File
@@ -70,6 +70,14 @@ _PAYMENT_MARKERS = ("输入支付密码", "立即支付", "支付成功", "支
_FINAL_SUBMIT_MARKERS = ("提交订单", "现在买,仅", "确认购买")
_OUT_OF_STOCK_MARKERS = ("已售罄", "暂时缺货", "库存不足", "该商品已售罄")
_ADDRESS_ACTION_SETTLE_SECONDS = 1.0
_COLOR_DIMENSION_HEADINGS = frozenset(
{"颜色分类", "顏色分類", "颜色", "顏色", "款式", "花色", "颜色款式"}
)
_SECOND_DIMENSION_HEADINGS = frozenset(
{"尺码", "尺碼", "尺寸", "套餐", "规格", "規格", "型号", "型號"}
)
_PANEL_TOP_MAX_SWIPES = 8
_PANEL_TOP_STABLE_READS = 2
_SUPPORTED_OPTION_KEYS = frozenset({"color", "size"})
_MAX_QUANTITY_BUTTON_CLICKS = 5
_KEYBOARD_STATUS_MARKERS = (
@@ -167,6 +175,159 @@ def _parent_nodes(root: ET.Element) -> dict[ET.Element, ET.Element]:
}
def _dimension_heading(label: str) -> str:
"""只接受独立规格标题,明确排除选择摘要和提交提示。"""
compact = re.sub(r"\s+", "", str(label or ""))
if not compact or compact.startswith(
("请选择", "請選擇", "已选", "已選", "已选择", "已選擇")
):
return ""
if compact.startswith("选择") and "后" in compact:
return ""
compact = re.sub(r"[((]\d+[))]$", "", compact)
return compact
def _screen_bounds(root: ET.Element) -> Optional[Bounds]:
parsed = [
bounds
for node in root.iter("node")
if (bounds := _parse_bounds(node.get("bounds", ""))) is not None
]
if not parsed:
return None
return 0, 0, max(item[2] for item in parsed), max(item[3] for item in parsed)
def _has_visible_color_region(root: ET.Element) -> bool:
"""正式颜色标题下必须同时存在至少一个可靠的可点击选项。"""
parents = _parent_nodes(root)
headings: list[Bounds] = []
second_heading_tops: list[int] = []
for node in root.iter("node"):
heading = _dimension_heading(_label(node))
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is None or node.get("visible-to-user", "true") == "false":
continue
if heading in _COLOR_DIMENSION_HEADINGS:
headings.append(bounds)
elif heading in _SECOND_DIMENSION_HEADINGS:
second_heading_tops.append(bounds[1])
screen = _screen_bounds(root)
if not headings or screen is None:
return False
excluded = {
"增加数量",
"减少数量",
"提交订单",
"确定",
"打开大图",
}
for heading_bounds in headings:
boundary = min(
(
top
for top in second_heading_tops
if top > heading_bounds[3]
),
default=screen[3],
)
for node in root.iter("node"):
label = _label(node).strip()
bounds = _parse_bounds(node.get("bounds", ""))
if not label or bounds is None:
continue
if bounds[1] < heading_bounds[3] or bounds[1] >= boundary:
continue
if _dimension_heading(label) in (
_COLOR_DIMENSION_HEADINGS | _SECOND_DIMENSION_HEADINGS
):
continue
compact = label.replace(" ", "")
if (
compact.startswith(
("请选择", "請選擇", "已选", "已選", "选择", "選擇")
)
or any(marker in compact for marker in excluded)
or re.fullmatch(r"[¥¥]?\d+(?:\.\d{1,2})?", compact)
):
continue
if _nearest_clickable_bounds(node, parents) is not None:
return True
return False
def _needs_color_region_restore(root: ET.Element) -> bool:
"""只有第二规格已出现且颜色摘要仍存在时,才判定面板停在中部。"""
labels = [_label(node).strip() for node in root.iter("node")]
has_color_summary = any(
label.replace(" ", "").startswith(
("请选择", "請選擇", "已选", "已選", "已选择", "已選擇")
)
and any(heading in label.replace(" ", "") for heading in _COLOR_DIMENSION_HEADINGS)
for label in labels
)
has_second_heading = any(
_dimension_heading(label) in _SECOND_DIMENSION_HEADINGS
for label in labels
)
return has_color_summary and has_second_heading
def _purchase_panel_scroll_region(root: ET.Element) -> Optional[Bounds]:
"""返回规格面板中唯一可靠的纵向滚动容器。"""
screen = _screen_bounds(root)
if screen is None:
return None
screen_width = screen[2] - screen[0]
screen_height = screen[3] - screen[1]
candidates: list[Bounds] = []
for node in root.iter("node"):
if (
node.get("scrollable") != "true"
or node.get("package") != PDD_PACKAGE_NAME
or node.get("visible-to-user", "true") == "false"
or node.get("enabled", "true") == "false"
):
continue
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is None:
continue
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
if width >= screen_width * 0.60 and height >= screen_height * 0.20:
candidates.append(bounds)
unique = sorted(set(candidates))
return unique[0] if len(unique) == 1 else None
def _region_signature(root: ET.Element, region: Bounds) -> str:
"""生成当前滚动视口的短签名,只用于判断是否已经无法继续归位。"""
parts = []
for node in root.iter("node"):
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is None:
continue
center_x = (bounds[0] + bounds[2]) // 2
center_y = (bounds[1] + bounds[3]) // 2
if not (
region[0] <= center_x <= region[2]
and region[1] <= center_y <= region[3]
):
continue
label = _label(node)
if label:
parts.append(f"{label}|{bounds}")
return hashlib.sha256("\n".join(parts).encode("utf-8")).hexdigest()
def _nearest_clickable_bounds(
node: ET.Element, parents: Mapping[ET.Element, ET.Element]
) -> Optional[Bounds]:
@@ -862,6 +1023,7 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
color = checked.get("color")
if color:
panel_xml = self._restore_purchase_panel_color_region(panel_xml)
trace = current_performance_trace()
stage = (
trace.stage("purchase_select_color")
@@ -1258,6 +1420,104 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
diagnostics=diagnostics,
)
def _restore_purchase_panel_color_region(self, xml_data: str) -> str:
"""选择颜色前把规格内容有界归位到正式颜色区域。"""
root = _parse_xml(xml_data)
if _has_visible_color_region(root):
return xml_data
if not _needs_color_region_restore(root):
# 兼容不暴露正式标题或滚动属性、但原选择器可以处理的旧面板。
return xml_data
region = _purchase_panel_scroll_region(root)
if region is None:
self._raise_color_region_not_found(
swipe_count=0,
stable_reads=0,
scroll_region_found=False,
)
device = self._require_device()
previous_signature = _region_signature(root, region)
unchanged_reads = 0
swipe_count = 0
for _ in range(_PANEL_TOP_MAX_SWIPES):
self._check_cancelled("purchase_select_options")
left, top, right, bottom = region
height = bottom - top
x = (left + right) // 2
# 手指向下滑,让规格内容返回顶部;坐标始终限制在规格容器内。
device.swipe(
x,
top + int(height * 0.25),
x,
top + int(height * 0.82),
duration=0.35,
)
swipe_count += 1
self._sleep(0.2)
current_xml = self._dump_hierarchy()
root = _parse_xml(current_xml)
page_kind = _page_kind(root, "")
if page_kind in {
"captcha",
"login_required",
"risk_control",
"payment",
}:
self._raise_special_page(page_kind)
if page_kind != "order_confirmation":
self._raise_color_region_not_found(
swipe_count=swipe_count,
stable_reads=unchanged_reads,
scroll_region_found=True,
)
if _has_visible_color_region(root):
return current_xml
current_region = _purchase_panel_scroll_region(root)
if current_region is None:
break
region = current_region
signature = _region_signature(root, region)
if signature == previous_signature:
unchanged_reads += 1
else:
unchanged_reads = 0
previous_signature = signature
if unchanged_reads >= _PANEL_TOP_STABLE_READS:
break
self._raise_color_region_not_found(
swipe_count=swipe_count,
stable_reads=unchanged_reads,
scroll_region_found=True,
)
def _raise_color_region_not_found(
self,
*,
swipe_count: int,
stable_reads: int,
scroll_region_found: bool,
) -> None:
diagnostics: dict[str, Any] = {
"swipe_count": swipe_count,
"stable_reads": stable_reads,
"scroll_region_found": scroll_region_found,
}
artifact = self._save_last_xml("purchase-color-region-not-found")
if artifact is not None:
diagnostics["artifacts"] = [artifact]
raise PddPurchaseError(
"PURCHASE_PANEL_COLOR_REGION_NOT_FOUND",
"规格面板已打开,但无法回到颜色分类区域",
step="purchase_select_options",
diagnostics=diagnostics,
)
def _dump_hierarchy(self) -> str:
raw = self._require_device().dump_hierarchy()
xml_data = (