"""采集人工已停在规格面板的三种状态证据;不执行任何页面操作。""" from __future__ import annotations import argparse 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.product_url import ProductUrl, ProductUrlError, parse_product_url from cmbuyer_client.pdd.sku_panel_spike import ( HUMAN_DECLARED_STATES, SkuPanelEvidenceCapturer, SkuPanelEvidenceError, ) def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="采集人工已打开的拼多多规格面板状态证据。") parser.add_argument("--serial", required=True, help="ADB device serial;禁止自动选择。") product_source = parser.add_mutually_exclusive_group(required=True) product_source.add_argument("--url", help="唯一 canonical goods.html?goods_id= 直链。") product_source.add_argument("--goods-id", help="纯数字商品标识;仅用于记录证据归属。") parser.add_argument("--state", required=True, choices=sorted(HUMAN_DECLARED_STATES), help="人工声明的面板状态。") parser.add_argument("--output-dir", required=True, type=Path, help="新建的本地证据目录;不得覆盖已有目录。") parser.add_argument("--timeout", type=float, default=10.0, help="ADB 和只读 RPC 超时(秒)。") parser.add_argument("--adb", default="adb", help="adb 可执行文件路径。") return parser.parse_args(argv) def resolve_product_url(arguments: argparse.Namespace) -> ProductUrl: if isinstance(arguments.url, str): return parse_product_url(arguments.url) if isinstance(arguments.goods_id, str): # 仅使用严格 parser 重新验证并构建,不把输入交给 ADB 或页面。 return parse_product_url(f"https://mobile.yangkeduo.com/goods.html?goods_id={arguments.goods_id}") raise ValueError("必须提供 --url 或 --goods-id。") def validate_arguments(arguments: argparse.Namespace) -> ProductUrl: if not isinstance(arguments.serial, str) or not arguments.serial.strip(): raise ValueError("必须显式提供非空 --serial。") 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 的有限数值。") if arguments.state not in HUMAN_DECLARED_STATES: raise ValueError("--state 必须是允许的人工声明状态。") return resolve_product_url(arguments) def main(argv: list[str] | None = None) -> int: arguments = parse_arguments(argv) try: link = validate_arguments(arguments) except (ValueError, ProductUrlError) as error: print(f"失败:{error}", file=sys.stderr) return 2 try: import adbutils import uiautomator2 as u2 except ImportError: print("失败:缺少 uiautomator2;请在采购工具虚拟环境中运行。", file=sys.stderr) return 2 client = AdbClient(SubprocessAdbRunner(arguments.adb), timeout_seconds=arguments.timeout) connector = NoReconnectUiautomatorConnector( adbutils.AdbClient(socket_timeout=arguments.timeout).device_list, u2.connect, ) capturer = SkuPanelEvidenceCapturer(client, connector, timeout_seconds=arguments.timeout) try: result = capturer.capture( arguments.serial, link.canonical_url, arguments.state, arguments.output_dir, ) except (DeviceConnectionError, SkuPanelEvidenceError) as error: # 不打印 ADB 输出、serial、Activity、XML 或页面正文。 print(f"规格面板证据采集失败:{error}", file=sys.stderr) return 1 except OSError: print("规格面板证据采集失败:无法创建或发布本地证据目录。", file=sys.stderr) return 1 print(f"规格面板证据采集完成:{result.output_directory}") print(f"manifest:{result.manifest_path}") return 0 if __name__ == "__main__": raise SystemExit(main())