Files
grok-keygen-new/sso_to_cpa.py
T
chaos ee151343e0 Prefer SSO OAuth PKCE for CPA minting and sync latest accounts.
Switch CPA export to SSO→Authorization Code with referrer=grok-build, keep device-code as optional fallback, and capture new register/auth artifacts.
2026-07-12 15:54:24 +08:00

275 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
邮箱----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())