feat: 完整采集采购尺码候选 (#256)

This commit is contained in:
chengma
2026-08-17 17:04:43 +08:00
parent f187bfe0fe
commit 58dc9022eb
7 changed files with 458 additions and 7 deletions
+122
View File
@@ -11,6 +11,7 @@ import re
import time
import xml.etree.ElementTree as ET
from contextlib import nullcontext
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from pathlib import Path
from typing import Any, Callable, Mapping, Optional
@@ -24,6 +25,11 @@ from .pdd_device_service import (
current_thread_device_service,
)
from .performance_timing import current_performance_trace
from .pdd_collect_service import (
PddCollectError,
PddCollectService,
SpecDimension,
)
from .pdd_page_classifier import (
ACTION_NETWORK_ERROR,
ACTION_READY,
@@ -46,11 +52,15 @@ from .pdd_purchase_adapter import (
PddLivePurchaseAdapter,
PddPurchaseAdapter,
PddPurchaseError,
PddPurchaseSpecResolutionRequired,
PurchaseSizeObservation,
PurchaseSpecCandidateSnapshot,
PurchasePageState,
)
from .util.get_size_panle_coord import get_size_panel_coord
from .util.select_color_size import (
color_selection_failure_reason,
normalize_spec_text,
select_color,
select_size,
size_selection_failure_reason,
@@ -890,6 +900,10 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
panel_timeout: float = 10.0,
select_color_fn: Callable[..., bool] = select_color,
select_size_fn: Callable[..., bool] = select_size,
size_candidate_collector: Optional[
Callable[[Any], Optional[SpecDimension]]
] = None,
now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
artifact_directory: Optional[Path] = None,
) -> None:
self._device_address = str(device_address or "").strip()
@@ -901,6 +915,10 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
self._panel_timeout = panel_timeout
self._select_color = select_color_fn
self._select_size = select_size_fn
self._size_candidate_collector = (
size_candidate_collector or self._collect_size_candidates
)
self._now = now
self._artifact_directory = artifact_directory
self._last_xml: Optional[str] = None
self._hierarchy_reads = 0
@@ -1023,6 +1041,7 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
panel_xml = self._wait_for_confirmation_panel()
color = checked.get("color")
selected_color_for_resolution = ""
if color:
panel_xml = self._restore_purchase_panel_color_region(panel_xml)
trace = current_performance_trace()
@@ -1065,6 +1084,7 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
step="purchase_select_options",
diagnostics=diagnostics,
)
selected_color_for_resolution = color
size = checked.get("size")
if size:
latest_xml = self._dump_hierarchy()
@@ -1082,6 +1102,15 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
failure_reason = size_selection_failure_reason(
failed_xml, size
)
if (
failure_reason == "target_not_visible"
and selected_color_for_resolution
):
self._raise_spec_resolution_required(
failed_xml,
selected_color_for_resolution,
size,
)
messages = {
"target_not_visible": f"没有找到目标尺码:{size}",
"target_ambiguous": (
@@ -1105,6 +1134,99 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
except Exception as exc:
self._raise_device_or_page_error(exc, "purchase_select_options")
def _raise_spec_resolution_required(
self,
failed_xml: str,
selected_color: str,
target_size: str,
) -> None:
"""完整只读遍历后,仅为页面确实不存在的目标返回候选快照。"""
try:
self._restore_purchase_panel_color_region(failed_xml)
dimension = self._size_candidate_collector(self._require_device())
except PddPurchaseError:
raise
except PddCollectError as exc:
raise PddPurchaseError(
"PURCHASE_SIZE_CANDIDATE_SCAN_INCOMPLETE",
f"目标尺码未找到,且无法完整读取页面候选:{exc.message}",
step="purchase_select_options",
diagnostics={
"selection_failure": "target_not_visible",
"candidate_scan_error": exc.code,
},
) from exc
except Exception as exc:
self._raise_device_or_page_error(exc, "purchase_select_options")
if dimension is None or not dimension.values:
raise PddPurchaseError(
"PURCHASE_SIZE_CANDIDATE_SCAN_INCOMPLETE",
"目标尺码未找到,且页面没有可确认完整的第二规格候选",
step="purchase_select_options",
diagnostics={"selection_failure": "target_not_visible"},
)
available_values = tuple(
item for item in dimension.values if item.available
)
if not available_values:
raise PddPurchaseError(
"PURCHASE_OPTIONS_MISMATCH",
f"当前颜色没有可购买的尺码候选:{selected_color}",
step="purchase_select_options",
diagnostics={"selection_failure": "target_not_visible"},
)
# 目标若在完整列表中精确或繁简等价出现,说明前面的点击/确认失败,
# 不能把它伪装成需要远端解析的问题。
normalized_target = normalize_spec_text(target_size)
if any(
item.text == target_size
or normalize_spec_text(item.text) == normalized_target
for item in available_values
):
raise PddPurchaseError(
"PURCHASE_OPTIONS_MISMATCH",
f"页面存在目标尺码,但没有可靠确认选中:{target_size}",
step="purchase_select_options",
diagnostics={"selection_failure": "selection_unconfirmed"},
)
observed_at = (
self._now()
.astimezone(timezone.utc)
.isoformat()
.replace("+00:00", "Z")
)
snapshot = PurchaseSpecCandidateSnapshot.build(
goods_id=self._goods_id,
selected_color=selected_color,
target_size=target_size,
dimension_name=dimension.name,
observations=tuple(
PurchaseSizeObservation(item.text, item.available)
for item in dimension.values
),
observed_at=observed_at,
)
raise PddPurchaseSpecResolutionRequired(snapshot)
def _collect_size_candidates(self, device: Any) -> Optional[SpecDimension]:
"""使用采集模块同一套第二规格遍历,不建立新连接也不保存 XML。"""
service = PddCollectService(
self._device_service,
self._device_address,
"purchase-runtime",
sleeper=self._sleep,
monotonic=self._monotonic,
cancelled=self._cancelled,
artifact_directory=None,
)
return service.collect_second_dimension_candidates(device)
def set_quantity(self, quantity: int) -> None:
device = self._require_device()
if isinstance(quantity, bool) or not isinstance(quantity, int) or quantity <= 0: