"""运行 T-107 最终面板 Gate3 只读观察与一次安全返回。""" from __future__ import annotations import argparse from datetime import datetime from math import isfinite from pathlib import Path import sys CLIENT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(CLIENT_ROOT / "src")) from cmbuyer_client.device.adb import AdbClient, DeviceConnectionError, SubprocessAdbRunner from cmbuyer_client.device.baseline import NoReconnectUiautomatorConnector from cmbuyer_client.pdd.final_submit_panel import FinalSubmitPanelError from cmbuyer_client.pdd.final_submit_panel_runner import FinalSubmitPanelRunner from cmbuyer_client.pdd.quantity_gate2 import ( EXPECTED_GATE1_UNIT_PRICE, EXPECTED_GOODS_ID, TASK_COLOR, TASK_SIZE, Gate2Observation, ) from cmbuyer_client.pdd.quantity_gate2_spike import Android16TopResumedForegroundReader def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="运行 T-107 围栏前最终面板只读 dry-run。") parser.add_argument("--serial", required=True, help="显式 ADB serial;禁止自动选择。") parser.add_argument("--goods-id", required=True) parser.add_argument("--color", required=True) parser.add_argument("--size", required=True) parser.add_argument("--quantity", required=True, type=int) parser.add_argument("--gate1-unit-price", required=True) parser.add_argument("--gate2-panel-total-price", required=True) parser.add_argument("--gate2-screenshot", required=True, type=Path) parser.add_argument("--gate2-captured-at", required=True) parser.add_argument("--max-total-price", required=True) parser.add_argument("--output-dir", required=True, type=Path) parser.add_argument("--timeout", type=float, default=10.0) parser.add_argument("--adb", default="adb") return parser.parse_args(argv) def validate_arguments(arguments: argparse.Namespace) -> datetime: if type(arguments.serial) is not str or not arguments.serial.strip() or arguments.serial != arguments.serial.strip(): raise ValueError("必须显式提供非空 --serial。") if arguments.goods_id != EXPECTED_GOODS_ID: raise ValueError("--goods-id 不是 T-107 已批准目标。") if arguments.color != TASK_COLOR or arguments.size != TASK_SIZE: raise ValueError("颜色或尺码不是 T-107 已批准目标。") if type(arguments.quantity) is not int or arguments.quantity != 2: raise ValueError("本次真机验收只批准 --quantity 2。") if arguments.gate1_unit_price != EXPECTED_GATE1_UNIT_PRICE: raise ValueError("--gate1-unit-price 与已确认 Gate1 不一致。") if arguments.gate2_panel_total_price != "32.76": raise ValueError("--gate2-panel-total-price 与 T-106 已确认值不一致。") if not isinstance(arguments.gate2_screenshot, Path) or not arguments.gate2_screenshot.is_file(): raise ValueError("--gate2-screenshot 必须是现有显式文件。") try: captured_at = datetime.fromisoformat(arguments.gate2_captured_at) except (TypeError, ValueError) as error: raise ValueError("--gate2-captured-at 必须是带时区 ISO 时间。") from error if captured_at.utcoffset() is None: raise ValueError("--gate2-captured-at 必须带时区。") if arguments.max_total_price != "40.00": raise ValueError("本次真机验收固定 --max-total-price 40.00。") if not isinstance(arguments.output_dir, Path) or not arguments.output_dir.name or arguments.output_dir.exists(): raise ValueError("--output-dir 必须是不存在的明确新目录。") if ( not isinstance(arguments.timeout, (int, float)) or isinstance(arguments.timeout, bool) or arguments.timeout <= 0 or not isfinite(arguments.timeout) ): raise ValueError("--timeout 必须是大于 0 的有限数值。") return captured_at def main(argv: list[str] | None = None) -> int: arguments = parse_arguments(argv) try: captured_at = validate_arguments(arguments) except ValueError as error: print(f"失败:{error}", file=sys.stderr) return 2 try: import adbutils import uiautomator2 as u2 except ImportError: print("失败:缺少采购工具真机依赖。", file=sys.stderr) return 2 adb_runner = SubprocessAdbRunner(arguments.adb) runner = FinalSubmitPanelRunner( AdbClient(adb_runner, timeout_seconds=arguments.timeout), NoReconnectUiautomatorConnector( adbutils.AdbClient(socket_timeout=arguments.timeout).device_list, u2.connect, ), Android16TopResumedForegroundReader(adb_runner, arguments.timeout), timeout_seconds=arguments.timeout, ) gate2 = Gate2Observation( requested_color=arguments.color, requested_size=arguments.size, actual_color=arguments.color, actual_size=arguments.size, requested_quantity=arguments.quantity, quantity_read=arguments.quantity, gate1_unit_price=arguments.gate1_unit_price, gate2_panel_total_price=arguments.gate2_panel_total_price, max_total_price=arguments.max_total_price, screenshot_path=arguments.gate2_screenshot, captured_at=captured_at, ) try: runner.run( arguments.serial, arguments.goods_id, gate2, arguments.output_dir, ) except (DeviceConnectionError, FinalSubmitPanelError, OSError): # 不回显第三方异常、serial、本机路径或页面正文。 print("T-107 最终面板 dry-run 失败:已停止,未发布证据目录。", file=sys.stderr) return 1 print("T-107 最终面板 dry-run 完成。") print("人工复核:Gate2/Gate3、最终控件唯一、一次安全返回,且未创建订单或进入付款。") return 0 if __name__ == "__main__": raise SystemExit(main())