Initial commit: grok-register with private config and auth artifacts.
Include local config, CPA auth files, account dumps, and temp-mail deploy helpers for the private Gitea repo.
This commit is contained in:
commit
33f966ccea
110 files changed
+6903
No files matched your search
@@ -0,0 +1,30 @@
|
||||
"""CPA (CLIProxyAPI) xai auth 组装 / 写盘 / 推送远端。"""
|
||||
|
||||
from .client import CpaPushError, push_auth_file
|
||||
from .schema import (
|
||||
API_BASE_URL,
|
||||
CLI_BASE_URL,
|
||||
CLIENT_ID,
|
||||
DEFAULT_BASE_URL,
|
||||
REDIRECT_URI,
|
||||
TOKEN_ENDPOINT,
|
||||
build_cpa_xai_auth,
|
||||
credential_file_name,
|
||||
expired_from_access_token,
|
||||
)
|
||||
from .writer import write_cpa_xai_auth
|
||||
|
||||
__all__ = [
|
||||
"API_BASE_URL",
|
||||
"CLI_BASE_URL",
|
||||
"CLIENT_ID",
|
||||
"DEFAULT_BASE_URL",
|
||||
"REDIRECT_URI",
|
||||
"TOKEN_ENDPOINT",
|
||||
"CpaPushError",
|
||||
"build_cpa_xai_auth",
|
||||
"credential_file_name",
|
||||
"expired_from_access_token",
|
||||
"push_auth_file",
|
||||
"write_cpa_xai_auth",
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
"""把 CPA xai auth JSON 推送到远端 CLIProxyAPI 导入。
|
||||
|
||||
对齐 router-for-me/CLIProxyAPI 管理 API:
|
||||
POST {base}/v0/management/auth-files?name=xai-<email>.json
|
||||
X-Management-Key: <secret-key> (管理员密钥)
|
||||
body = 原始 auth JSON
|
||||
成功 -> 200 {"status":"ok"}
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import ssl
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
UPLOAD_PATH = "/v0/management/auth-files"
|
||||
|
||||
|
||||
class CpaPushError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _opener(proxy: str | None, verify_tls: bool) -> urllib.request.OpenerDirector:
|
||||
handlers: list[Any] = []
|
||||
if proxy:
|
||||
handlers.append(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
|
||||
else:
|
||||
# 远端管理口通常在内网/直连,显式禁用系统代理,避免误走 mint 代理
|
||||
handlers.append(urllib.request.ProxyHandler({}))
|
||||
ctx = None
|
||||
if not verify_tls:
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
if ctx is not None:
|
||||
handlers.append(urllib.request.HTTPSHandler(context=ctx))
|
||||
return urllib.request.build_opener(*handlers)
|
||||
|
||||
|
||||
def push_auth_file(
|
||||
*,
|
||||
remote_base: str,
|
||||
secret: str,
|
||||
filename: str,
|
||||
payload: dict | bytes | str,
|
||||
proxy: str | None = None,
|
||||
verify_tls: bool = True,
|
||||
timeout: float = 30.0,
|
||||
) -> tuple[bool, int, str]:
|
||||
"""把一个 auth 文件推送到远端 CLIProxyAPI。返回 (ok, status, text)。"""
|
||||
base = (remote_base or "").strip().rstrip("/")
|
||||
if not base:
|
||||
raise CpaPushError("cpa_remote_base 未配置")
|
||||
if not secret:
|
||||
raise CpaPushError("cpa_remote_secret 未配置")
|
||||
if "://" not in base:
|
||||
base = "http://" + base
|
||||
if not filename.endswith(".json"):
|
||||
filename += ".json"
|
||||
|
||||
if isinstance(payload, dict):
|
||||
body = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
|
||||
elif isinstance(payload, str):
|
||||
body = payload.encode("utf-8")
|
||||
else:
|
||||
body = payload
|
||||
|
||||
url = f"{base}{UPLOAD_PATH}?name={urllib.parse.quote(filename)}"
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
method="POST",
|
||||
headers={
|
||||
"X-Management-Key": secret,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "grok-reg-cpa-push/1.0",
|
||||
},
|
||||
)
|
||||
opener = _opener(proxy, verify_tls)
|
||||
try:
|
||||
with opener.open(req, timeout=timeout) as resp:
|
||||
status = int(getattr(resp, "status", 200) or 200)
|
||||
text = resp.read().decode("utf-8", errors="replace")
|
||||
return (200 <= status < 300), status, text
|
||||
except urllib.error.HTTPError as e:
|
||||
text = e.read().decode("utf-8", errors="replace")
|
||||
return False, int(e.code), text
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise CpaPushError(str(e)) from e
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
"""CPA (CLIProxyAPI) xAI auth JSON 组装,对齐 router-for-me/CLIProxyAPI
|
||||
internal/auth/xai/token.go 的 TokenStorage 结构。
|
||||
|
||||
生成的 xai-<email>.json 可被 CLIProxyAPI 直接加载;CLIProxyAPI 请求 grok 时会
|
||||
自带 x-grok-client-version 头(xai_executor.go 硬编码 0.2.93),免费 Build 账号
|
||||
不会 426。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
# 对齐 CLIProxyAPI internal/auth/xai/types.go
|
||||
CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
|
||||
ISSUER = "https://auth.x.ai"
|
||||
TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token"
|
||||
REDIRECT_URI = "http://127.0.0.1:56121/callback"
|
||||
# 免费 Grok 4.5 (Build) 走 cli-chat-proxy;付费 API 用 https://api.x.ai/v1
|
||||
CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1"
|
||||
API_BASE_URL = "https://api.x.ai/v1"
|
||||
DEFAULT_BASE_URL = CLI_BASE_URL
|
||||
|
||||
|
||||
def _sanitize_file_segment(value: str) -> str:
|
||||
"""对齐 CPA CredentialFileName 的清洗规则。"""
|
||||
value = (value or "").strip()
|
||||
if not value:
|
||||
return ""
|
||||
out: list[str] = []
|
||||
for ch in value:
|
||||
if (
|
||||
("a" <= ch <= "z")
|
||||
or ("A" <= ch <= "Z")
|
||||
or ("0" <= ch <= "9")
|
||||
or ch in {"@", ".", "_", "-"}
|
||||
):
|
||||
out.append(ch)
|
||||
else:
|
||||
out.append("-")
|
||||
return "".join(out).strip("-")
|
||||
|
||||
|
||||
def credential_file_name(email: str = "", sub: str = "") -> str:
|
||||
"""返回 CPA 认证文件名:xai-<email>.json。"""
|
||||
email_s = _sanitize_file_segment(email)
|
||||
if email_s:
|
||||
return f"xai-{email_s}.json"
|
||||
sub_s = _sanitize_file_segment(sub)
|
||||
if sub_s:
|
||||
return f"xai-{sub_s}.json"
|
||||
ts = int(datetime.now(tz=timezone.utc).timestamp() * 1000)
|
||||
return f"xai-{ts}.json"
|
||||
|
||||
|
||||
def _jwt_payload(token: str) -> dict[str, Any]:
|
||||
parts = (token or "").split(".")
|
||||
if len(parts) < 2:
|
||||
raise ValueError("not a JWT")
|
||||
seg = parts[1]
|
||||
seg += "=" * (-len(seg) % 4)
|
||||
return json.loads(base64.urlsafe_b64decode(seg))
|
||||
|
||||
|
||||
def expired_from_access_token(access_token: str) -> tuple[str, int, str]:
|
||||
"""从 access_token 解析 (expired_rfc3339, expires_in, sub)。"""
|
||||
pl = _jwt_payload(access_token)
|
||||
exp = int(pl["exp"])
|
||||
iat = int(pl["iat"]) if pl.get("iat") is not None else exp - 21600
|
||||
expired = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
sub = str(pl.get("sub") or pl.get("principal_id") or "").strip()
|
||||
return expired, max(exp - iat, 0), sub
|
||||
|
||||
|
||||
def build_cpa_xai_auth(
|
||||
*,
|
||||
email: str,
|
||||
access_token: str,
|
||||
refresh_token: str,
|
||||
sub: str | None = None,
|
||||
id_token: str | None = None,
|
||||
expires_in: int | None = None,
|
||||
expired: str | None = None,
|
||||
last_refresh: str | None = None,
|
||||
base_url: str = DEFAULT_BASE_URL,
|
||||
token_endpoint: str = TOKEN_ENDPOINT,
|
||||
redirect_uri: str = REDIRECT_URI,
|
||||
) -> dict[str, Any]:
|
||||
"""组装一个 CLIProxyAPI 可导入的 xai auth 对象(TokenStorage 字段)。"""
|
||||
access_token = (access_token or "").strip()
|
||||
refresh_token = (refresh_token or "").strip()
|
||||
if not access_token:
|
||||
raise ValueError("access_token is required")
|
||||
if not refresh_token:
|
||||
raise ValueError("refresh_token is required (CPA 无法在缺 refresh_token 时续期)")
|
||||
|
||||
try:
|
||||
exp_s, exp_in, sub_jwt = expired_from_access_token(access_token)
|
||||
except Exception:
|
||||
exp_s, exp_in, sub_jwt = "", 21600, ""
|
||||
|
||||
if not expired:
|
||||
expired = exp_s
|
||||
if expires_in is None:
|
||||
expires_in = exp_in or 21600
|
||||
if not sub:
|
||||
sub = sub_jwt
|
||||
if not last_refresh:
|
||||
last_refresh = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
||||
if not re.search(r"/v1$", base_url) and base_url.endswith("cli-chat-proxy.grok.com"):
|
||||
base_url = base_url + "/v1"
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"type": "xai",
|
||||
"auth_kind": "oauth",
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": int(expires_in),
|
||||
"expired": expired,
|
||||
"last_refresh": last_refresh,
|
||||
"email": (email or "").strip(),
|
||||
"sub": (sub or "").strip(),
|
||||
"base_url": base_url,
|
||||
"token_endpoint": token_endpoint,
|
||||
"redirect_uri": redirect_uri,
|
||||
}
|
||||
if id_token:
|
||||
payload["id_token"] = id_token.strip()
|
||||
return payload
|
||||
@@ -0,0 +1,56 @@
|
||||
"""原子写 CPA xAI auth 文件(mode 0600)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .schema import credential_file_name
|
||||
|
||||
|
||||
def write_cpa_xai_auth(
|
||||
auth_dir: str | Path,
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
filename: str | None = None,
|
||||
) -> Path:
|
||||
"""把 payload 原子写到 auth_dir/xai-<email>.json,返回最终路径。"""
|
||||
auth_dir = Path(auth_dir).expanduser().resolve()
|
||||
auth_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not filename:
|
||||
filename = credential_file_name(
|
||||
str(payload.get("email") or ""),
|
||||
str(payload.get("sub") or ""),
|
||||
)
|
||||
if not filename.endswith(".json"):
|
||||
filename = filename + ".json"
|
||||
|
||||
dest = auth_dir / filename
|
||||
data = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
||||
|
||||
fd, tmp_name = tempfile.mkstemp(prefix=".xai-", suffix=".tmp", dir=str(auth_dir))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
try:
|
||||
os.chmod(tmp_name, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
os.replace(tmp_name, dest)
|
||||
try:
|
||||
os.chmod(dest, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
if os.path.exists(tmp_name):
|
||||
try:
|
||||
os.unlink(tmp_name)
|
||||
except OSError:
|
||||
pass
|
||||
return dest
|
||||
Reference in new issue
Block a user