Switch default email provider to mail.bblbb.com, queue failed CPA auth pushes for retry, and refresh local auth/account outputs.
300 lines
9.4 KiB
Python
300 lines
9.4 KiB
Python
#!/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())
|