fix: 采购选色前归位规格面板 (#230)
This commit is contained in:
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user