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.
This commit is contained in:
chaos committed 2026-07-12 15:54:24 +08:00
1 parent 8b3664a2d5
commit ee151343e0
455 files changed
+9051 -59

No files matched your search

+274
View File
@@ -0,0 +1,274 @@
#!/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())