#!/usr/bin/env python # -*- coding: utf-8 -*- """批量 SSO → CPA OAuth(referrer=grok-build)。 用法: python sso_to_cpa.py --sso accounts.txt --out-dir ./cpa_auths python sso_to_cpa.py --sso-cookie "eyJ..." --email a@b.com python sso_to_cpa.py --sso accounts.txt --concurrency 4 --proxy http://127.0.0.1:7897 输入行格式: 邮箱----密码----sso_jwt 邮箱----sso_jwt """ from __future__ import annotations import argparse import json import os import sys import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path _ROOT = Path(__file__).resolve().parent if str(_ROOT) not in sys.path: sys.path.insert(0, str(_ROOT)) def _load_config() -> dict: cfg_path = _ROOT / "config.json" if not cfg_path.is_file(): return {} try: with open(cfg_path, "r", encoding="utf-8") as f: return json.load(f) except Exception: return {} def _parse_line(line: str) -> tuple[str, str]: line = (line or "").strip() if not line or line.startswith("#"): return "", "" email = "" sso = line if "----" in line: parts = [p.strip() for p in line.split("----")] if parts: email = parts[0] sso = parts[-1] return email, sso def load_records(sso_file: str, sso_cookie: str, email: str) -> list[dict]: from oidc_mint.oauth_code import normalize_sso_cookie out: list[dict] = [] seen: set[str] = set() if sso_cookie.strip(): sso = normalize_sso_cookie(sso_cookie) if sso and sso not in seen: seen.add(sso) out.append({"email": (email or "").strip(), "sso": sso}) return out if not sso_file: return out path = Path(sso_file) data = path.read_text(encoding="utf-8", errors="replace") for line in data.splitlines(): em, sso_raw = _parse_line(line) sso = normalize_sso_cookie(sso_raw) if not sso or sso in seen: continue seen.add(sso) out.append({"email": em, "sso": sso}) return out def convert_one( rec: dict, *, out_dir: Path, proxy: str | None, push: bool, cfg: dict, lock: threading.Lock, index: int, total: int, ) -> dict: from oidc_mint.oauth_code import OAuthCodeError, mint_from_sso import cpa email = (rec.get("email") or "").strip() sso = rec["sso"] tag = email or sso[:16] def log(msg: str) -> None: print(f"[{index}/{total}] {msg}", flush=True) # 预检查已存在 if email: pre = out_dir / cpa.credential_file_name(email) if pre.is_file(): log(f"skip exists {pre.name}") return {"ok": True, "skipped": True, "email": email, "path": str(pre)} log(f"converting {tag}") try: tokens = mint_from_sso( sso, proxy=proxy, log=lambda m: log(f" {m}"), require_referrer=True, ) except OAuthCodeError as exc: log(f"FAIL {tag}: {exc}") return {"ok": False, "email": email, "error": str(exc)} except Exception as exc: # noqa: BLE001 log(f"FAIL {tag}: {exc}") return {"ok": False, "email": email, "error": str(exc)} # email 可从 id_token 补 if not email and tokens.get("id_token"): try: from cpa.schema import _jwt_payload email = str(_jwt_payload(tokens["id_token"]).get("email") or "").strip() except Exception: pass payload = cpa.build_cpa_xai_auth( email=email, access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], id_token=tokens.get("id_token"), expires_in=tokens.get("expires_in"), base_url=cfg.get("cpa_base_url") or cpa.CLI_BASE_URL, sso=sso, ) with lock: path = cpa.write_cpa_xai_auth(out_dir, payload) log(f"OK {path.name} referrer={payload.get('referrer')!r}") result = { "ok": True, "email": email or payload.get("email"), "path": str(path), "referrer": payload.get("referrer"), "pushed": False, } if push: remote_base = str(cfg.get("cpa_remote_base") or "").strip() secret = str(cfg.get("cpa_remote_secret") or "").strip() if remote_base and secret: try: ok, status, text = cpa.push_auth_file( remote_base=remote_base, secret=secret, filename=Path(path).name, payload=payload, proxy=str(cfg.get("cpa_push_proxy") or "").strip() or None, verify_tls=bool(cfg.get("cpa_remote_verify_tls", True)), ) result["pushed"] = ok result["push_status"] = status if ok: log(f"pushed HTTP {status}") else: log(f"push fail HTTP {status}: {text[:160]}") except Exception as exc: # noqa: BLE001 log(f"push error: {exc}") result["push_error"] = str(exc) return result def main(argv: list[str] | None = None) -> int: cfg = _load_config() default_proxy = ( cfg.get("mint_proxy") or cfg.get("proxy") or os.environ.get("https_proxy") or os.environ.get("http_proxy") or "http://127.0.0.1:7897" ) default_out = cfg.get("cpa_auth_dir") or "./cpa_auths" p = argparse.ArgumentParser(description="SSO cookie → CPA xai auth (referrer=grok-build)") p.add_argument("--sso", default="", help="sso 列表文件") p.add_argument("--sso-cookie", default="", help="单个 sso cookie") p.add_argument("--email", default="", help="单 cookie 时的 email") p.add_argument("--out-dir", default=default_out, help="输出目录") p.add_argument("--proxy", default=default_proxy, help="HTTP 代理") p.add_argument("--concurrency", type=int, default=1, help="并发数") p.add_argument("--delay", type=float, default=0, help="串行时每个间隔秒") p.add_argument("--push", action="store_true", help="推送到 config 里的远端 CPA") args = p.parse_args(argv) records = load_records(args.sso, args.sso_cookie, args.email) if not records: print("需要 --sso 或 --sso-cookie", file=sys.stderr) return 1 out_dir = Path(args.out_dir) if not out_dir.is_absolute(): out_dir = (_ROOT / out_dir).resolve() out_dir.mkdir(parents=True, exist_ok=True) proxy = (args.proxy or "").strip() or None lock = threading.Lock() total = len(records) ok = fail = skipped = 0 print( f"开始转换 count={total} concurrency={args.concurrency} out={out_dir} proxy={proxy}", flush=True, ) if args.concurrency <= 1: for i, rec in enumerate(records, 1): r = convert_one( rec, out_dir=out_dir, proxy=proxy, push=args.push or bool(cfg.get("cpa_push_enabled")), cfg=cfg, lock=lock, index=i, total=total, ) if r.get("skipped"): skipped += 1 elif r.get("ok"): ok += 1 else: fail += 1 if args.delay > 0 and i < total: time.sleep(args.delay) else: with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: futs = [ ex.submit( convert_one, rec, out_dir=out_dir, proxy=proxy, push=args.push or bool(cfg.get("cpa_push_enabled")), cfg=cfg, lock=lock, index=i, total=total, ) for i, rec in enumerate(records, 1) ] for fut in as_completed(futs): r = fut.result() if r.get("skipped"): skipped += 1 elif r.get("ok"): ok += 1 else: fail += 1 print(f"完成 success={ok} failed={fail} skipped={skipped} total={total}", flush=True) return 1 if fail else 0 if __name__ == "__main__": raise SystemExit(main())