mirror of
https://github.com/zc-zhangchen/any-auto-register.git
synced 2026-05-08 00:04:07 +08:00
306 lines
8.9 KiB
Python
306 lines
8.9 KiB
Python
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
from urllib.parse import urljoin
|
||
|
||
import requests
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
|
||
from core.config_store import config_store
|
||
|
||
router = APIRouter(prefix="/contribution", tags=["contribution"])
|
||
|
||
DEFAULT_CONTRIBUTION_SERVER_URL = "http://new.xem8k5.top:7317/"
|
||
SERVER_STATS_CANDIDATES: list[tuple[str, str]] = [
|
||
("GET", "/public/quota-stats"),
|
||
("GET", "/public/quota/stats"),
|
||
("POST", "/public/quota-stats"),
|
||
("POST", "/public/quota/stats"),
|
||
]
|
||
KEY_INFO_CANDIDATES: list[tuple[str, str]] = [
|
||
("GET", "/public/key-info"),
|
||
("GET", "/public/key/info"),
|
||
("POST", "/public/key-info"),
|
||
("POST", "/public/key/info"),
|
||
]
|
||
REDEEM_CANDIDATES: list[tuple[str, str]] = [
|
||
("POST", "/public/redeem"),
|
||
("POST", "/api/contribution/redeem"),
|
||
]
|
||
|
||
|
||
class ContributionProxyRequest(BaseModel):
|
||
server_url: str | None = None
|
||
key: str | None = None
|
||
|
||
|
||
class ContributionRedeemRequest(ContributionProxyRequest):
|
||
amount_usd: float = Field(..., gt=0)
|
||
|
||
|
||
class ContributionGenerateKeyRequest(BaseModel):
|
||
server_url: str | None = None
|
||
name: str | None = None
|
||
|
||
|
||
def _resolve_server_url(server_url: str | None) -> str:
|
||
raw = str(server_url or config_store.get("contribution_server_url", "") or "").strip()
|
||
if not raw:
|
||
raw = DEFAULT_CONTRIBUTION_SERVER_URL
|
||
if not raw.startswith(("http://", "https://")):
|
||
raw = f"http://{raw}"
|
||
return raw.rstrip("/") + "/"
|
||
|
||
|
||
def _resolve_key(key: str | None) -> str:
|
||
resolved = str(key or config_store.get("contribution_key", "") or "").strip()
|
||
if resolved:
|
||
return resolved
|
||
raise HTTPException(status_code=400, detail="请先配置贡献 key")
|
||
|
||
|
||
def _resolve_key_optional(key: str | None) -> str:
|
||
return str(key or config_store.get("contribution_key", "") or "").strip()
|
||
|
||
|
||
def _request_json(
|
||
method: str,
|
||
server_url: str,
|
||
endpoint: str,
|
||
key: str | None = None,
|
||
payload: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
headers: dict[str, str] = {}
|
||
if key:
|
||
headers["X-Public-Key"] = key
|
||
headers["Authorization"] = f"Bearer {key}"
|
||
url = urljoin(server_url, endpoint.lstrip("/"))
|
||
request_kwargs: dict[str, Any] = {
|
||
"method": method.upper(),
|
||
"url": url,
|
||
"timeout": 15,
|
||
}
|
||
if headers:
|
||
request_kwargs["headers"] = headers
|
||
if payload is not None:
|
||
request_kwargs["json"] = payload
|
||
|
||
try:
|
||
response = requests.request(**request_kwargs)
|
||
except requests.RequestException as exc:
|
||
raise HTTPException(status_code=502, detail=f"连接贡献服务器失败: {exc}") from exc
|
||
|
||
data: Any
|
||
try:
|
||
data = response.json()
|
||
except ValueError:
|
||
data = {"raw": response.text}
|
||
|
||
if response.status_code >= 400:
|
||
detail: Any = data
|
||
if isinstance(data, dict):
|
||
detail = data.get("detail") or data.get("error") or data.get("message") or data
|
||
if isinstance(detail, dict):
|
||
detail = detail.get("message") or detail.get("error") or detail.get("code") or json_dumps_safe(detail)
|
||
if not isinstance(detail, str):
|
||
detail = json_dumps_safe(detail)
|
||
raise HTTPException(status_code=response.status_code, detail=detail)
|
||
|
||
if isinstance(data, dict):
|
||
return data
|
||
return {"data": data}
|
||
|
||
|
||
def json_dumps_safe(value: Any) -> str:
|
||
try:
|
||
import json
|
||
|
||
return json.dumps(value, ensure_ascii=False)
|
||
except Exception:
|
||
return str(value)
|
||
|
||
|
||
@router.post("/quota-stats")
|
||
def get_quota_stats(body: ContributionProxyRequest):
|
||
server_url = _resolve_server_url(body.server_url)
|
||
key = _resolve_key_optional(body.key)
|
||
attempts: list[dict[str, Any]] = []
|
||
|
||
server_data: dict[str, Any] | None = None
|
||
server_endpoint = ""
|
||
server_method = ""
|
||
for method, endpoint in SERVER_STATS_CANDIDATES:
|
||
try:
|
||
server_data = _request_json(method, server_url, endpoint)
|
||
server_endpoint = endpoint
|
||
server_method = method
|
||
break
|
||
except HTTPException as exc:
|
||
attempts.append(
|
||
{
|
||
"method": method,
|
||
"endpoint": endpoint,
|
||
"status_code": exc.status_code,
|
||
"detail": exc.detail,
|
||
}
|
||
)
|
||
|
||
if server_data is None:
|
||
raise HTTPException(
|
||
status_code=502,
|
||
detail={
|
||
"message": "调用额度统计接口失败,请确认 codex2api 是否已启用该接口",
|
||
"attempts": attempts,
|
||
},
|
||
)
|
||
|
||
key_data: dict[str, Any] | None = None
|
||
key_method = ""
|
||
key_endpoint = ""
|
||
key_attempts: list[dict[str, Any]] = []
|
||
if key:
|
||
for method, endpoint in KEY_INFO_CANDIDATES:
|
||
try:
|
||
key_data = _request_json(method, server_url, endpoint, key)
|
||
key_method = method
|
||
key_endpoint = endpoint
|
||
break
|
||
except HTTPException as exc:
|
||
key_attempts.append(
|
||
{
|
||
"method": method,
|
||
"endpoint": endpoint,
|
||
"status_code": exc.status_code,
|
||
"detail": exc.detail,
|
||
}
|
||
)
|
||
|
||
response_data: dict[str, Any] = {
|
||
"server_info": server_data,
|
||
"key_info": key_data,
|
||
}
|
||
|
||
result: dict[str, Any] = {
|
||
"ok": True,
|
||
"server_method": server_method,
|
||
"server_endpoint": server_endpoint,
|
||
"data": response_data,
|
||
}
|
||
if key:
|
||
result["key_method"] = key_method
|
||
result["key_endpoint"] = key_endpoint
|
||
if key_data is None:
|
||
result["key_error"] = {
|
||
"message": "key 信息接口调用失败",
|
||
"attempts": key_attempts,
|
||
}
|
||
else:
|
||
result["key_error"] = "未配置 key,仅返回服务器额度统计"
|
||
return result
|
||
|
||
|
||
@router.post("/key-info")
|
||
def get_key_info(body: ContributionProxyRequest):
|
||
server_url = _resolve_server_url(body.server_url)
|
||
key = _resolve_key(body.key)
|
||
attempts: list[dict[str, Any]] = []
|
||
|
||
for method, endpoint in KEY_INFO_CANDIDATES:
|
||
try:
|
||
data = _request_json(method, server_url, endpoint, key)
|
||
return {
|
||
"ok": True,
|
||
"method": method,
|
||
"endpoint": endpoint,
|
||
"data": data,
|
||
}
|
||
except HTTPException as exc:
|
||
attempts.append(
|
||
{
|
||
"method": method,
|
||
"endpoint": endpoint,
|
||
"status_code": exc.status_code,
|
||
"detail": exc.detail,
|
||
}
|
||
)
|
||
|
||
raise HTTPException(
|
||
status_code=502,
|
||
detail={
|
||
"message": "调用 key 信息接口失败,请确认 codex2api 是否已启用该接口",
|
||
"attempts": attempts,
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/redeem")
|
||
def redeem(body: ContributionRedeemRequest):
|
||
server_url = _resolve_server_url(body.server_url)
|
||
key = _resolve_key(body.key)
|
||
attempts: list[dict[str, Any]] = []
|
||
data: dict[str, Any] | None = None
|
||
endpoint_hit = ""
|
||
|
||
for method, endpoint in REDEEM_CANDIDATES:
|
||
try:
|
||
data = _request_json(
|
||
method,
|
||
server_url,
|
||
endpoint,
|
||
key,
|
||
payload={"amount_usd": body.amount_usd},
|
||
)
|
||
endpoint_hit = endpoint
|
||
break
|
||
except HTTPException as exc:
|
||
attempts.append(
|
||
{
|
||
"method": method,
|
||
"endpoint": endpoint,
|
||
"status_code": exc.status_code,
|
||
"detail": exc.detail,
|
||
}
|
||
)
|
||
|
||
if data is None:
|
||
raise HTTPException(
|
||
status_code=502,
|
||
detail={
|
||
"message": "调用提现接口失败,请确认 codex2api 是否已启用 /public/redeem",
|
||
"attempts": attempts,
|
||
},
|
||
)
|
||
|
||
redeemed_amount = data.get("redeemed_amount_usd")
|
||
redeem_code = data.get("code")
|
||
return {
|
||
"ok": True,
|
||
"endpoint": endpoint_hit or "/public/redeem",
|
||
"redeemed_amount_usd": redeemed_amount,
|
||
"code": redeem_code,
|
||
"message": f"提现成功!额度:{redeemed_amount if redeemed_amount is not None else '-'} 兑换码:{redeem_code or '-'}",
|
||
"data": data,
|
||
}
|
||
|
||
|
||
@router.post("/generate-key")
|
||
def generate_key(body: ContributionGenerateKeyRequest):
|
||
server_url = _resolve_server_url(body.server_url)
|
||
payload: dict[str, Any] | None = None
|
||
name = str(body.name or "").strip()
|
||
if name:
|
||
payload = {"name": name}
|
||
|
||
data = _request_json(
|
||
"POST",
|
||
server_url,
|
||
"/public/generate",
|
||
payload=payload,
|
||
)
|
||
return {
|
||
"ok": True,
|
||
"endpoint": "/public/generate",
|
||
"data": data,
|
||
}
|