"""注册成功钩子: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 _proxy_label(proxy: str | None) -> str: try: from oidc_mint.proxyutil import proxy_log_label return proxy_log_label(proxy or "") or "(direct)" except Exception: return proxy or "(direct)" def _port_open(host: str, port: int, timeout: float = 0.25) -> bool: import socket try: with socket.create_connection((host, port), timeout=timeout): return True except OSError: return False def _local_proxy_candidates(cfg: dict) -> list[str]: """直连被 CF 拦时尝试的本机/配置代理列表(去重)。""" cands: list[str] = [] seen: set[str] = set() def _add(raw: str | None) -> None: p = (raw or "").strip() if not p or p in seen: return seen.add(p) cands.append(p) for key in ("mint_proxy", "proxy"): _add(str(cfg.get(key) or "")) raw_list = cfg.get("proxy_pool") or [] if isinstance(raw_list, str): for line in raw_list.replace(",", "\n").splitlines(): _add(line) elif isinstance(raw_list, (list, tuple)): for x in raw_list: _add(str(x)) try: import proxy_pool for p in proxy_pool.pool_snapshot()[:8]: _add(p) except Exception: pass # 仅探测本机已监听的常见代理端口,避免空转超时 for port in (7890, 7897, 10809, 10808, 7891, 20171, 6152, 1080): if _port_open("127.0.0.1", port): _add(f"http://127.0.0.1:{port}") return cands def _is_cf_mint_error(exc: BaseException | str) -> bool: try: from oidc_mint.oauth_code import is_cloudflare_block return is_cloudflare_block(exc=exc) except Exception: text = str(exc or "").lower() return "403" in text and ( "cloudflare" in text or " dict[str, Any]: """优先 SSO 授权码;可选回退设备码。""" from oidc_mint import set_runtime_proxy from oidc_mint.oauth_code import OAuthCodeError, mint_from_sso, normalize_sso_cookie sso_token = normalize_sso_cookie(sso or "") prefer_sso = bool(cfg.get("cpa_prefer_sso_oauth", True)) allow_device = bool(cfg.get("cpa_allow_device_fallback", False)) timeout = float(cfg.get("mint_timeout_sec", 300) or 300) max_proxy_tries = int(cfg.get("mint_proxy_retries", 4) or 4) if prefer_sso and sso_token: log("[cpa] 使用 SSO→OAuth(PKCE, referrer=grok-build)") tried: set[str] = set() proxies_to_try: list[str | None] = [proxy] last_exc: Exception | None = None def _expand_proxy_candidates() -> None: for p in _local_proxy_candidates(cfg): if p and p not in tried and p not in { x for x in proxies_to_try if x }: proxies_to_try.append(p) # 直连时先挂上本机已开端口,减少首次 403 后的空等 if not proxy: _expand_proxy_candidates() idx = 0 while idx < len(proxies_to_try) and idx < max(1, max_proxy_tries): use_proxy = proxies_to_try[idx] label = _proxy_label(use_proxy) if idx == 0: log(f"[cpa] mint 出口={label}") else: log(f"[cpa] CF/403 换出口重试 ({idx + 1}/{max_proxy_tries}): {label}") set_runtime_proxy(use_proxy) tried.add(use_proxy or "") try: return mint_from_sso( sso_token, proxy=use_proxy, log=lambda m: log(f"[Debug] {m}"), require_referrer=bool(cfg.get("cpa_require_referrer", True)), ) except OAuthCodeError as exc: last_exc = exc log(f"[!] SSO→OAuth 失败: {exc}") if _is_cf_mint_error(exc): if use_proxy: try: import proxy_pool proxy_pool.report_failure( use_proxy, reason=f"mint CF: {str(exc)[:120]}" ) except Exception: pass _expand_proxy_candidates() idx += 1 continue if not allow_device: raise log("[cpa] 回退设备码铸造(可能缺 referrer)") break except Exception as exc: # noqa: BLE001 last_exc = exc log(f"[!] SSO→OAuth 异常: {exc}") if _is_cf_mint_error(exc): _expand_proxy_candidates() idx += 1 continue if not allow_device: raise log("[cpa] 回退设备码铸造(可能缺 referrer)") break # 成功已 return;失败已 continue/break idx += 1 # pragma: no cover else: if last_exc is not None and not allow_device: raise last_exc if last_exc is not None and not allow_device: raise last_exc if not allow_device and not sso_token: raise RuntimeError("无 sso cookie,且已禁用设备码回退;无法铸造带 referrer 的 token") # 设备码回退(旧路径,通常无 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