Add automatic CPA Management API import after mint
Upload xai-*.json to POST /v0/management/auth-files when cpa_auto_import_remote is enabled. Failures are logged only and never block registration.
This commit is contained in:
1 parent
d8e65ae4ef
commit
0d8265de03
5 files changed
+299
-1
No files matched your search
@@ -26,3 +26,10 @@ GROK2API_APP_KEY=
|
||||
# 代理(config.json 的 proxy / cpa_proxy 为空时会回退到这些)
|
||||
# https_proxy=http://127.0.0.1:7890
|
||||
# http_proxy=http://127.0.0.1:7890
|
||||
|
||||
# ===== CPA 远程自动导入(Management API)=====
|
||||
# 写出 xai-*.json 后 POST 到 {CPA_REMOTE_BASE}/v0/management/auth-files
|
||||
# 需同时在 config.json 设 cpa_auto_import_remote=true,或仅用环境变量时也要打开该开关
|
||||
# CPA_REMOTE_BASE=http://127.0.0.1:8317
|
||||
# CPA_REMOTE_PASSWORD=
|
||||
# 兼容别名:CPA_MANAGEMENT_PASSWORD / MANAGEMENT_PASSWORD
|
||||
@@ -36,7 +36,9 @@
|
||||
↓
|
||||
cpa_auths/xai-email.json 【注册机主导出】
|
||||
↓ (cpa_copy_to_hotload=true 时)
|
||||
CPA auth-dir 热加载 【可选】
|
||||
CPA auth-dir 热加载 【可选·本地拷贝】
|
||||
↓ (cpa_auto_import_remote=true 时)
|
||||
CPA Management API 导入 【可选·HTTP】
|
||||
↓
|
||||
CLIProxyAPI :8317 model=grok-4.5
|
||||
```
|
||||
@@ -128,6 +130,8 @@ cp config.example.json config.json
|
||||
| `GROK2API_APP_KEY` | 远端 grok2api Admin |
|
||||
| `API_REVERSE_TOOLS` | 可选:外部 `cpa_xai` 父目录 |
|
||||
| `CPA_EXPORT` | `auto_register` 是否导出 CPA(`0/false` 关) |
|
||||
| `CPA_REMOTE_BASE` | 远程 CPA 根地址(配合 `cpa_auto_import_remote`) |
|
||||
| `CPA_REMOTE_PASSWORD` | 远程 CPA 管理密码 |
|
||||
| `https_proxy` / `http_proxy` | 代理回退 |
|
||||
|
||||
模板见 `.env.example`。
|
||||
@@ -220,6 +224,48 @@ cpa_proxy > proxy > 环境变量 https_proxy/http_proxy
|
||||
| `cpa_mint_cookie_inject` | `true` | 回退时注入注册 cookie |
|
||||
| `cpa_mint_workers` | `-1` | mint 并发:`-1` 自动;`0` 内联;`1-10` 固定 |
|
||||
| `cpa_mint_required` | `false` | mint 失败是否整号失败 |
|
||||
| `cpa_auto_import_remote` | `false` | 写出后是否自动导入远程 CPA |
|
||||
| `cpa_remote_base` | `""` | CPA **根地址**,如 `http://127.0.0.1:8317` |
|
||||
| `cpa_remote_password` | `""` | CPA 管理密码(也可用环境变量) |
|
||||
| `cpa_remote_import_retries` | `3` | 远程导入重试次数 |
|
||||
| `cpa_remote_import_retry_delay` | `2` | 重试间隔(秒) |
|
||||
|
||||
### 自动导入远程 CPA(Management API)
|
||||
|
||||
写出 `cpa_auths/xai-*.json` 后,可自动调用 CLIProxyAPI 管理接口导入,无需再手动 `cp` 到 auth-dir。
|
||||
|
||||
```json
|
||||
{
|
||||
"cpa_export_enabled": true,
|
||||
"cpa_auto_import_remote": true,
|
||||
"cpa_remote_base": "http://127.0.0.1:8317",
|
||||
"cpa_remote_password": "你的管理密码"
|
||||
}
|
||||
```
|
||||
|
||||
或写在 `.env`:
|
||||
|
||||
```bash
|
||||
CPA_REMOTE_BASE=http://127.0.0.1:8317
|
||||
CPA_REMOTE_PASSWORD=你的管理密码
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- **只填根地址**(到端口即可);程序会请求
|
||||
`POST {base}/v0/management/auth-files?name=xai-email.json`
|
||||
- 认证头:`Authorization: Bearer <管理密码>`(同时附带 `X-Management-Key`)
|
||||
- 管理密码对应 CPA 的 `MANAGEMENT_PASSWORD` 或 `remote-management.secret-key` **原文**
|
||||
- 远程访问需 CPA 允许管理端(设置 `MANAGEMENT_PASSWORD` 或 `allow-remote: true`)
|
||||
- **导入失败不阻断注册**:本地文件已写出仍算成功,只打日志
|
||||
`[cpa] remote import failed: ...`
|
||||
- 成功日志:`[cpa] remote import ok -> http://host:8317 name=xai-....json`
|
||||
|
||||
等价手动调用:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "http://127.0.0.1:8317/v0/management/auth-files?name=xai-user@domain.json" -H "Authorization: Bearer $CPA_REMOTE_PASSWORD" -H "Content-Type: application/json" --data-binary @./cpa_auths/xai-user@domain.json
|
||||
```
|
||||
|
||||
### 落盘约定
|
||||
|
||||
@@ -306,6 +352,7 @@ uv run python grok_register_ttk.py
|
||||
2. 可选:推 grok2api
|
||||
3. 若 `cpa_export_enabled`:协议 mint(失败则浏览器)→ `cpa_auths/xai-<email>.json`
|
||||
4. 若 `cpa_copy_to_hotload`:再拷到 `cpa_hotload_dir`
|
||||
5. 若 `cpa_auto_import_remote`:POST 导入到 CPA Management API(失败只记日志)
|
||||
|
||||
### B. 存量号补 CPA(只 mint,不重新注册)
|
||||
|
||||
@@ -392,6 +439,7 @@ curl -sS http://127.0.0.1:8317/v1/chat/completions \
|
||||
| Hotmail 收不到码 | 检查四段凭证、ClientID/Token、IMAP 主机与 alias 计数 |
|
||||
| 有 token 但无 grok-4.5 | `cpa_base_url` 是否为 `cli-chat-proxy` |
|
||||
| 注册成功但无 `cpa_auths` | `cpa_export_enabled`?看 `cpa_auths/cpa_auth_failed.txt` |
|
||||
| 远程导入失败 | 检查 `cpa_remote_base` / 管理密码、CPA 是否开启 management、`allow-remote`;本地文件仍在 `cpa_auths/` |
|
||||
|
||||
调试原则:以 **token 端点返回 `access_token` + refresh_token** 为准;probe 看 `/v1/models` 是否含 `grok-4.5`。
|
||||
|
||||
|
||||
@@ -131,6 +131,16 @@
|
||||
"cpa_copy_to_hotload": false,
|
||||
"// cpa_hotload_dir": "CPA 容器/进程挂载的 auth-dir(copy_to_hotload=true 时生效)",
|
||||
"cpa_hotload_dir": "",
|
||||
"// cpa_auto_import_remote": "写出 xai-*.json 后是否自动 POST 到 CPA Management API 导入",
|
||||
"cpa_auto_import_remote": false,
|
||||
"// cpa_remote_base": "CPA 根地址,只填到端口即可,如 http://127.0.0.1:8317(程序自动拼 /v0/management/auth-files)",
|
||||
"cpa_remote_base": "",
|
||||
"// cpa_remote_password": "CPA 管理密码(MANAGEMENT_PASSWORD / remote-management.secret-key 原文)。也可用环境变量 CPA_REMOTE_PASSWORD",
|
||||
"cpa_remote_password": "",
|
||||
"// cpa_remote_import_retries": "远程导入失败重试次数",
|
||||
"cpa_remote_import_retries": 3,
|
||||
"// cpa_remote_import_retry_delay": "远程导入重试间隔(秒)",
|
||||
"cpa_remote_import_retry_delay": 2,
|
||||
"// cpa_base_url": "免费 Build 必须是 cli-chat-proxy,不要写成 api.x.ai",
|
||||
"cpa_base_url": "https://cli-chat-proxy.grok.com/v1",
|
||||
"// cpa_proxy": "OIDC mint 专用代理(device-code/token/probe/浏览器)。优先级: cpa_proxy > proxy > 环境 https_proxy。空=用 proxy。",
|
||||
|
||||
+213
@@ -3,6 +3,10 @@
|
||||
OIDC package lives at ./cpa_xai (bundled with this project).
|
||||
Optional override: config `api_reverse_tools` / env `API_REVERSE_TOOLS`
|
||||
points at a directory that *contains* the `cpa_xai` package.
|
||||
|
||||
After mint, optionally:
|
||||
- copy into local CPA auth-dir (`cpa_copy_to_hotload`)
|
||||
- POST into remote CPA Management API (`cpa_auto_import_remote`)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,6 +17,7 @@ import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from urllib.parse import quote, urlparse, urlunparse
|
||||
|
||||
_REG_DIR = Path(__file__).resolve().parent
|
||||
_DEFAULT_OUT = _REG_DIR / "cpa_auths"
|
||||
@@ -64,6 +69,191 @@ def export_cookies_from_page(page: Any) -> list[dict]:
|
||||
return []
|
||||
|
||||
|
||||
def _normalize_cpa_remote_base(base: str) -> str:
|
||||
"""Normalize user-provided CPA root URL to scheme://host[:port].
|
||||
|
||||
Accepts:
|
||||
http://127.0.0.1:8317
|
||||
http://host:8317/
|
||||
http://host:8317/v0/management
|
||||
http://host:8317/v0/management/auth-files
|
||||
"""
|
||||
raw = (base or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if "://" not in raw:
|
||||
raw = "http://" + raw
|
||||
parsed = urlparse(raw)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
return raw.rstrip("/")
|
||||
# Keep only scheme + netloc (drop path/query/fragment)
|
||||
return urlunparse((parsed.scheme, parsed.netloc, "", "", "", "")).rstrip("/")
|
||||
|
||||
|
||||
def resolve_cpa_remote_settings(cfg: dict | None = None) -> dict[str, Any]:
|
||||
"""Resolve remote CPA import settings from config + env."""
|
||||
cfg = cfg or {}
|
||||
enabled = bool(cfg.get("cpa_auto_import_remote", False))
|
||||
base = (
|
||||
(cfg.get("cpa_remote_base") or "").strip()
|
||||
or (os.environ.get("CPA_REMOTE_BASE") or "").strip()
|
||||
)
|
||||
password = (
|
||||
(cfg.get("cpa_remote_password") or "").strip()
|
||||
or (os.environ.get("CPA_REMOTE_PASSWORD") or "").strip()
|
||||
or (os.environ.get("CPA_MANAGEMENT_PASSWORD") or "").strip()
|
||||
or (os.environ.get("MANAGEMENT_PASSWORD") or "").strip()
|
||||
)
|
||||
retries = int(cfg.get("cpa_remote_import_retries", 3) or 3)
|
||||
delay = float(cfg.get("cpa_remote_import_retry_delay", 2) or 2)
|
||||
return {
|
||||
"enabled": enabled,
|
||||
"base": _normalize_cpa_remote_base(base),
|
||||
"password": password,
|
||||
"retries": max(1, retries),
|
||||
"delay": max(0.0, delay),
|
||||
}
|
||||
|
||||
|
||||
def import_cpa_auth_to_remote(
|
||||
path: str | Path,
|
||||
*,
|
||||
base: str,
|
||||
password: str,
|
||||
retries: int = 3,
|
||||
delay: float = 2.0,
|
||||
log: Callable[[str], None] | None = None,
|
||||
timeout: float = 12.0,
|
||||
) -> dict[str, Any]:
|
||||
"""Upload one local xai-*.json to CPA Management API.
|
||||
|
||||
POST {base}/v0/management/auth-files?name=<filename>
|
||||
Authorization: Bearer <password>
|
||||
Body: raw JSON file content
|
||||
|
||||
Failures are returned in the result dict; caller decides whether to ignore.
|
||||
"""
|
||||
_log = log or (lambda m: print(m, flush=True))
|
||||
src = Path(path)
|
||||
root = _normalize_cpa_remote_base(base)
|
||||
if not root:
|
||||
return {"ok": False, "error": "empty cpa_remote_base"}
|
||||
if not password:
|
||||
return {"ok": False, "error": "empty cpa_remote_password"}
|
||||
if not src.is_file():
|
||||
return {"ok": False, "error": f"auth file not found: {src}"}
|
||||
|
||||
name = src.name
|
||||
if not name.endswith(".json"):
|
||||
return {"ok": False, "error": f"filename must end with .json: {name}"}
|
||||
|
||||
try:
|
||||
body = src.read_bytes()
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"ok": False, "error": f"read file: {e}"}
|
||||
|
||||
url = f"{root}/v0/management/auth-files?name={quote(name)}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {password}",
|
||||
"X-Management-Key": password,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as _req # type: ignore
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"ok": False, "error": f"import curl_cffi failed: {e}"}
|
||||
|
||||
last_err = "unknown"
|
||||
attempts = max(1, int(retries or 1))
|
||||
for i in range(1, attempts + 1):
|
||||
try:
|
||||
# Admin API is local/remote management — do not use outbound proxy.
|
||||
resp = _req.post(
|
||||
url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
proxies={},
|
||||
impersonate="chrome",
|
||||
)
|
||||
status = int(getattr(resp, "status_code", 0) or 0)
|
||||
text = ""
|
||||
try:
|
||||
text = (resp.text or "")[:500]
|
||||
except Exception:
|
||||
text = ""
|
||||
if status in (200, 207):
|
||||
# Prefer JSON status field when present
|
||||
ok_status = True
|
||||
try:
|
||||
j = resp.json()
|
||||
st = str((j or {}).get("status") or "").lower()
|
||||
if st and st not in ("ok", "partial", "success"):
|
||||
ok_status = False
|
||||
last_err = f"status={st} body={text}"
|
||||
except Exception:
|
||||
pass
|
||||
if ok_status:
|
||||
_log(f"[cpa] remote import ok -> {root} name={name}")
|
||||
return {
|
||||
"ok": True,
|
||||
"base": root,
|
||||
"name": name,
|
||||
"status_code": status,
|
||||
"body": text,
|
||||
}
|
||||
else:
|
||||
last_err = f"HTTP {status}: {text or '(empty)'}"
|
||||
except Exception as e: # noqa: BLE001
|
||||
last_err = str(e)
|
||||
|
||||
if i < attempts:
|
||||
_log(f"[cpa] remote import retry {i}/{attempts}: {last_err}")
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
_log(f"[cpa] remote import failed: {last_err}")
|
||||
return {
|
||||
"ok": False,
|
||||
"base": root,
|
||||
"name": name,
|
||||
"error": last_err,
|
||||
}
|
||||
|
||||
|
||||
def maybe_import_cpa_auth_remote(
|
||||
path: str | Path,
|
||||
*,
|
||||
config: dict | None = None,
|
||||
log_callback: Callable[[str], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Config-gated remote import helper (safe no-op when disabled)."""
|
||||
cfg = config or {}
|
||||
log = log_callback or (lambda m: print(m, flush=True))
|
||||
settings = resolve_cpa_remote_settings(cfg)
|
||||
if not settings["enabled"]:
|
||||
return {"ok": False, "skipped": True, "reason": "disabled"}
|
||||
if not settings["base"] or not settings["password"]:
|
||||
log(
|
||||
"[cpa] remote import enabled but missing cpa_remote_base / "
|
||||
"cpa_remote_password (or CPA_REMOTE_BASE / CPA_REMOTE_PASSWORD)"
|
||||
)
|
||||
return {
|
||||
"ok": False,
|
||||
"skipped": True,
|
||||
"reason": "missing_base_or_password",
|
||||
}
|
||||
return import_cpa_auth_to_remote(
|
||||
path,
|
||||
base=settings["base"],
|
||||
password=settings["password"],
|
||||
retries=settings["retries"],
|
||||
delay=settings["delay"],
|
||||
log=log,
|
||||
)
|
||||
|
||||
|
||||
def export_cpa_xai_for_account(
|
||||
email: str,
|
||||
password: str,
|
||||
@@ -220,6 +410,29 @@ def export_cpa_xai_for_account(
|
||||
log(f"[cpa] hotload copy failed: {e}")
|
||||
result["cpa_copy_error"] = str(e)
|
||||
|
||||
# Optional: push auth file into remote CPA via Management API.
|
||||
# Failures never flip result["ok"] — local mint already succeeded.
|
||||
if result.get("ok") and result.get("path"):
|
||||
try:
|
||||
remote = maybe_import_cpa_auth_remote(
|
||||
result["path"],
|
||||
config=cfg,
|
||||
log_callback=log,
|
||||
)
|
||||
if remote.get("skipped"):
|
||||
result["remote_import_skipped"] = remote.get("reason") or "skipped"
|
||||
elif remote.get("ok"):
|
||||
result["remote_import_ok"] = True
|
||||
result["remote_import_base"] = remote.get("base")
|
||||
result["remote_import_name"] = remote.get("name")
|
||||
else:
|
||||
result["remote_import_ok"] = False
|
||||
result["remote_import_error"] = remote.get("error") or "unknown"
|
||||
except Exception as e: # noqa: BLE001
|
||||
log(f"[cpa] remote import unexpected error: {e}")
|
||||
result["remote_import_ok"] = False
|
||||
result["remote_import_error"] = str(e)
|
||||
|
||||
# failure log under register dir
|
||||
if not result.get("ok"):
|
||||
fail_path = out_dir / "cpa_auth_failed.txt"
|
||||
|
||||
@@ -25,6 +25,7 @@ if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
|
||||
from cpa_xai import existing_cpa_emails, mint_and_export, parse_accounts_file # noqa: E402
|
||||
from cpa_export import maybe_import_cpa_auth_remote # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -87,6 +88,7 @@ def main() -> int:
|
||||
help="Always open fresh Chromium (default)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
args._register_cfg = {}
|
||||
|
||||
if args.headless:
|
||||
args.headed = False
|
||||
@@ -108,6 +110,7 @@ def main() -> int:
|
||||
args.proxy = (cfg.get("cpa_proxy") or cfg.get("proxy") or "").strip()
|
||||
if not args.cpa_dir:
|
||||
args.cpa_dir = (cfg.get("cpa_hotload_dir") or "").strip()
|
||||
args._register_cfg = cfg
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"warn: read config proxy failed: {e}", flush=True)
|
||||
if not args.proxy:
|
||||
@@ -176,6 +179,23 @@ def main() -> int:
|
||||
shutil.copy2(src, dst)
|
||||
os.chmod(dst, 0o600)
|
||||
print(f"copied -> {dst}", flush=True)
|
||||
# optional remote Management API import (config-gated; never fails the mint)
|
||||
try:
|
||||
reg_cfg = getattr(args, "_register_cfg", None) or {}
|
||||
remote = maybe_import_cpa_auth_remote(
|
||||
r["path"],
|
||||
config=reg_cfg,
|
||||
log_callback=lambda m: print(m, flush=True),
|
||||
)
|
||||
if remote.get("ok"):
|
||||
print(
|
||||
f"remote import ok -> {remote.get('base')} name={remote.get('name')}",
|
||||
flush=True,
|
||||
)
|
||||
elif not remote.get("skipped"):
|
||||
print(f"remote import failed: {remote.get('error')}", flush=True)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"remote import unexpected error: {e}", flush=True)
|
||||
else:
|
||||
fail_n += 1
|
||||
if args.fail_log:
|
||||
|
||||
Reference in new issue
Block a user