Files
grok-keygen/scripts/backfill_cpa_xai_from_accounts.py
T
chaos 0d8265de03 Add automatic CPA Management API import after mint
Upload xai-*.json to POST /v0/management/auth-files when cpa_auto_import_remote is enabled. Failures are logged only and never block registration.
2026-07-11 17:47:34 +08:00

217 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Batch mint CPA xai-*.json from register accounts_cli.txt.
Default: headed Chromium + turnstilePatch (headless is Cloudflare-blocked on
accounts.x.ai). Token poll is source of truth; consent Allow uses real click.
Example (from grok_reg project root):
export DISPLAY=:0
uv run python -u scripts/backfill_cpa_xai_from_accounts.py \\
--limit 1 --probe
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import sys
import time
from pathlib import Path
_ROOT = Path(__file__).resolve().parents[1]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
from cpa_xai import existing_cpa_emails, mint_and_export, parse_accounts_file # noqa: E402
from cpa_export import maybe_import_cpa_auth_remote # noqa: E402
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"--accounts",
default=str(_ROOT / "accounts_cli.txt"),
)
ap.add_argument(
"--out-dir",
default=str(_ROOT / "cpa_auths"),
help="Primary output under register machine",
)
ap.add_argument(
"--cpa-dir",
default="",
help="Optional CPA hot-load auth-dir; files are copied here after success",
)
ap.add_argument("--limit", type=int, default=0, help="0 = all missing")
ap.add_argument("--offset", type=int, default=0)
ap.add_argument("--email", default="", help="Only this email")
ap.add_argument("--skip-existing", action="store_true", default=True)
ap.add_argument("--no-skip-existing", action="store_false", dest="skip_existing")
ap.add_argument(
"--headless",
action="store_true",
default=False,
help="Headless Chromium (usually blocked by Cloudflare on accounts.x.ai)",
)
ap.add_argument(
"--headed",
action="store_true",
default=True,
help="Show browser (default; required for stable device consent)",
)
ap.add_argument("--probe", action="store_true", default=True)
ap.add_argument("--no-probe", action="store_false", dest="probe")
ap.add_argument("--probe-chat", action="store_true", default=False)
ap.add_argument(
"--proxy",
default="",
help="Outbound proxy. Empty → read register config.json cpa_proxy/proxy, else env",
)
ap.add_argument(
"--config",
default=str(_ROOT / "config.json"),
help="register config.json for cpa_proxy/proxy defaults",
)
ap.add_argument("--timeout", type=float, default=300.0)
ap.add_argument("--sleep", type=float, default=3.0, help="Sleep between accounts")
ap.add_argument(
"--fail-log",
default=str(_ROOT / "cpa_auths" / "backfill_failed.jsonl"),
help="Append failures JSONL",
)
ap.add_argument(
"--force-standalone",
action="store_true",
default=True,
help="Always open fresh Chromium (default)",
)
args = ap.parse_args()
args._register_cfg = {}
if args.headless:
args.headed = False
else:
args.headless = False
# Resolve proxy: CLI > config cpa_proxy/proxy > env
if not args.proxy:
try:
cfg_path = Path(args.config)
if cfg_path.is_file():
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
if isinstance(cfg, dict):
cfg = {
k: v
for k, v in cfg.items()
if not (isinstance(k, str) and (k.startswith("//") or k.startswith("#")))
}
args.proxy = (cfg.get("cpa_proxy") or cfg.get("proxy") or "").strip()
if not args.cpa_dir:
args.cpa_dir = (cfg.get("cpa_hotload_dir") or "").strip()
args._register_cfg = cfg
except Exception as e: # noqa: BLE001
print(f"warn: read config proxy failed: {e}", flush=True)
if not args.proxy:
args.proxy = (
os.environ.get("https_proxy")
or os.environ.get("HTTPS_PROXY")
or os.environ.get("http_proxy")
or ""
).strip()
print(f"proxy={args.proxy or '(none)'}", flush=True)
accounts = parse_accounts_file(args.accounts)
if args.email:
accounts = [a for a in accounts if a.email.lower() == args.email.lower()]
accounts = accounts[args.offset :]
have = set()
if args.skip_existing:
have |= {e.lower() for e in existing_cpa_emails(args.out_dir)}
if args.cpa_dir:
have |= {e.lower() for e in existing_cpa_emails(args.cpa_dir)}
todo = []
for a in accounts:
if args.skip_existing and a.email.lower() in have:
continue
todo.append(a)
if args.limit and len(todo) >= args.limit:
break
print(f"accounts total={len(parse_accounts_file(args.accounts))} todo={len(todo)} out={args.out_dir}")
Path(args.out_dir).mkdir(parents=True, exist_ok=True)
if args.cpa_dir:
Path(args.cpa_dir).mkdir(parents=True, exist_ok=True)
ok_n = fail_n = 0
results = []
for i, acc in enumerate(todo, 1):
print(f"\n=== [{i}/{len(todo)}] {acc.email} ===", flush=True)
def log(msg: str, _email=acc.email) -> None:
print(f"[{time.strftime('%H:%M:%S')}] [{_email}] {msg}", flush=True)
r = mint_and_export(
email=acc.email,
password=acc.password,
auth_dir=args.out_dir,
page=None,
proxy=args.proxy or None,
headless=args.headless,
probe=args.probe,
probe_chat=args.probe_chat,
browser_timeout_sec=args.timeout,
force_standalone=args.force_standalone,
sso=acc.sso or None,
prefer_protocol=True,
log=log,
)
results.append(r)
if r.get("ok") and r.get("path"):
ok_n += 1
# mirror into CPA auth-dir
if args.cpa_dir:
src = Path(r["path"])
dst = Path(args.cpa_dir) / src.name
shutil.copy2(src, dst)
os.chmod(dst, 0o600)
print(f"copied -> {dst}", flush=True)
# optional remote Management API import (config-gated; never fails the mint)
try:
reg_cfg = getattr(args, "_register_cfg", None) or {}
remote = maybe_import_cpa_auth_remote(
r["path"],
config=reg_cfg,
log_callback=lambda m: print(m, flush=True),
)
if remote.get("ok"):
print(
f"remote import ok -> {remote.get('base')} name={remote.get('name')}",
flush=True,
)
elif not remote.get("skipped"):
print(f"remote import failed: {remote.get('error')}", flush=True)
except Exception as e: # noqa: BLE001
print(f"remote import unexpected error: {e}", flush=True)
else:
fail_n += 1
if args.fail_log:
Path(args.fail_log).parent.mkdir(parents=True, exist_ok=True)
with open(args.fail_log, "a", encoding="utf-8") as f:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
if args.sleep and i < len(todo):
time.sleep(args.sleep)
print(f"\n=== done ok={ok_n} fail={fail_n} ===", flush=True)
summary = Path(args.out_dir) / f"backfill_summary_{int(time.time())}.json"
summary.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"summary {summary}")
return 0 if ok_n > 0 or not todo else 1
if __name__ == "__main__":
raise SystemExit(main())