Prefer Cloudflare Worker KV for verification codes with optional short IMAP fallback disabled for private deploy; track config.json/.env for private repo. Also add worker project, protocol mint backoff, and fail-fast Turnstile/mail timeouts from batch runs.
232 lines
8.7 KiB
Python
232 lines
8.7 KiB
Python
"""High-level: mint CPA xai-*.json for one free registered account."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
from .browser_confirm import mint_with_browser
|
|
from .probe import probe_mini_response, probe_models
|
|
from .protocol_mint import ProtocolMintError, extract_sso_from_cookies, mint_with_sso_protocol
|
|
from .proxyutil import proxy_log_label, resolve_proxy, set_runtime_proxy
|
|
from .schema import DEFAULT_BASE_URL, build_cpa_xai_auth
|
|
from .writer import write_cpa_xai_auth
|
|
|
|
LogFn = Callable[[str], None]
|
|
|
|
# Serialize/spacing protocol mints to reduce accounts.x.ai rate_limited.
|
|
_PROTOCOL_MINT_LOCK = threading.Lock()
|
|
_PROTOCOL_LAST_OK_AT = 0.0
|
|
_PROTOCOL_MIN_INTERVAL_SEC = 1.5
|
|
|
|
|
|
def _noop(_: str) -> None:
|
|
return None
|
|
|
|
|
|
def mint_and_export(
|
|
*,
|
|
email: str,
|
|
password: str,
|
|
auth_dir: str | Path,
|
|
page: Any | None = None,
|
|
proxy: str | None = None,
|
|
headless: bool = False,
|
|
base_url: str = DEFAULT_BASE_URL,
|
|
probe: bool = True,
|
|
probe_chat: bool = False,
|
|
browser_timeout_sec: float = 240.0,
|
|
force_standalone: bool = True,
|
|
cookies: Any | None = None,
|
|
sso: str | None = None,
|
|
reuse_browser: bool = True,
|
|
recycle_every: int = 15,
|
|
prefer_protocol: bool = True,
|
|
protocol_only: bool = False,
|
|
protocol_poll_timeout_sec: float = 90.0,
|
|
log: LogFn | None = None,
|
|
cancel: Callable[[], bool] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Full pipeline: (protocol SSO device-flow |) browser device-auth → write CPA → probe.
|
|
|
|
Protocol path (curl_cffi + sso cookie) is tried first when prefer_protocol
|
|
and an sso cookie is available. On failure, falls back to browser mint unless
|
|
protocol_only=True.
|
|
|
|
Returns dict with keys: ok, path, email, probe, error?, mint_method?
|
|
"""
|
|
log = log or _noop
|
|
global _PROTOCOL_LAST_OK_AT
|
|
email = (email or "").strip()
|
|
if not email or not password:
|
|
# Protocol can work with sso alone; password only required for browser fallback
|
|
if not email:
|
|
return {"ok": False, "email": email, "error": "missing email"}
|
|
if not (sso or extract_sso_from_cookies(cookies)):
|
|
return {"ok": False, "email": email, "error": "missing email/password"}
|
|
|
|
# Config/explicit proxy wins over shell https_proxy (common 7890 trap).
|
|
# Thread-local pin — safe under concurrent mint workers.
|
|
resolved = resolve_proxy(proxy)
|
|
set_runtime_proxy(resolved or None)
|
|
log(f"mint start: {email} proxy={proxy_log_label(resolved) or '(none)'}")
|
|
|
|
sso_val = (sso or "").strip() or extract_sso_from_cookies(cookies)
|
|
tokens: dict[str, Any] | None = None
|
|
protocol_err: str | None = None
|
|
|
|
if prefer_protocol and sso_val:
|
|
log("mint try protocol (SSO HTTP device flow)")
|
|
max_protocol_attempts = 3
|
|
for attempt in range(1, max_protocol_attempts + 1):
|
|
if cancel and cancel():
|
|
return {"ok": False, "email": email, "error": "cancelled", "mint_method": "protocol"}
|
|
# Throttle protocol calls across mint workers
|
|
with _PROTOCOL_MINT_LOCK:
|
|
gap = time.time() - _PROTOCOL_LAST_OK_AT
|
|
if _PROTOCOL_LAST_OK_AT > 0 and gap < _PROTOCOL_MIN_INTERVAL_SEC:
|
|
wait = _PROTOCOL_MIN_INTERVAL_SEC - gap
|
|
log(f"protocol throttle sleep {wait:.1f}s")
|
|
time.sleep(wait)
|
|
try:
|
|
tokens = mint_with_sso_protocol(
|
|
sso_cookie=sso_val,
|
|
email=email,
|
|
proxy=resolved or None,
|
|
poll_timeout_sec=protocol_poll_timeout_sec,
|
|
log=log,
|
|
cancel=cancel,
|
|
)
|
|
with _PROTOCOL_MINT_LOCK:
|
|
_PROTOCOL_LAST_OK_AT = time.time()
|
|
log("mint protocol SUCCESS")
|
|
protocol_err = None
|
|
break
|
|
except ProtocolMintError as e:
|
|
protocol_err = str(e)
|
|
log(f"mint protocol failed (try {attempt}/{max_protocol_attempts}): {e}")
|
|
rate_limited = "rate_limited" in protocol_err.lower()
|
|
if rate_limited and attempt < max_protocol_attempts:
|
|
backoff = 5.0 * attempt
|
|
log(f"protocol rate_limited → backoff {backoff:.0f}s then retry")
|
|
time.sleep(backoff)
|
|
continue
|
|
if protocol_only:
|
|
return {
|
|
"ok": False,
|
|
"email": email,
|
|
"error": f"protocol_only: {e}",
|
|
"mint_method": "protocol",
|
|
}
|
|
log("mint fallback → browser")
|
|
break
|
|
except Exception as e: # noqa: BLE001
|
|
protocol_err = str(e)
|
|
log(f"mint protocol exception (try {attempt}/{max_protocol_attempts}): {e}")
|
|
if protocol_only:
|
|
return {
|
|
"ok": False,
|
|
"email": email,
|
|
"error": f"protocol_only: {e}",
|
|
"mint_method": "protocol",
|
|
}
|
|
log("mint fallback → browser")
|
|
break
|
|
elif prefer_protocol and not sso_val:
|
|
log("mint protocol skipped (no sso cookie) → browser")
|
|
if protocol_only:
|
|
return {
|
|
"ok": False,
|
|
"email": email,
|
|
"error": "protocol_only but no sso cookie",
|
|
"mint_method": "protocol",
|
|
}
|
|
elif not prefer_protocol:
|
|
log("mint protocol disabled → browser")
|
|
|
|
if tokens is None:
|
|
if not password:
|
|
return {
|
|
"ok": False,
|
|
"email": email,
|
|
"error": protocol_err or "protocol failed and no password for browser fallback",
|
|
"protocol_error": protocol_err,
|
|
}
|
|
try:
|
|
tokens = mint_with_browser(
|
|
email=email,
|
|
password=password,
|
|
page=None if force_standalone else page,
|
|
proxy=resolved or None,
|
|
headless=headless,
|
|
browser_timeout_sec=browser_timeout_sec,
|
|
force_standalone=force_standalone,
|
|
cookies=cookies,
|
|
reuse_browser=reuse_browser,
|
|
recycle_every=recycle_every,
|
|
poll_log=log,
|
|
cancel=cancel,
|
|
)
|
|
tokens["mint_method"] = "browser"
|
|
if protocol_err:
|
|
tokens["protocol_error"] = protocol_err
|
|
except Exception as e: # noqa: BLE001
|
|
log(f"mint failed: {e}")
|
|
err = str(e)
|
|
if protocol_err:
|
|
err = f"{err} (protocol: {protocol_err})"
|
|
return {
|
|
"ok": False,
|
|
"email": email,
|
|
"error": err,
|
|
"protocol_error": protocol_err,
|
|
}
|
|
|
|
payload = 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,
|
|
)
|
|
path = write_cpa_xai_auth(auth_dir, payload)
|
|
log(f"wrote {path}")
|
|
|
|
result: dict[str, Any] = {
|
|
"ok": True,
|
|
"email": email,
|
|
"path": str(path),
|
|
"user_code": tokens.get("user_code"),
|
|
"base_url": base_url,
|
|
"proxy": proxy_log_label(resolved),
|
|
"mint_method": tokens.get("mint_method") or "browser",
|
|
}
|
|
if protocol_err and result["mint_method"] != "protocol":
|
|
result["protocol_error"] = protocol_err
|
|
|
|
if probe:
|
|
pr = probe_models(tokens["access_token"], base_url=base_url, proxy=resolved or None)
|
|
result["probe_models"] = pr
|
|
log(
|
|
f"probe models: ok={pr.get('ok')} status={pr.get('status')} "
|
|
f"has_grok_45={pr.get('has_grok_45')} ids={pr.get('model_ids')} "
|
|
f"error={str(pr.get('error') or '')[:200]}"
|
|
)
|
|
if not pr.get("has_grok_45"):
|
|
result["ok"] = False
|
|
result["error"] = "token ok but grok-4.5 not listed"
|
|
if probe_chat and pr.get("has_grok_45"):
|
|
ch = probe_mini_response(
|
|
tokens["access_token"], base_url=base_url, proxy=resolved or None
|
|
)
|
|
result["probe_chat"] = ch
|
|
log(f"probe chat: ok={ch.get('ok')} model={ch.get('model')} text={ch.get('text')!r}")
|
|
if not ch.get("ok"):
|
|
result["ok"] = False
|
|
result["error"] = f"chat probe failed: {ch.get('error') or ch.get('status')}"
|
|
return result
|