Files
server-core/app/services/compute_client.py
T
Pine f067c02ec2 feat(compute): 新增 /admin/compute/proxy/{path} 透传 compute-engine 管理 API
- compute_client.proxy(): 转发到引擎 /api/<path>,保留原始状态码/响应体
- rbac_operator 新增 compute_proxy 端点(GET/POST/PUT/DELETE/PATCH),运营端鉴权
- 使 admin 端可 1:1 驱动 new-api 完整管理面(models/channels/groups/tokens/users/logs/redemption/ratio)
2026-08-25 01:52:35 +08:00

125 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""算力引擎(compute-engine / new-api)客户端封装。
云超服算力统一经此服务消费引擎(loopback :3000,仅服务),前端不直连引擎:
模型中继 ``/v1/*`` + 管理 API ``/api/*``(建号 / 发 PAT / 额度 / 流水)。
- 引擎仅作模型计量与额度/令牌/流水,不承担身份(身份在 server-core)。
- 管理调用带引擎管理令牌(``COMPUTE_ADMIN_TOKEN``);用户侧余额/流水用其 PAT。
路径:引擎管理路由起点见 compute-engine ``controller/user.go``、``controller/log.go``、
``router/api-router.go`` 的 admin 段;以下默认路径按设计 §4.3 拟定,若与引擎实际
路由不符,仅需调整对应 ``_path`` 常量,不影响上层调用。
"""
from __future__ import annotations
import asyncio
import httpx
from .. import config
class ComputeError(Exception):
"""算力引擎调用异常(网络 / 非 2xx / 引擎未就绪等)。"""
def _base() -> str:
return config.COMPUTE_BASE_URL.rstrip("/")
def _admin_headers() -> dict[str, str]:
return {"Authorization": f"Bearer {config.COMPUTE_ADMIN_TOKEN}"}
async def _request(*, method: str, path: str, headers: dict[str, str] | None = None,
json: dict | None = None) -> dict:
"""带超时/重试的引擎请求;2xx 返回 JSON,否则抛 ComputeError。"""
url = f"{_base()}{path}"
last: Exception | None = None
for _ in range(max(1, config.COMPUTE_RETRIES + 1)):
try:
async with httpx.AsyncClient(timeout=config.COMPUTE_TIMEOUT) as client:
resp = await client.request(
method, url, json=json, headers=headers,
)
if resp.status_code == 200:
try:
return resp.json() if resp.content else {}
except Exception: # noqa: BLE001
return {}
if resp.status_code < 500:
raise ComputeError(
f"compute-engine {resp.status_code}: {resp.text[:200]}"
)
last = ComputeError(f"compute-engine {resp.status_code}: {resp.text[:200]}")
except httpx.HTTPError as exc:
last = ComputeError(f"compute-engine request failed: {exc}")
except ComputeError as exc:
last = exc
if " 4" in str(exc) or " 3" in str(exc):
break
await asyncio.sleep(0.2)
raise last or ComputeError("compute-engine unavailable")
async def ping() -> dict:
"""引擎连通性健康检查。"""
try:
return await _request(method="GET", path="/api/status", headers=_admin_headers())
except ComputeError as exc:
return {"status": "error", "detail": str(exc)}
async def create_user(username: str) -> dict:
"""在引擎建号(username = 云超服 uid)。"""
return await _request(
method="POST", path="/api/user", headers=_admin_headers(),
json={"username": username},
)
async def issue_pat(username: str) -> str:
"""为引擎用户签发 personal access token(下发前端注入 provider)。"""
data = await _request(
method="POST", path="/api/token", headers=_admin_headers(),
json={"username": username},
)
return str(data.get("key") or data.get("token") or "")
async def get_balance(user_pat: str) -> dict:
"""用户余额(需该用户 PAT)。"""
headers = {"Authorization": f"Bearer {user_pat}"}
return await _request(method="GET", path="/api/user/self", headers=headers)
async def get_month_usage(user_pat: str) -> dict:
"""本月消耗(按日额度)。"""
headers = {"Authorization": f"Bearer {user_pat}"}
return await _request(method="GET", path="/api/data/self", headers=headers)
async def get_logs(user_pat: str) -> dict:
"""用户 token 流水。"""
headers = {"Authorization": f"Bearer {user_pat}"}
return await _request(method="GET", path="/api/log/self", headers=headers)
async def proxy(method: str, path: str, *, json_body: dict | None = None, params: dict | None = None) -> dict:
"""管理 API 透传:转发到引擎 ``/api<path>``,返回 ``{status, body}``。
供 /admin/compute/proxy/** 使用,让 admin 端可 1:1 驱动 compute-engine(new-api) 的完整管理面
models/channels/groups/tokens/users/logs/redemption/ratio 等),不改写每个端点契约。
保留引擎原始状态码与响应体(网关层不吞错误)。
"""
url = f"{_base()}/api{path}"
try:
async with httpx.AsyncClient(timeout=config.COMPUTE_TIMEOUT) as client:
resp = await client.request(
method, url, json=json_body, params=params, headers=_admin_headers(),
)
return {"status": resp.status_code, "body": resp.text or "{}"}
except httpx.HTTPError as exc:
return {"status": 502, "body": '{"error": "%s"}' % str(exc)}