#!/usr/bin/env python3 """检查第二阶段验证码验证所需的离线证据是否齐备。""" from __future__ import annotations import argparse import imghdr import json import sys import wave from pathlib import Path from typing import Any def configure_utf8_stdio() -> None: for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): stream.reconfigure(encoding="utf-8") configure_utf8_stdio() REQUIRED_BY_TYPE = { "text": ["image"], "math": ["image_or_text"], "slider": ["background_or_screenshot", "track_width"], "click-select": ["screenshot", "prompt"], "rotate": ["image", "track_width"], "grid": ["screenshot", "prompt"], "audio": ["audio"], "drag-drop": ["screenshot", "source_target_bounds"], "trace-draw": ["screenshot_or_path_points"], "scratch": ["box"], "image-restore": ["image_or_tiles"], "area-select": ["screenshot", "prompt"], "difference-click": ["image_pair_or_screenshot"], "font-identify": ["screenshot", "prompt"], "semantic-reasoning": ["screenshot", "prompt"], "game-challenge": ["screenshot", "prompt_or_provider_key"], "pow-challenge": ["challenge_payload"], "risk-score": ["sitekey", "action_or_server_log"], "one-click": ["component_html"], "multi-step": ["round_screenshots"], "qa-logic": ["question"], "token-widget": ["sitekey", "pageurl"], "waf-challenge": ["headers_or_challenge_html"], } def inspect_file(path: Path) -> dict[str, Any]: item: dict[str, Any] = { "path": str(path), "exists": path.exists(), "is_file": path.is_file() if path.exists() else False, } if not item["is_file"]: return item item["size_bytes"] = path.stat().st_size image_type = imghdr.what(path) if image_type: item["kind"] = f"image/{image_type}" elif path.suffix.lower() in {".mp3", ".ogg", ".m4a", ".flac"}: item["kind"] = f"audio/{path.suffix.lower().lstrip('.')}" elif path.suffix.lower() == ".wav": item["kind"] = "audio/wav" try: with wave.open(str(path), "rb") as handle: item["audio"] = { "channels": handle.getnchannels(), "sample_width": handle.getsampwidth(), "frame_rate": handle.getframerate(), "frames": handle.getnframes(), } except wave.Error: item["audio_error"] = "无法读取 wav 元信息" else: item["kind"] = "text-or-binary" return item def build_report(args: argparse.Namespace) -> dict[str, Any]: files = [inspect_file(Path(value)) for value in args.file or []] provided = set(args.provided or []) captcha_type = args.captcha_type required = REQUIRED_BY_TYPE.get(captcha_type, []) missing = [name for name in required if name not in provided] return { "captcha_type": captcha_type, "required_evidence": required, "provided_evidence": sorted(provided), "missing_evidence": missing, "files": files, "ready_for_offline_flow": not missing and all(item.get("exists") for item in files), "notes": [ "只检查离线证据齐备性,不打开网页、不读取 Cookie/Storage、不提交验证。", "如果要真实网页执行,必须按 verification-workflow.md 再次确认。", ], } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="检查验证码验证流程所需证据") parser.add_argument("--captcha-type", required=True) parser.add_argument("--file", action="append", help="证据文件路径,可重复") parser.add_argument("--provided", action="append", help="已具备的证据项,如 image/sitekey/pageurl") parser.add_argument("--pretty", action="store_true") return parser def main() -> int: args = build_parser().parse_args() report = build_report(args) print(json.dumps(report, ensure_ascii=False, indent=2 if args.pretty else None)) return 0 if __name__ == "__main__": raise SystemExit(main())