refactor: modularize registration runtime safely

This commit is contained in:
github-actions[bot]
2026-07-14 18:10:13 +00:00
parent df525d0962
commit b845be012a
23 changed files with 3945 additions and 8518 deletions
+270
View File
@@ -2,6 +2,7 @@
import json
import os
import tempfile
import time
from datetime import datetime, timezone
from filelock import FileLock
@@ -123,3 +124,272 @@ def retry_pending_file(pending_path, output_path=None, log_callback=None):
if temp_path and os.path.exists(temp_path):
os.unlink(temp_path)
return {"restored": restored, "remaining": len(unresolved), "output_path": target_path}
# Token-pool runtime dependencies are injected by the application adapter.
config = {}
_http_get = None
_http_post = None
_log_exception = None
_remote_compat_error = RuntimeError
_remote_request_error = RuntimeError
def configure_token_runtime(config_ref, http_get, http_post, log_exception,
compatibility_error=RuntimeError, request_error=RuntimeError):
global config, _http_get, _http_post, _log_exception
global _remote_compat_error, _remote_request_error
config = config_ref
_http_get = http_get
_http_post = http_post
_log_exception = log_exception
_remote_compat_error = compatibility_error
_remote_request_error = request_error
globals()["http_get"] = http_get
globals()["http_post"] = http_post
globals()["log_exception"] = log_exception
globals()["RemoteTokenCompatibilityError"] = compatibility_error
globals()["RemoteTokenRequestError"] = request_error
def resolve_grok2api_local_token_file():
configured = str(config.get("grok2api_local_token_file", "") or "").strip()
if configured:
return configured
return os.path.join(os.path.dirname(__file__), "token.json")
def _normalize_sso_token(raw_token):
token = str(raw_token or "").strip()
if token.startswith("sso="):
token = token[4:]
return token
def add_token_to_grok2api_local_pool(raw_token, email="", log_callback=None):
token = _normalize_sso_token(raw_token)
if not token:
return False
token_file = os.path.abspath(resolve_grok2api_local_token_file())
pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip() or "ssoBasic"
parent = os.path.dirname(token_file)
os.makedirs(parent, exist_ok=True)
lock_path = token_file + ".lock"
try:
with open(lock_path, "a", encoding="utf-8"):
pass
os.chmod(lock_path, 0o600)
except Exception:
pass
try:
from filelock import FileLock
except Exception as exc:
raise RuntimeError(f"filelock 依赖不可用,拒绝非原子写入 token 池: {exc}")
with FileLock(lock_path, timeout=30):
data = {}
if os.path.exists(token_file):
try:
with open(token_file, "r", encoding="utf-8") as f:
data = json.load(f) or {}
except Exception as exc:
broken_path = token_file + f".broken-{int(time.time())}"
try:
os.replace(token_file, broken_path)
except Exception:
broken_path = token_file
raise RuntimeError(f"本地 token 文件 JSON 解析失败,已停止写入以避免覆盖: {broken_path}: {exc}")
if not isinstance(data, dict):
raise RuntimeError("本地 token 文件根节点不是 JSON object,拒绝覆盖")
pool = data.get(pool_name)
if pool is None:
pool = []
elif not isinstance(pool, list):
raise RuntimeError(f"本地 token 池 {pool_name} 不是列表,拒绝覆盖")
existing = set()
for item in pool:
if isinstance(item, str):
existing.add(_normalize_sso_token(item))
elif isinstance(item, dict):
existing.add(_normalize_sso_token(item.get("token", "")))
if token in existing:
if log_callback:
log_callback(f"[*] grok2api 本地池已存在 token: {pool_name}")
return True
pool.append({"token": token, "tags": ["auto-register"], "note": email})
data[pool_name] = pool
if os.path.exists(token_file):
backup_path = token_file + ".bak"
try:
with open(token_file, "rb") as src, open(backup_path, "wb") as dst:
dst.write(src.read())
dst.flush()
os.fsync(dst.fileno())
try:
os.chmod(backup_path, 0o600)
except Exception:
pass
except Exception as exc:
raise RuntimeError(f"创建本地 token 备份失败,拒绝继续写入: {exc}")
fd, temp_path = tempfile.mkstemp(prefix=".token-", suffix=".tmp", dir=parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
f.flush()
os.fsync(f.fileno())
try:
os.chmod(temp_path, 0o600)
except Exception:
pass
os.replace(temp_path, token_file)
temp_path = None
try:
os.chmod(token_file, 0o600)
except Exception:
pass
finally:
if temp_path and os.path.exists(temp_path):
try:
os.unlink(temp_path)
except Exception:
pass
if log_callback:
log_callback(f"[+] 已写入 grok2api 本地池: {pool_name} ({token_file})")
return True
def get_grok2api_remote_api_bases(base):
"""生成 grok2api 管理 API 候选根路径。
参数:
- base str: 用户配置的 grok2api 远端地址
返回:
- list[str]: 依次尝试的管理 API 根路径
"""
normalized = str(base or "").strip().rstrip("/")
if not normalized:
return []
lower = normalized.lower()
candidates = [normalized]
if lower.endswith("/admin/api"):
return candidates
if lower.endswith("/admin"):
candidates.append(f"{normalized}/api")
else:
candidates.append(f"{normalized}/admin/api")
seen = set()
unique = []
for item in candidates:
if item not in seen:
unique.append(item)
seen.add(item)
return unique
def add_token_to_grok2api_remote_pool(raw_token, email="", log_callback=None):
token = _normalize_sso_token(raw_token)
if not token:
return False
base = str(config.get("grok2api_remote_base", "") or "").strip().rstrip("/")
app_key = str(config.get("grok2api_remote_app_key", "") or "").strip()
pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip()
if not base or not app_key:
raise RemoteTokenRequestError("grok2api 远端未配置 base/app_key")
headers = {"Content-Type": "application/json"}
query = {"app_key": app_key}
remote_pool = {"ssoBasic": "basic", "ssoSuper": "super"}[pool_name]
api_bases = get_grok2api_remote_api_bases(base)
incompatible = []
add_payload = {"tokens": [token], "pool": remote_pool, "tags": ["auto-register"]}
for api_base in api_bases:
endpoint = f"{api_base}/tokens/add"
try:
response = http_post(endpoint, headers=headers, params=query, json=add_payload, timeout=30)
except Exception as exc:
raise RemoteTokenRequestError(f"远端 /tokens/add 网络请求失败: {endpoint}: {exc}") from exc
status = int(getattr(response, "status_code", 0) or 0)
if 200 <= status < 300:
if log_callback:
log_callback(f"[+] 已写入 grok2api 远端池: {pool_name} ({endpoint})")
return True
if status in (404, 405):
incompatible.append(f"{endpoint}: HTTP {status}")
continue
body = str(getattr(response, "text", "") or "")[:300]
raise RemoteTokenRequestError(f"远端 /tokens/add 请求失败,不允许全量回退: {endpoint}: HTTP {status}: {body}")
if not bool(config.get("grok2api_allow_legacy_full_save", False)):
raise RemoteTokenCompatibilityError(
"/tokens/add 不受支持,旧版全量保存默认禁用以避免并发覆盖: " + "; ".join(incompatible)
)
current = None
fallback_base = None
etag = None
load_errors = []
for api_base in api_bases or [base]:
endpoint = f"{api_base}/tokens"
try:
response = http_get(endpoint, headers=headers, params=query, timeout=20)
except Exception as exc:
raise RemoteTokenRequestError(f"旧版远端池读取网络失败: {endpoint}: {exc}") from exc
status = int(getattr(response, "status_code", 0) or 0)
if status != 200:
load_errors.append(f"{endpoint}: HTTP {status}")
continue
payload = response.json()
candidate = payload.get("tokens") if isinstance(payload, dict) and "tokens" in payload else payload
if not isinstance(candidate, dict):
load_errors.append(f"{endpoint}: unexpected payload")
continue
current = candidate
fallback_base = api_base
response_headers = getattr(response, "headers", {}) or {}
etag = response_headers.get("ETag") or response_headers.get("etag")
break
if current is None or fallback_base is None:
raise RemoteTokenRequestError("无法安全读取旧版远端 token 池: " + "; ".join(load_errors))
pool = current.get(pool_name)
if pool is None:
pool = []
elif not isinstance(pool, list):
raise RemoteTokenRequestError(f"远端 token 池 {pool_name} 不是列表,拒绝全量覆盖")
existing = {
_normalize_sso_token(item if isinstance(item, str) else item.get("token", ""))
for item in pool if isinstance(item, (str, dict))
}
if token not in existing:
pool.append({"token": token, "tags": ["auto-register"], "note": email})
current[pool_name] = pool
if not etag:
raise RemoteTokenCompatibilityError(
"旧版远端接口未提供 ETag,无法保证并发安全,已拒绝全量保存"
)
save_headers = dict(headers)
save_headers["If-Match"] = etag
endpoint = f"{fallback_base}/tokens"
try:
response = http_post(endpoint, headers=save_headers, params=query, json=current, timeout=30)
except Exception as exc:
raise RemoteTokenRequestError(f"旧版远端池保存网络失败: {endpoint}: {exc}") from exc
status = int(getattr(response, "status_code", 0) or 0)
if not 200 <= status < 300:
raise RemoteTokenRequestError(f"旧版远端池保存失败: {endpoint}: HTTP {status}")
if log_callback:
log_callback(f"[+] 已写入 grok2api 远端池(旧版兼容): {pool_name} ({endpoint})")
return True
def add_token_to_grok2api_pools(raw_token, email="", log_callback=None):
result = {
"local": {"enabled": bool(config.get("grok2api_auto_add_local", False)), "ok": None, "error": None},
"remote": {"enabled": bool(config.get("grok2api_auto_add_remote", False)), "ok": None, "error": None},
}
if result["local"]["enabled"]:
try:
result["local"]["ok"] = bool(add_token_to_grok2api_local_pool(raw_token, email=email, log_callback=log_callback))
except Exception as exc:
result["local"]["ok"] = False
result["local"]["error"] = log_exception("写入 grok2api 本地池失败", exc, log_callback)
if result["remote"]["enabled"]:
try:
result["remote"]["ok"] = bool(add_token_to_grok2api_remote_pool(raw_token, email=email, log_callback=log_callback))
except Exception as exc:
result["remote"]["ok"] = False
result["remote"]["error"] = log_exception("写入 grok2api 远端池失败", exc, log_callback)
return result