fix: 兼容提交提示型非滚动规格面板 (#175)

This commit is contained in:
chengma
2026-08-12 10:15:19 +08:00
parent 354aa6b264
commit 0f24165ee2
4 changed files with 229 additions and 26 deletions
+97 -26
View File
@@ -31,6 +31,7 @@ from .pdd_page_classifier import (
PAGE_HOME,
PAGE_LOGIN_REQUIRED,
PAGE_NETWORK_ERROR,
PAGE_ORDER_CONFIRMATION,
PAGE_PAYMENT,
PAGE_RISK_CONTROL,
GoodsOpenTracker,
@@ -466,6 +467,8 @@ def _dimension_key(name: str, used: set[str]) -> str:
def _is_dimension_heading(label: str) -> bool:
compact = label.replace(" ", "")
if compact.startswith(("请选择", "請選擇", "已选", "已選")):
return False
# 新版页面会把可选数量写进标题,例如“颜色 (6)”或“颜色(6)”。
# 数量不是规格名称的一部分,只在判断标题类型时去掉,最终展示仍保留原文。
compact = re.sub(r"[((]\d+[))]$", "", compact)
@@ -496,37 +499,92 @@ def _find_non_scrollable_spec_panel(
) -> Optional[ET.Element]:
"""用多项强证据定位不暴露 scrollable 属性的自绘规格面板。"""
headings: list[ET.Element] = []
summaries: list[ET.Element] = []
panel_cues: list[ET.Element] = []
confirms: list[ET.Element] = []
for node in root.iter("node"):
label = _preferred_node_label(node).strip()
compact = label.replace(" ", "")
if not label or _parse_bounds(node.get("bounds", "")) is None:
continue
if _is_dimension_heading(label):
headings.append(node)
if compact.startswith(("已选", "已選", "请选择", "請選擇")):
summaries.append(node)
if compact in ("确认款式", "確認款式", "关闭", "關閉"):
panel_cues.append(node)
if compact in ("确定", "確定") and node.get("clickable") == "true":
confirms.append(node)
if not headings or not summaries or not panel_cues or not confirms:
return None
required = [*headings, summaries[0], panel_cues[0], confirms[0]]
common = set([required[0], *_ancestors(required[0], parents)])
for node in required[1:]:
common.intersection_update([node, *_ancestors(node, parents)])
all_bounds = [
bounds
for node in root.iter("node")
if (bounds := _parse_bounds(node.get("bounds", ""))) is not None
]
screen_bottom = max((item[3] for item in all_bounds), default=0)
pdd_node_count = sum(
node.get("package") == PDD_PACKAGE_NAME
for node in root.iter("node")
)
headings: list[ET.Element] = []
summaries: list[ET.Element] = []
panel_cues: list[ET.Element] = []
confirms: list[ET.Element] = []
submit_hints: list[ET.Element] = []
quantity_editors: list[ET.Element] = []
decreases: list[ET.Element] = []
increases: list[ET.Element] = []
for node in root.iter("node"):
label = _preferred_node_label(node).strip()
compact = label.replace(" ", "")
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is None:
continue
is_summary = compact.startswith(
("已选", "已選", "请选择", "請選擇")
)
if label and not is_summary and _is_dimension_heading(label):
headings.append(node)
if is_summary:
summaries.append(node)
if compact in ("确认款式", "確認款式", "关闭", "關閉"):
panel_cues.append(node)
if compact in ("确定", "確定") and node.get("clickable") == "true":
confirms.append(node)
descendant_label = _preferred_or_descendant_label(node).replace(" ", "")
if (
node.get("clickable") == "true"
and node.get("enabled", "true") != "false"
and node.get("visible-to-user", "true") != "false"
and screen_bottom
and (bounds[1] + bounds[3]) // 2 >= screen_bottom * 0.6
and "提交订单" in descendant_label
and any(
word in descendant_label
for word in ("选择", "颜色", "尺码", "规格")
)
):
submit_hints.append(node)
if (
node.get("class") == "android.widget.EditText"
and node.get("text", "").strip().isdigit()
and int(node.get("text", "0")) > 0
):
quantity_editors.append(node)
if compact == "减少数量" and node.get("clickable") == "true":
decreases.append(node)
if compact == "增加数量" and node.get("clickable") == "true":
increases.append(node)
if not headings or not summaries or not panel_cues:
return None
if confirms:
action_nodes = [confirms[0]]
elif (
pdd_node_count >= 3
and len(submit_hints) == 1
and len(quantity_editors) == 1
and len(decreases) == 1
and len(increases) == 1
):
action_nodes = [
submit_hints[0],
quantity_editors[0],
decreases[0],
increases[0],
]
else:
return None
required = [*headings, summaries[0], panel_cues[0], *action_nodes]
common = set([required[0], *_ancestors(required[0], parents)])
for node in required[1:]:
common.intersection_update([node, *_ancestors(node, parents)])
screen_area = 0
if all_bounds:
screen_area = max(item[2] for item in all_bounds) * max(
@@ -1223,21 +1281,34 @@ class PddCollectService:
"""等待点击后的规格面板真正出现,不能只依赖固定延时。"""
deadline = self._monotonic() + self._spec_panel_timeout
saw_confirmation_page = False
incomplete_reads = 0
while self._monotonic() < deadline:
self._check_cancelled()
xml_data = self._dump_hierarchy(device)
root = _parse_xml(xml_data)
if classify_pdd_page(root, "").kind == PAGE_ORDER_CONFIRMATION:
saw_confirmation_page = True
try:
snapshot = parse_spec_panel(xml_data)
except PddCollectError as exc:
if exc.code != "PDD_DATA_SPEC_INCOMPLETE":
raise
self._last_invalid_spec_xml = xml_data
incomplete_reads += 1
else:
if _is_spec_panel_open(xml_data):
self._last_valid_spec_xml = xml_data
return snapshot
self._last_invalid_spec_xml = xml_data
incomplete_reads += 1
self._sleep(0.25)
if saw_confirmation_page:
raise PddCollectError(
"PDD_DATA_SPEC_INCOMPLETE",
"规格面板已经打开,但页面结构无法识别",
{"incomplete_spec_reads": incomplete_reads},
)
raise PddCollectError(
"PDD_PAGE_SPEC_PANEL_TIMEOUT",
"点击规格入口后,等待规格面板加载超时",