fix: 兼容无维度名颜色摘要归位 (#268)
This commit is contained in:
@@ -277,10 +277,10 @@ def _has_visible_color_region(root: ET.Element) -> bool:
|
||||
|
||||
|
||||
def _needs_color_region_restore(root: ET.Element) -> bool:
|
||||
"""只有第二规格已出现且颜色摘要仍存在时,才判定面板停在中部。"""
|
||||
"""用维度名或强结构证据判断颜色区域是否已经滑出视口。"""
|
||||
|
||||
labels = [_label(node).strip() for node in root.iter("node")]
|
||||
has_color_summary = any(
|
||||
has_named_color_summary = any(
|
||||
label.replace(" ", "").startswith(
|
||||
("请选择", "請選擇", "已选", "已選", "已选择", "已選擇")
|
||||
)
|
||||
@@ -291,7 +291,87 @@ def _needs_color_region_restore(root: ET.Element) -> bool:
|
||||
_dimension_heading(label) in _SECOND_DIMENSION_HEADINGS
|
||||
for label in labels
|
||||
)
|
||||
return has_color_summary and has_second_heading
|
||||
if not has_second_heading:
|
||||
return False
|
||||
if has_named_color_summary:
|
||||
return True
|
||||
|
||||
# 部分 PDD 面板的摘要只写“已选:具体颜色 具体尺码”,不再带
|
||||
# “颜色分类”维度名。只有唯一纵向容器内、第二规格标题上方同时
|
||||
# 存在至少两个完整可点击卡片时,才把它认作已滚动的第一规格。
|
||||
has_plain_selected_summary = any(
|
||||
label.replace(" ", "").startswith(
|
||||
("已选", "已選", "已选择", "已選擇")
|
||||
)
|
||||
for label in labels
|
||||
)
|
||||
return has_plain_selected_summary and _has_first_dimension_structure(root)
|
||||
|
||||
|
||||
def _has_first_dimension_structure(root: ET.Element) -> bool:
|
||||
"""确认唯一纵向容器的第二规格标题上方仍有多个第一规格卡片。"""
|
||||
|
||||
region = _purchase_panel_scroll_region(root)
|
||||
if region is None:
|
||||
return False
|
||||
width = region[2] - region[0]
|
||||
height = region[3] - region[1]
|
||||
if height < width * 0.70:
|
||||
return False
|
||||
|
||||
second_heading_tops = [
|
||||
bounds[1]
|
||||
for node in root.iter("node")
|
||||
if _dimension_heading(_label(node)) in _SECOND_DIMENSION_HEADINGS
|
||||
if (bounds := _parse_bounds(node.get("bounds", ""))) is not None
|
||||
if region[0] <= bounds[0]
|
||||
and region[1] <= bounds[1]
|
||||
and bounds[2] <= region[2]
|
||||
and bounds[3] <= region[3]
|
||||
]
|
||||
if not second_heading_tops:
|
||||
return False
|
||||
second_heading_top = min(second_heading_tops)
|
||||
|
||||
excluded = {
|
||||
"增加数量",
|
||||
"减少数量",
|
||||
"提交订单",
|
||||
"确定",
|
||||
"打开大图",
|
||||
}
|
||||
candidate_bounds: set[Bounds] = set()
|
||||
for node in root.iter("node"):
|
||||
if (
|
||||
node.get("clickable") != "true"
|
||||
or node.get("enabled", "true") == "false"
|
||||
or node.get("visible-to-user", "true") == "false"
|
||||
):
|
||||
continue
|
||||
bounds = _parse_bounds(node.get("bounds", ""))
|
||||
label = _label(node).strip()
|
||||
if not label or bounds is None:
|
||||
continue
|
||||
if not (
|
||||
region[0] <= bounds[0]
|
||||
and region[1] <= bounds[1]
|
||||
and bounds[2] <= region[2]
|
||||
and bounds[3] <= second_heading_top
|
||||
):
|
||||
continue
|
||||
compact = label.replace(" ", "")
|
||||
if (
|
||||
compact.startswith(
|
||||
("请选择", "請選擇", "已选", "已選", "选择", "選擇")
|
||||
)
|
||||
or any(marker in compact for marker in excluded)
|
||||
or _dimension_heading(label) in (
|
||||
_COLOR_DIMENSION_HEADINGS | _SECOND_DIMENSION_HEADINGS
|
||||
)
|
||||
):
|
||||
continue
|
||||
candidate_bounds.add(bounds)
|
||||
return len(candidate_bounds) >= 2
|
||||
|
||||
|
||||
def _purchase_panel_scroll_region(root: ET.Element) -> Optional[Bounds]:
|
||||
|
||||
@@ -19,6 +19,7 @@ from src.pdd_u2_purchase_adapter import (
|
||||
U2PddLivePurchaseAdapter,
|
||||
U2PddPurchaseAdapter,
|
||||
_final_submit_targets,
|
||||
_needs_color_region_restore,
|
||||
_page_kind,
|
||||
_tagged_shipping_address,
|
||||
)
|
||||
@@ -152,6 +153,98 @@ def vertically_scrolled_purchase_panel_xml(*, at_color_top: bool) -> str:
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def numbered_vertical_color_panel_xml(
|
||||
*,
|
||||
at_color_top: bool,
|
||||
target_selected: bool = False,
|
||||
middle_option_count: int = 8,
|
||||
extra_scrollable: bool = False,
|
||||
) -> str:
|
||||
"""模拟摘要不含维度名、当前停在 E09~E16 的纵向颜色列表。"""
|
||||
|
||||
if at_color_top:
|
||||
selected = ' selected="true"' if target_selected else ""
|
||||
content = f"""
|
||||
<node text="颜色分类" bounds="[36,1012][216,1065]"/>
|
||||
<node text="宠粉诱惑-E01【一件】 ¥3.13" clickable="true"{selected}
|
||||
enabled="true" visible-to-user="true" bounds="[36,1090][604,1175]"/>
|
||||
<node text="宠粉诱惑-E02【一件】 ¥3.37" clickable="true"
|
||||
enabled="true" visible-to-user="true" bounds="[36,1205][604,1290]"/>
|
||||
<node text="宠粉诱惑-E03【一件】 ¥3.95" clickable="true"
|
||||
enabled="true" visible-to-user="true" bounds="[36,1320][604,1405]"/>
|
||||
<node text="尺码" bounds="[36,1515][126,1568]"/>
|
||||
<node text="均码【70-140斤】" clickable="true"
|
||||
enabled="true" visible-to-user="true" bounds="[36,1590][520,1675]"/>
|
||||
"""
|
||||
else:
|
||||
cards = []
|
||||
for index in range(middle_option_count):
|
||||
number = index + 9
|
||||
top = 997 + index * 115
|
||||
bottom = top + 85
|
||||
cards.append(
|
||||
f'<node text="宠粉诱惑-E{number:02d}【一件】 ¥3.13" '
|
||||
f'clickable="true" enabled="true" visible-to-user="true" '
|
||||
f'bounds="[36,{top}][604,{bottom}]"/>'
|
||||
)
|
||||
content = "\n".join(cards) + """
|
||||
<node text="尺码" bounds="[36,1927][126,1980]"/>
|
||||
<node text="均码【70-140斤】" clickable="true"
|
||||
enabled="true" visible-to-user="true" bounds="[36,2006][520,2079]"/>
|
||||
"""
|
||||
extra = """
|
||||
<node package="com.xunmeng.pinduoduo"
|
||||
class="androidx.recyclerview.widget.RecyclerView"
|
||||
scrollable="true" enabled="true" visible-to-user="true"
|
||||
bounds="[0,300][1080,900]"/>
|
||||
""" if extra_scrollable else ""
|
||||
summary = (
|
||||
"已选: 宠粉诱惑-E01【一件】 均码【70-140斤】"
|
||||
if target_selected
|
||||
else "已选: 宠粉诱惑-E10【一件】 均码【70-140斤】"
|
||||
)
|
||||
return f"""<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node text="{summary}" bounds="[396,731][1053,845]"/>
|
||||
<node class="android.widget.EditText" text="1" bounds="[462,881][540,959]"/>
|
||||
<node content-desc="减少数量" clickable="true" bounds="[396,881][462,959]"/>
|
||||
<node content-desc="增加数量" clickable="true" bounds="[540,881][606,959]"/>
|
||||
<node package="com.xunmeng.pinduoduo"
|
||||
class="androidx.recyclerview.widget.RecyclerView"
|
||||
scrollable="true" enabled="true" visible-to-user="true"
|
||||
bounds="[0,997][1080,2079]">
|
||||
{content}
|
||||
</node>
|
||||
{extra}
|
||||
<node text="提交订单 ¥3.13" clickable="true"
|
||||
enabled="true" visible-to-user="true" bounds="[0,2181][1080,2328]"/>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
def size_only_panel_xml() -> str:
|
||||
"""单规格尺码面板不能被结构兜底误认为颜色列表中部。"""
|
||||
|
||||
return """<hierarchy>
|
||||
<node package="com.xunmeng.pinduoduo" bounds="[0,0][1080,2376]">
|
||||
<node text="已选: 均码" bounds="[396,731][1053,845]"/>
|
||||
<node content-desc="增加数量" clickable="true" bounds="[540,881][606,959]"/>
|
||||
<node package="com.xunmeng.pinduoduo"
|
||||
class="androidx.recyclerview.widget.RecyclerView"
|
||||
scrollable="true" enabled="true" visible-to-user="true"
|
||||
bounds="[0,997][1080,2079]">
|
||||
<node text="尺码" bounds="[36,1012][126,1065]"/>
|
||||
<node text="均码" clickable="true" enabled="true"
|
||||
visible-to-user="true" bounds="[36,1090][520,1175]"/>
|
||||
<node text="大码" clickable="true" enabled="true"
|
||||
visible-to-user="true" bounds="[36,1205][520,1290]"/>
|
||||
</node>
|
||||
<node text="提交订单 ¥3.13" clickable="true"
|
||||
enabled="true" visible-to-user="true" bounds="[0,2181][1080,2328]"/>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
|
||||
class FakeEditor:
|
||||
def __init__(self, device) -> None:
|
||||
self.device = device
|
||||
@@ -316,6 +409,40 @@ class VerticallyScrolledColorPanelDevice(FakeDevice):
|
||||
self.at_color_top = True
|
||||
|
||||
|
||||
class NumberedVerticalColorPanelDevice(FakeDevice):
|
||||
"""模拟目标 E01 在纵向列表顶部、当前视口停在 E09~E16。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.at_color_top = False
|
||||
self.target_selected = False
|
||||
self.swipes = []
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if not self.has_opened:
|
||||
return super().dump_hierarchy()
|
||||
if self.mode == "home":
|
||||
return home_xml()
|
||||
if self.mode == "panel":
|
||||
return numbered_vertical_color_panel_xml(
|
||||
at_color_top=self.at_color_top,
|
||||
target_selected=self.target_selected,
|
||||
)
|
||||
return super().dump_hierarchy()
|
||||
|
||||
def click(self, x, y):
|
||||
self.clicks.append((x, y))
|
||||
if self.mode == "panel" and self.at_color_top and 1090 <= y <= 1175:
|
||||
self.target_selected = True
|
||||
return
|
||||
self.mode = "panel"
|
||||
|
||||
def swipe(self, x1, y1, x2, y2, duration=0.0):
|
||||
self.swipes.append((x1, y1, x2, y2, duration))
|
||||
if y2 > y1:
|
||||
self.at_color_top = True
|
||||
|
||||
|
||||
class ColdStartDevice(FakeDevice):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -1622,6 +1749,58 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
||||
self.assertIn('text="颜色分类"', selected_xml[0])
|
||||
adapter.close()
|
||||
|
||||
def test_purchase_restores_plain_summary_and_matches_traditional_target(self):
|
||||
clock = FakeClock()
|
||||
device = NumberedVerticalColorPanelDevice()
|
||||
adapter = U2PddPurchaseAdapter(
|
||||
"USB-001",
|
||||
device_service=PddDeviceService(connector=lambda _serial: device),
|
||||
sleeper=clock.sleep,
|
||||
monotonic=clock.monotonic,
|
||||
)
|
||||
adapter.open_goods(GOODS_URL)
|
||||
|
||||
adapter.select_options({"color": "寵粉誘惑-E01【一件】"})
|
||||
|
||||
self.assertTrue(device.at_color_top)
|
||||
self.assertTrue(device.target_selected)
|
||||
self.assertEqual(len(device.swipes), 1)
|
||||
# 一次打开规格面板、一次点击唯一的简体 E01 卡片。
|
||||
self.assertEqual(len(device.clicks), 2)
|
||||
adapter.close()
|
||||
|
||||
def test_plain_summary_restore_requires_strong_first_dimension_structure(self):
|
||||
cases = (
|
||||
(
|
||||
"valid_vertical_first_dimension",
|
||||
numbered_vertical_color_panel_xml(at_color_top=False),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"only_one_candidate_before_size",
|
||||
numbered_vertical_color_panel_xml(
|
||||
at_color_top=False,
|
||||
middle_option_count=1,
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"multiple_scrollables",
|
||||
numbered_vertical_color_panel_xml(
|
||||
at_color_top=False,
|
||||
extra_scrollable=True,
|
||||
),
|
||||
False,
|
||||
),
|
||||
("size_only", size_only_panel_xml(), False),
|
||||
)
|
||||
for name, xml_data, expected in cases:
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual(
|
||||
_needs_color_region_restore(ET.fromstring(xml_data)),
|
||||
expected,
|
||||
)
|
||||
|
||||
def test_purchase_does_not_scroll_when_color_region_is_already_visible(self):
|
||||
device = VerticallyScrolledColorPanelDevice(starts_at_top=True)
|
||||
adapter = self._live_adapter(
|
||||
|
||||
@@ -119,6 +119,9 @@ Client 不保存或读取 `purchase.live_*` 手工授权设置。`purchase_mode`
|
||||
同一滚动容器内候选坐标分布和容器形状判断主要方向,再按方向有界遍历;
|
||||
方向证据不足时不滑动、不点击。找到目标后仍须通过精确或唯一繁简等价匹配,
|
||||
不能用近似颜色代替任务规格。
|
||||
- 纵向颜色列表停在中部时,“已选”摘要可能不包含“颜色分类”维度名。只有唯一
|
||||
纵向规格容器、第二规格标题及其上方多个完整可点击卡片同时存在,才允许有界
|
||||
归位到正式颜色标题;单规格、候选不足或容器不唯一时不得猜测滑动。
|
||||
- 运行时规格解析请求必须在网络发送前写入独立 SQLite 记录;只调用一次专用命令,
|
||||
不轮询 Admin。只有 `matched` 响应的哈希、候选编号、原文和 options 都是本次候选的
|
||||
逐字副本时才继续。响应返回后重新完整遍历真机候选并复算哈希,页面、颜色或候选
|
||||
|
||||
Reference in New Issue
Block a user