fix: 兼容快速确认页进入规格面板 (#190)

This commit is contained in:
chengma
2026-08-12 17:30:03 +08:00
parent 275b9888b7
commit 9109b8891d
3 changed files with 300 additions and 2 deletions
+144 -2
View File
@@ -645,6 +645,132 @@ def _is_spec_panel_open(xml_data: str | bytes) -> bool:
return has_selection_summary or has_submit_hint or has_dimension_heading
def _quick_confirmation_spec_coord(root: ET.Element) -> Optional[tuple[int, int]]:
"""定位快速确认页中的当前主规格值,不接触底部购买按钮。"""
if classify_pdd_page(root, "").kind != PAGE_ORDER_CONFIRMATION:
return None
nodes = list(root.iter("node"))
if sum(node.get("package") == PDD_PACKAGE_NAME for node in nodes) < 3:
return None
bounded_nodes = [
(node, bounds)
for node in nodes
if (bounds := _parse_bounds(node.get("bounds", ""))) is not None
and node.get("visible-to-user", "true") != "false"
]
screen_bottom = max((bounds[3] for _, bounds in bounded_nodes), default=0)
if not screen_bottom:
return None
labels = [_preferred_or_descendant_label(node).replace(" ", "") for node in nodes]
has_close = any(label in ("关闭", "關閉") for label in labels)
has_summary = any(
label.startswith(("已选", "已選")) for label in labels
)
has_payment_area = any(
word in label for label in labels for word in ("微信支付", "先用后付", "支付方式")
)
has_quick_buy_action = any(
"现在买" in _preferred_or_descendant_label(node).replace(" ", "")
and (bounds[1] + bounds[3]) // 2 >= screen_bottom * 0.75
for node, bounds in bounded_nodes
)
quantity_editors = [
node
for node in nodes
if node.get("class") == "android.widget.EditText"
and node.get("text", "").strip().isdigit()
and int(node.get("text", "0")) > 0
]
decreases = [
node
for node in nodes
if _preferred_or_descendant_label(node).replace(" ", "") == "减少数量"
and node.get("clickable") == "true"
]
increases = [
node
for node in nodes
if _preferred_or_descendant_label(node).replace(" ", "") == "增加数量"
and node.get("clickable") == "true"
]
if not (
has_close
and has_summary
and has_payment_area
and has_quick_buy_action
and len(quantity_editors) == 1
and len(decreases) == 1
and len(increases) == 1
):
return None
headings = []
for node, bounds in bounded_nodes:
label = _preferred_node_label(node).strip()
if (
label
and node.get("clickable") != "true"
and _is_dimension_heading(label)
):
headings.append((bounds[1], label, bounds))
headings.sort(key=lambda item: item[0])
used_keys: set[str] = set()
primary_index: Optional[int] = None
for index, (_, label, _) in enumerate(headings):
key = _dimension_key(label, used_keys)
used_keys.add(key)
if key == "color":
primary_index = index
break
if primary_index is None:
return None
_, _, heading_bounds = headings[primary_index]
next_heading_top = (
headings[primary_index + 1][0]
if primary_index + 1 < len(headings)
else int(screen_bottom * 0.75)
)
candidates: list[tuple[int, int, Bounds]] = []
for node, bounds in bounded_nodes:
if node.get("clickable") != "true" or not _is_available(node):
continue
label = _preferred_or_descendant_label(node).strip()
compact = label.replace(" ", "")
if not label:
continue
if compact.startswith(("已选", "已選", "请选择", "請選擇")):
continue
if any(
word in compact
for word in (
"现在买",
"提交订单",
"确认购买",
"支付",
"增加数量",
"减少数量",
"关闭",
)
):
continue
if bounds[1] < heading_bounds[3] or bounds[3] > next_heading_top:
continue
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
candidates.append((bounds[1], area, bounds))
if not candidates:
return None
_, _, target = min(candidates, key=lambda item: (item[0], item[1]))
left, top, right, bottom = target
return (left + right) // 2, (top + bottom) // 2
def _top_level_clickable_options(
container: ET.Element,
parents: Mapping[ET.Element, ET.Element],
@@ -1282,6 +1408,7 @@ class PddCollectService:
deadline = self._monotonic() + self._spec_panel_timeout
saw_confirmation_page = False
quick_confirmation_recovery_attempted = False
incomplete_reads = 0
while self._monotonic() < deadline:
self._check_cancelled()
@@ -1302,12 +1429,27 @@ class PddCollectService:
return snapshot
self._last_invalid_spec_xml = xml_data
incomplete_reads += 1
if not quick_confirmation_recovery_attempted:
coordinate = _quick_confirmation_spec_coord(root)
if coordinate is not None:
# 这里只打开当前主规格,禁止触碰“现在买”等不可逆动作。
device.click(*coordinate)
quick_confirmation_recovery_attempted = True
self._sleep(0.25)
if saw_confirmation_page:
message = "规格面板已经打开,但页面结构无法识别"
if quick_confirmation_recovery_attempted:
message = "快速确认页无法进入可识别的规格选择区域"
raise PddCollectError(
"PDD_DATA_SPEC_INCOMPLETE",
"规格面板已经打开,但页面结构无法识别",
{"incomplete_spec_reads": incomplete_reads},
message,
{
"incomplete_spec_reads": incomplete_reads,
"quick_confirmation_recovery_attempted": (
quick_confirmation_recovery_attempted
),
},
)
raise PddCollectError(
"PDD_PAGE_SPEC_PANEL_TIMEOUT",