feat(compute): 算力中心 余额门控+充值+真实来源数值

- relay 匿名不再回落admin令牌(root无限额)→401或受限RELAY令牌(修无余额可用)
- /admin/compute/user-balance(充值add/subtract/override)+审计; compute_client.adjust_user_quota
- proxy follow_redirects(修gin尾斜杠301/307空数据); ping 返 version+quota_per_unit
This commit is contained in:
Pine
2026-08-26 11:59:54 +08:00
parent cbe0d03357
commit e761a089b9
4 changed files with 76 additions and 14 deletions
+31 -1
View File
@@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response
from pydantic import BaseModel
from ..dependencies import get_db
from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityCreateRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeBalanceRequest, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityCreateRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
from ...rbac import require_permission, require_roles, write_audit
from ...infrastructure.repositories import Database
from ...services import compute_client
@@ -274,6 +274,7 @@ async def compute_ping(
return ComputePingResponse(
ok=status in ("ok", "success") or "success" in status or not data.get("detail"),
version=data.get("version"),
quota_per_unit=data.get("quota_per_unit"),
detail=data.get("detail"),
)
@@ -308,6 +309,35 @@ async def compute_provision(
)
@router.post("/compute/user-balance", summary="充值/调整引擎用户余额(add_quota)")
async def compute_user_balance(
req: ComputeBalanceRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_roles("operator")),
):
"""给引擎用户充值/扣减/覆盖余额(配额单位)。
value 为配额单位:/api/status 的 quota_per_unit(默认 500000 = $1)。
modeadd 充值 / subtract 扣减 / override 直接设为该值。
引擎按用户余额计量,额度耗尽时调用 /v1 返回 403。
"""
if req.mode not in ("add", "subtract", "override"):
raise HTTPException(status_code=400, detail="mode 需为 add/subtract/override")
if req.mode != "override" and req.value <= 0:
raise HTTPException(status_code=400, detail="调整量需大于 0")
try:
result = await compute_client.adjust_user_quota(req.engine_user_id, req.value, req.mode)
except compute_client.ComputeError as exc:
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
await write_audit(db, action="compute.balance", resource="compute",
resource_id=str(req.engine_user_id),
detail=f"{req.mode} {req.value} quota" + (f" ({req.reason})" if req.reason else ""),
user=actor, request=request)
return {"ok": result.get("success", True), "engine_user_id": req.engine_user_id,
"mode": req.mode, "value": req.value, "message": result.get("message", "")}
@router.post("/compute/sync-users", summary="同步引擎用户=平台总用户(对账)")
async def sync_compute_users(
request: Request,
+17 -11
View File
@@ -22,17 +22,23 @@ from ...services import compute_catalog
router = APIRouter(prefix="/v1", tags=["relay"])
def _auth_headers(request: Request | None = None) -> dict[str, str]:
# 优先透传客户端 Authorization(用户算力 PAT → compute 归户计量)。
# 无则回落服务端消费令牌(COMPUTE_RELAY_TOKEN)→ compute 计匿名账。
if request is not None:
client_auth = request.headers.get("Authorization", "")
if client_auth:
return {"Authorization": client_auth}
token = config.COMPUTE_RELAY_TOKEN or config.COMPUTE_ADMIN_TOKEN
if not token:
return {}
return {"Authorization": f"Bearer {token}"}
def _auth_headers(request: Request) -> dict[str, str]:
"""模型调用鉴权:把请求归到「真实用户令牌」,由引擎按该用户余额计量。
- 优先透传客户端 Authorization(用户算力 PAT → compute 归户计量,引擎按其余额判定)。
- 无用户令牌:仅回落受限消费令牌 COMPUTE_RELAY_TOKEN(引擎按该受限用户余额计量)。
- 二者皆无:**拒绝匿名**——绝不回落 COMPUTE_ADMIN_TOKENroot 无限额度),
否则「用户无余额仍能免费使用」。(引擎用户额度耗尽会返回 403,见 billing_session)。
"""
client_auth = request.headers.get("Authorization", "")
if client_auth:
return {"Authorization": client_auth}
if config.COMPUTE_RELAY_TOKEN:
return {"Authorization": f"Bearer {config.COMPUTE_RELAY_TOKEN}"}
raise HTTPException(
status_code=401,
detail="缺少算力令牌:请携带有效的用户算力令牌(Bearer <PAT>)调用",
)
@router.post("/chat/completions")
+11 -1
View File
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
class TaskCreateRequest(BaseModel):
title: str
@@ -168,9 +168,19 @@ class TestStatusRequest(BaseModel):
class ComputePingResponse(BaseModel):
ok: bool
version: str | None = None
quota_per_unit: float | None = None # 引擎 1 元 = 多少配额(500000 = $1),来自 /api/status
detail: str | None = None
class ComputeBalanceRequest(BaseModel):
"""充值/调整引擎用户余额。``value`` 为配额单位(见 quota_per_unit)。"""
engine_user_id: int
value: int = Field(ge=0, description="调整量(配额单位),add/subtract 用")
mode: str = "add" # add | subtract | override
reason: str = ""
class ComputeProvisionRequest(BaseModel):
user_id: str