Files
grok-keygen-new/cpa_export.py
T
chaos d74d14d3f4 Disable browser PKCE mint by default.
User does not want web/browser casting; HTTP-only unless
cpa_allow_browser_fallback is explicitly re-enabled.
2026-07-14 13:48:53 +08:00

291 lines
10 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.
"""注册成功钩子:SSO → OAuth Authorization Code(PKCE, referrer=grok-build)
→ 写出 CPA (CLIProxyAPI) 的 xai-<email>.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-<email>.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