"""注册成功钩子:SSO → OAuth Authorization Code(PKCE, referrer=grok-build) → 写出 CPA (CLIProxyAPI) 的 xai-.json → 推送到远端 CLIProxyAPI 导入。 - 本地写盘目录:config['cpa_auth_dir'](默认 ./cpa_auths) - 远端推送:POST config['cpa_remote_base'] + /v0/management/auth-files?name=... 认证 X-Management-Key: config['cpa_remote_secret'] 2026-07 起:设备码铸造的 token 缺 referrer=grok-build,cli-chat-proxy 不可用。 默认优先走 SSO cookie 的授权码流程;仅在无 sso 且允许时才回退设备码。 """ from __future__ import annotations import os import time from pathlib import Path from typing import Any, Callable _REG_DIR = Path(__file__).resolve().parent _DEFAULT_OUT = _REG_DIR / "cpa_auths" def _resolve_out_dir(cfg: dict) -> Path: raw = str(cfg.get("cpa_auth_dir") or _DEFAULT_OUT).strip() p = Path(raw).expanduser() if not p.is_absolute(): p = (_REG_DIR / p).resolve() return p def _record_failure(out_dir: Path, email: str, reason: str) -> None: try: out_dir.mkdir(parents=True, exist_ok=True) with open(out_dir / "cpa_auth_failed.txt", "a", encoding="utf-8") as f: f.write(f"{email}----{reason}----{int(time.time())}\n") except Exception: pass def _resolve_proxy(cfg: dict) -> str | None: from oidc_mint import resolve_proxy, set_runtime_proxy # 1) 显式 mint_proxy / proxy proxy = (cfg.get("mint_proxy") or cfg.get("proxy") or "").strip() # 2) 注册机代理池线程绑定(与浏览器同出口) if not proxy: try: import proxy_pool proxy = (proxy_pool.get_thread_proxy() or "").strip() except Exception: proxy = "" # 3) 环境变量 if not proxy: proxy = ( os.environ.get("https_proxy") or os.environ.get("HTTPS_PROXY") or os.environ.get("http_proxy") or "" ).strip() resolved = resolve_proxy(proxy or None) set_runtime_proxy(resolved or None) return resolved or None def _mint_tokens( *, email: str, password: str, sso: str, page: Any | None, cfg: dict, log: Callable[[str], None], proxy: str | None, ) -> dict[str, Any]: """铸造策略(仅 HTTP;浏览器/设备码默认关): 1. 仅当 cpa_prefer_browser_mint=true 且有 page:先浏览器 PKCE 2. HTTP SSO→OAuth(curl_cffi,短超时)— 默认唯一主路径 3. 仅当 cpa_allow_browser_fallback=true 且 HTTP 失败:浏览器 PKCE 4. 可选设备码(通常无 referrer,默认关) """ from oidc_mint.oauth_code import ( OAuthCodeError, _is_http_tls_failure, mint_from_sso, mint_from_sso_browser, normalize_sso_cookie, ) sso_token = normalize_sso_cookie(sso or "") prefer_sso = bool(cfg.get("cpa_prefer_sso_oauth", True)) allow_browser = bool(cfg.get("cpa_allow_browser_fallback", False)) # 默认 False:不走浏览器优先 prefer_browser = bool(cfg.get("cpa_prefer_browser_mint", False)) allow_device = bool(cfg.get("cpa_allow_device_fallback", False)) timeout = float(cfg.get("mint_timeout_sec", 300) or 300) require_ref = bool(cfg.get("cpa_require_referrer", True)) http_err: Exception | None = None browser_timeout = min(timeout, float(cfg.get("cpa_browser_mint_timeout_sec", 90) or 90)) def _browser_mint(reason: str) -> dict[str, Any]: log(f"[cpa] 浏览器 PKCE 铸造({reason})") return mint_from_sso_browser( sso_token, page, log=lambda m: log(f"[Debug] {m}"), require_referrer=require_ref, timeout_sec=browser_timeout, ) # 路径 A:有 page 时优先浏览器(最稳,不依赖 Python→auth.x.ai 直连) if prefer_sso and sso_token and allow_browser and page is not None and prefer_browser: try: return _browser_mint("优先浏览器,绕开 Python 直连 auth.x.ai") except Exception as exc: # noqa: BLE001 log(f"[!] 浏览器 PKCE 优先路径失败: {exc}") log("[cpa] 继续尝试 HTTP SSO→OAuth") if prefer_sso and sso_token: log("[cpa] 使用 SSO→OAuth(PKCE, referrer=grok-build)") try: return mint_from_sso( sso_token, proxy=proxy, log=lambda m: log(f"[Debug] {m}"), require_referrer=require_ref, ) except OAuthCodeError as exc: http_err = exc log(f"[!] SSO→OAuth 失败: {exc}") except Exception as exc: # noqa: BLE001 http_err = exc log(f"[!] SSO→OAuth 异常: {exc}") # HTTP 失败后:有 page+sso 再试浏览器(若优先路径没走过或当时失败) if allow_browser and page is not None and sso_token and http_err is not None: why = "TLS/连接/超时" if _is_http_tls_failure(http_err) else "HTTP" try: return _browser_mint(f"{why}失败后回退") except Exception as exc: # noqa: BLE001 log(f"[!] 浏览器 PKCE 失败: {exc}") if not allow_device: raise log("[cpa] 继续回退设备码铸造(可能缺 referrer)") elif http_err is not None and not allow_device: raise http_err if not allow_device and not sso_token: raise RuntimeError("无 sso cookie,且已禁用设备码回退;无法铸造带 referrer 的 token") if not allow_device: # 有 sso 但 browser 也没开/没 page if http_err is not None: raise http_err raise RuntimeError("SSO 铸造失败,且未启用任何回退") # 设备码回退(旧路径,通常无 referrer) from oidc_mint import mint_with_browser if page is None and not (email and password): raise RuntimeError("设备码回退需要 page 或 email/password") log("[cpa] 使用设备码铸造(兼容路径)") tokens = mint_with_browser( email=email, password=password, page=page, proxy=proxy, browser_timeout_sec=timeout, force_standalone=(page is None), cookies=None, poll_log=lambda m: log(f"[Debug] {m}"), ) return tokens # ── 主入口 ── def export_cpa_for_account( email: str, password: str = "", *, page: Any | None = None, sso: str = "", config: dict | None = None, log_callback: Callable[[str], None] | None = None, ) -> dict: """铸造 OIDC → 写本地 xai-.json → 推送远端 CLIProxyAPI。 优先使用 sso cookie 走 Authorization Code + referrer=grok-build。 返回 {ok, email, path, pushed, push_status?, error?}。 """ cfg = config or {} log = log_callback or (lambda m: print(m, flush=True)) if not cfg.get("cpa_export_enabled", True): log("[cpa] 已关闭导出,跳过") return {"ok": False, "skipped": True, "reason": "disabled"} email = (email or "").strip() if not email: return {"ok": False, "error": "缺少 email", "email": email} import cpa from oidc_mint.oauth_code import normalize_sso_cookie out_dir = _resolve_out_dir(cfg) proxy = _resolve_proxy(cfg) base_url = cfg.get("cpa_base_url") or cpa.CLI_BASE_URL sso_token = normalize_sso_cookie(sso or "") try: tokens = _mint_tokens( email=email, password=password or "", sso=sso_token, page=page, cfg=cfg, log=log, proxy=proxy, ) except Exception as exc: # noqa: BLE001 log(f"[!] 铸造失败: {exc}") _record_failure(out_dir, email, str(exc)) if cfg.get("mint_required", False): raise return {"ok": False, "error": str(exc), "email": email} try: 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=base_url, sso=tokens.get("sso") or sso_token or None, ) path = cpa.write_cpa_xai_auth(out_dir, payload) filename = Path(path).name except Exception as exc: # noqa: BLE001 log(f"[!] 写本地文件失败: {exc}") _record_failure(out_dir, email, f"write: {exc}") if cfg.get("mint_required", False): raise return {"ok": False, "error": str(exc), "email": email} ref = payload.get("referrer") or tokens.get("referrer") or "" log(f"[Debug] 已写本地: {path} referrer={ref or '(empty)'}") result: dict[str, Any] = { "ok": True, "email": email, "path": str(path), "pushed": False, "referrer": ref, } # 推送远端 CLIProxyAPI(失败记入 cpa_push_pending.txt,下次推送时一并重试) if cfg.get("cpa_push_enabled", False): remote_base = str(cfg.get("cpa_remote_base") or "").strip() secret = str(cfg.get("cpa_remote_secret") or "").strip() if not remote_base or not secret: log("[!] 推送已开启但未配置 cpa_remote_base/cpa_remote_secret,跳过推送") cpa.record_push_failure(out_dir, filename, "remote not configured") else: push_proxy = str(cfg.get("cpa_push_proxy") or "").strip() or None verify_tls = bool(cfg.get("cpa_remote_verify_tls", True)) push_res = cpa.push_with_queue( out_dir, filename, payload, remote_base=remote_base, secret=secret, proxy=push_proxy, verify_tls=verify_tls, flush_first=True, log=log, ) 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"] if push_res.get("flush"): result["push_flush"] = push_res["flush"] if not result["pushed"] and cfg.get("cpa_push_required", False): result["ok"] = False result["error"] = ( f"push: {result.get('push_error') or result.get('push_status')}" ) return result