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:
1 parent
8b3664a2d5
commit
ee151343e0
455 files changed
+9051
-59
No files matched your search
+274
@@ -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())
|
||||
Reference in new issue
Block a user