fix(client): accept confirmed T-107 panel profile

This commit is contained in:
QiuSW
2026-08-06 15:33:15 +08:00
parent 1d60a91ace
commit 50a572f3e3
6 changed files with 125 additions and 27 deletions
@@ -31,15 +31,6 @@ FINAL_PANEL_QUANTITY = 2
_PDD_ID = "com.xunmeng.pinduoduo:id/pdd"
_QUANTITY_ID = "com.xunmeng.pinduoduo:id/gnl"
_TITLE_ID = "com.xunmeng.pinduoduo:id/tv_title"
_PANEL_BOUNDS = "[0,551][1080,938]"
_PRICE_FRAME_BOUNDS = "[396,575][1053,647]"
_PRICE_ROW_BOUNDS = "[396,575][740,647]"
_PRICE_TEXT_BOUNDS = "[396,580][722,647]"
_SUMMARY_BOUNDS = "[396,659][1053,721]"
_QUANTITY_BOUNDS = "[396,827][645,902]"
_MINUS_BOUNDS = "[396,827][474,902]"
_VALUE_BOUNDS = "[480,827][561,902]"
_PLUS_BOUNDS = "[567,827][645,902]"
_SUBMIT_TEXT_BOUNDS = "[366,2225][714,2284]"
_SUBMIT_TEXT_PARENT_BOUNDS = "[354,2181][726,2328]"
_SUBMIT_ACTION_BOUNDS = "[0,2181][1080,2328]"
@@ -63,6 +54,50 @@ _ENTRY_CHILDREN = (
_DANGEROUS_RETURN_TERMS = ("提交订单", "确认订单", "立即支付", "去支付", "付款")
@dataclass(frozen=True)
class _PanelProfile:
evidence_id: str
panel_bounds: str
price_frame_bounds: str
price_row_bounds: str
price_text_bounds: str
summary_bounds: str
quantity_bounds: str
minus_bounds: str
value_bounds: str
plus_bounds: str
# 两个配置分别绑定 T-106 原始证据和 T-107 同版本人工五项确认的新证据。
# 只能整套精确命中其中一个,禁止把 77px 位移改成范围或混搭坐标。
_PANEL_PROFILES = (
_PanelProfile(
evidence_id="t106-final-panel-8.17.0",
panel_bounds="[0,551][1080,938]",
price_frame_bounds="[396,575][1053,647]",
price_row_bounds="[396,575][740,647]",
price_text_bounds="[396,580][722,647]",
summary_bounds="[396,659][1053,721]",
quantity_bounds="[396,827][645,902]",
minus_bounds="[396,827][474,902]",
value_bounds="[480,827][561,902]",
plus_bounds="[567,827][645,902]",
),
_PanelProfile(
evidence_id="t107-final-panel-shifted-8.17.0",
panel_bounds="[0,474][1080,861]",
price_frame_bounds="[396,498][1053,570]",
price_row_bounds="[396,498][740,570]",
price_text_bounds="[396,503][722,570]",
summary_bounds="[396,582][1053,644]",
quantity_bounds="[396,750][645,825]",
minus_bounds="[396,750][474,825]",
value_bounds="[480,750][561,825]",
plus_bounds="[567,750][645,825]",
),
)
class FinalSubmitPanelError(RuntimeError):
"""最终面板或 Gate3 判据不成立时的脱敏安全停止。"""
@@ -254,28 +289,32 @@ def _validate_gate2(gate2: object) -> None:
def _verified_final_panel(nodes: list[_Node]) -> _VerifiedPanel:
panel = _one(
node
profile_matches = [
(profile, node)
for profile in _PANEL_PROFILES
for node in nodes
if _exact(node, "android.view.ViewGroup", _PANEL_BOUNDS, resource_id=_PDD_ID)
)
if _exact(node, "android.view.ViewGroup", profile.panel_bounds, resource_id=_PDD_ID)
]
if len(profile_matches) != 1:
raise FinalSubmitPanelError("最终面板未唯一命中已人工确认的精确布局配置。")
profile, panel = profile_matches[0]
price_frame = _one(
node
for node in nodes
if node.parent is panel
and _exact(node, "android.widget.FrameLayout", _PRICE_FRAME_BOUNDS, resource_id=_PDD_ID)
and _exact(node, "android.widget.FrameLayout", profile.price_frame_bounds, resource_id=_PDD_ID)
)
price_row = _one(
node
for node in nodes
if node.parent is price_frame
and _exact(node, "android.widget.LinearLayout", _PRICE_ROW_BOUNDS, resource_id=_PDD_ID)
and _exact(node, "android.widget.LinearLayout", profile.price_row_bounds, resource_id=_PDD_ID)
)
price = _one(
node
for node in nodes
if node.parent is price_row
and _exact_text_node(node, "android.widget.TextView", _PRICE_TEXT_BOUNDS, resource_id=_PDD_ID)
and _exact_text_node(node, "android.widget.TextView", profile.price_text_bounds, resource_id=_PDD_ID)
and _GATE2_TEXT.fullmatch(node.text) is not None
)
gate2_match = _GATE2_TEXT.fullmatch(price.text)
@@ -287,20 +326,20 @@ def _verified_final_panel(nodes: list[_Node]) -> _VerifiedPanel:
node
for node in nodes
if node.parent is panel
and _exact_text_node(node, "android.widget.TextView", _SUMMARY_BOUNDS, resource_id=_PDD_ID)
and _exact_text_node(node, "android.widget.TextView", profile.summary_bounds, resource_id=_PDD_ID)
and node.text == _EXPECTED_SUMMARY
)
quantity_outer = _one(
node
for node in nodes
if node.parent is panel
and _exact(node, "android.widget.LinearLayout", _QUANTITY_BOUNDS, resource_id=_QUANTITY_ID)
and _exact(node, "android.widget.LinearLayout", profile.quantity_bounds, resource_id=_QUANTITY_ID)
)
quantity_inner = _one(
node
for node in nodes
if node.parent is quantity_outer
and _exact(node, "android.widget.LinearLayout", _QUANTITY_BOUNDS, resource_id="")
and _exact(node, "android.widget.LinearLayout", profile.quantity_bounds, resource_id="")
)
quantity_children = _direct_children(quantity_inner)
if len(quantity_children) != 3:
@@ -308,21 +347,21 @@ def _verified_final_panel(nodes: list[_Node]) -> _VerifiedPanel:
_one(
node
for node in quantity_children
if _exact(node, "android.widget.ImageView", _MINUS_BOUNDS, resource_id=_PDD_ID, clickable="true")
if _exact(node, "android.widget.ImageView", profile.minus_bounds, resource_id=_PDD_ID, clickable="true")
and not node.text
and node.desc == "减少数量"
)
_one(
node
for node in quantity_children
if _exact(node, "android.widget.EditText", _VALUE_BOUNDS, resource_id=_PDD_ID, clickable="true")
if _exact(node, "android.widget.EditText", profile.value_bounds, resource_id=_PDD_ID, clickable="true")
and node.text == str(FINAL_PANEL_QUANTITY)
and not node.desc
)
_one(
node
for node in quantity_children
if _exact(node, "android.widget.ImageView", _PLUS_BOUNDS, resource_id=_PDD_ID, clickable="true")
if _exact(node, "android.widget.ImageView", profile.plus_bounds, resource_id=_PDD_ID, clickable="true")
and not node.text
and node.desc == "增加数量"
)
@@ -0,0 +1,25 @@
<hierarchy rotation="0">
<!-- T-107 2026-08-06 人工五项确认:同版本面板整体上移 77px;底部最终控件不变。 -->
<node package="com.xunmeng.pinduoduo" class="android.view.ViewGroup" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[0,474][1080,861]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node package="com.xunmeng.pinduoduo" class="android.widget.FrameLayout" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[396,498][1053,570]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node package="com.xunmeng.pinduoduo" class="android.widget.LinearLayout" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[396,498][740,570]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node text="快卖完 ¥32.76" package="com.xunmeng.pinduoduo" class="android.widget.TextView" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[396,503][722,570]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false" />
</node>
</node>
<node text="已选: 黑色 CHA (纯棉) M(建议100-115)" package="com.xunmeng.pinduoduo" class="android.widget.TextView" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[396,582][1053,644]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false" />
<node package="com.xunmeng.pinduoduo" class="android.widget.LinearLayout" resource-id="com.xunmeng.pinduoduo:id/gnl" bounds="[396,750][645,825]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node package="com.xunmeng.pinduoduo" class="android.widget.LinearLayout" bounds="[396,750][645,825]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node content-desc="减少数量" package="com.xunmeng.pinduoduo" class="android.widget.ImageView" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[396,750][474,825]" clickable="true" enabled="true" visible-to-user="true" selected="false" scrollable="false" />
<node text="2" package="com.xunmeng.pinduoduo" class="android.widget.EditText" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[480,750][561,825]" clickable="true" enabled="true" visible-to-user="true" selected="false" scrollable="false" />
<node content-desc="增加数量" package="com.xunmeng.pinduoduo" class="android.widget.ImageView" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[567,750][645,825]" clickable="true" enabled="true" visible-to-user="true" selected="false" scrollable="false" />
</node>
</node>
</node>
<node package="com.xunmeng.pinduoduo" class="android.view.ViewGroup" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[0,366][1080,2328]" clickable="true" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node package="com.xunmeng.pinduoduo" class="android.widget.FrameLayout" resource-id="com.xunmeng.pinduoduo:id/pdd" bounds="[0,2181][1080,2328]" clickable="true" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node package="com.xunmeng.pinduoduo" class="android.widget.LinearLayout" bounds="[354,2181][726,2328]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false">
<node text="提交订单 ¥32.76" package="com.xunmeng.pinduoduo" class="android.widget.TextView" bounds="[366,2225][714,2284]" clickable="false" enabled="true" visible-to-user="true" selected="false" scrollable="false" />
</node>
</node>
</node>
</hierarchy>
+23 -1
View File
@@ -52,8 +52,10 @@ from cmbuyer_client.pdd.quantity_gate2 import (
SERIAL = "192.168.0.173:5555"
FIXTURES = Path(__file__).with_name("fixtures")
PANEL_FIXTURE = FIXTURES / "final_submit_panel_8_17_0.xml"
SHIFTED_PANEL_FIXTURE = FIXTURES / "final_submit_panel_shifted_8_17_0.xml"
EXIT_FIXTURE = FIXTURES / "final_submit_panel_exit_8_17_0.xml"
PANEL = PANEL_FIXTURE.read_text(encoding="utf-8")
SHIFTED_PANEL = SHIFTED_PANEL_FIXTURE.read_text(encoding="utf-8")
EXIT = EXIT_FIXTURE.read_text(encoding="utf-8")
@@ -119,6 +121,23 @@ class FinalSubmitPanelObserverTests(unittest.TestCase):
self.assertTrue(observation.nearest_clickable_ancestor_unique)
self.assertEqual(observation.quantity_read, 2)
def test_each_human_confirmed_exact_panel_profile_is_accepted_independently(self) -> None:
for raw in (PANEL, SHIFTED_PANEL):
with self.subTest(panel_bounds=ElementTree.fromstring(raw)[0].get("bounds")):
observation = _observe(raw)
self.assertEqual(observation.gate2_panel_total_price, "32.76")
self.assertEqual(observation.gate3_submit_amount, "32.76")
def test_panel_profiles_cannot_be_ranged_or_mixed(self) -> None:
cases = (
SHIFTED_PANEL.replace("[0,474][1080,861]", "[0,475][1080,862]"),
SHIFTED_PANEL.replace("[396,582][1053,644]", "[396,659][1053,721]"),
PANEL.replace("[396,827][645,902]", "[396,750][645,825]"),
)
for raw in cases:
with self.subTest(), self.assertRaises(FinalSubmitPanelError):
_observe(raw)
def test_dynamic_canonical_amount_is_allowed_only_when_both_roles_and_gate2_agree(self) -> None:
raw = PANEL.replace("快卖完 ¥32.76", "快卖完 ¥39.99").replace(
"提交订单 ¥32.76", "提交订单 ¥39.99"
@@ -675,7 +694,10 @@ class FinalSubmitPanelStaticBoundaryTests(unittest.TestCase):
self.assertNotIn(token, source)
def test_fixtures_contain_no_unrelated_personal_or_payment_data(self) -> None:
combined = PANEL_FIXTURE.read_text(encoding="utf-8") + EXIT_FIXTURE.read_text(encoding="utf-8")
combined = "".join(
path.read_text(encoding="utf-8")
for path in (PANEL_FIXTURE, SHIFTED_PANEL_FIXTURE, EXIT_FIXTURE)
)
for forbidden in ("手机号", "收货地址", "支付密码", "微信支付", "支付宝"):
self.assertNotIn(forbidden, combined)