#!/usr/bin/env python3 from __future__ import annotations import argparse import json import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from autotrain_next import device, ledger, profiles, recorder, runner, sampling, web from autotrain_next.paths import CONFIG, WORK from autotrain_next.platform import scrcpy_binary def config() -> dict: return json.loads(CONFIG.read_text(encoding="utf-8")) if CONFIG.exists() else {"planner": {"enabled": False}, "safety": {"max_loop_steps": 30}} def print_json(data: dict) -> None: print(json.dumps(data, ensure_ascii=False, indent=2)) def main() -> int: parser = argparse.ArgumentParser(description="Autotrain Next 原子工具编排入口") subs = parser.add_subparsers(dest="command", required=True) subs.add_parser("device-status") roster = subs.add_parser("roster-import"); roster.add_argument("source", type=Path) sample = subs.add_parser("sample-import"); sample.add_argument("recording", type=Path); sample.add_argument("--label", default=""); sample.add_argument("--serial", default="") verify = subs.add_parser("profile-verify"); verify.add_argument("--serial", default="") record = subs.add_parser("sample-record"); record.add_argument("--output", default=""); record.add_argument("--serial", default="") task = subs.add_parser("task-create"); task.add_argument("--serial", default=""); task.add_argument("--roster", default=""); task.add_argument("--qr-image", default=""); task.add_argument("--success-action", choices=("download", "email"), default="download"); task.add_argument("--allow-submit", action="store_true") run_task = subs.add_parser("task-run"); run_task.add_argument("task_id") ui = subs.add_parser("web"); ui.add_argument("--port", type=int, default=8766) args = parser.parse_args() if args.command == "device-status": target = device.selected_device(); current = device.fingerprint(target.serial); print_json({"device": current, "profile": profiles.compatibility(profiles.load(target.serial), current)}); return 0 if args.command == "sample-record": # scrcpy 仅供人工控制;录制器明确绑定目标 serial,不记录输入内容到设备档案。 target = device.selected_device(args.serial); output = args.output or str(WORK / "replay" / time.strftime("%Y%m%d-%H%M%S")) subprocess.Popen([scrcpy_binary(), "-s", target.serial, "--window-title", "Autotrain Next 手机屏幕控制"]) recorder.record(Path(output), target.serial) return 0 if args.command == "roster-import": print_json({"roster": str(ledger.import_roster(args.source))}); return 0 if args.command == "sample-import": target = device.selected_device(args.serial); print_json(sampling.import_recording(args.recording, device.fingerprint(target.serial), args.label)); return 0 if args.command == "profile-verify": target = device.selected_device(args.serial); profile = profiles.load(target.serial) if not profile: raise RuntimeError("该手机尚未导入采样") samples = profile.get("sampling", {}).get("samples", {}) missing = sorted(profiles.REQUIRED_SAMPLE_PAGES - set(samples)) if missing: raise RuntimeError(f"采样不完整,缺少页面:{', '.join(missing)}") profile["status"] = "verified"; profiles.save(profile); print_json(profile); return 0 if args.command == "task-create": print_json(runner.create(args.serial, allow_submit=args.allow_submit, roster_path=args.roster, success_action=args.success_action, qr_image=args.qr_image)); return 0 if args.command == "task-run": print_json(runner.run(runner.load(args.task_id), config())); return 0 if args.command == "web": web.serve(args.port); return 0 return 2 if __name__ == "__main__": raise SystemExit(main())