fix: 兼容快速确认页进入规格面板 (#190)
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user