Files
grok-keygen-new/sso_to_cpa.py
T
chaos df9213463d Add bblbb mail provider and CPA push queue; sync runtime artifacts.
Switch default email provider to mail.bblbb.com, queue failed CPA auth pushes for retry, and refresh local auth/account outputs.
2026-07-14 09:14:06 +08:00

300 lines
9.4 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:
# 仅首条在 convert 前统一 flush;这里 flush_first=False 避免并发重复重试
push_res = cpa.push_with_queue(
out_dir,
Path(path).name,
payload,
remote_base=remote_base,
secret=secret,
proxy=str(cfg.get("cpa_push_proxy") or "").strip() or None,
verify_tls=bool(cfg.get("cpa_remote_verify_tls", True)),
flush_first=False,
log=lambda m: log(m.replace("[Debug] ", "").replace("[!] ", "")),
)
result["pushed"] = bool(push_res.get("pushed"))
if "push_status" in push_res:
result["push_status"] = push_res["push_status"]
if push_res.get("push_error"):
result["push_error"] = push_res["push_error"]
else:
cpa.record_push_failure(out_dir, Path(path).name, "remote not configured")
log("push skipped: remote not configured (queued)")
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
do_push = args.push or bool(cfg.get("cpa_push_enabled"))
print(
f"开始转换 count={total} concurrency={args.concurrency} out={out_dir} proxy={proxy}",
flush=True,
)
# 开启推送时先重试历史失败队列,再处理本批
if do_push:
import cpa
remote_base = str(cfg.get("cpa_remote_base") or "").strip()
secret = str(cfg.get("cpa_remote_secret") or "").strip()
if remote_base and secret:
flush = cpa.flush_push_pending(
out_dir,
remote_base=remote_base,
secret=secret,
proxy=str(cfg.get("cpa_push_proxy") or "").strip() or None,
verify_tls=bool(cfg.get("cpa_remote_verify_tls", True)),
log=lambda m: print(m, flush=True),
)
if flush.get("total"):
print(
f"队列重推 ok={flush.get('ok')} fail={flush.get('fail')} "
f"missing={flush.get('missing')} remaining={flush.get('remaining')}",
flush=True,
)
if args.concurrency <= 1:
for i, rec in enumerate(records, 1):
r = convert_one(
rec,
out_dir=out_dir,
proxy=proxy,
push=do_push,
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=do_push,
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())