fix: harden online validation flows
This commit is contained in:
@@ -8,6 +8,7 @@ url 字段不接收,token站固定自家站(架构方案§二)。
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -32,7 +33,8 @@ class CreateApiKeyRequest(BaseModel):
|
||||
def provider_not_empty(cls, v: str) -> str:
|
||||
if not v or not v.strip():
|
||||
raise ValueError("provider 不能为空")
|
||||
return v.strip().lower()
|
||||
provider = v.strip().lower()
|
||||
return "apiports" if provider == "openai" else provider
|
||||
|
||||
@field_validator("api_key")
|
||||
@classmethod
|
||||
@@ -49,14 +51,47 @@ class TestApiKeyRequest(CreateApiKeyRequest):
|
||||
|
||||
def _format_key(k: UserApiKey) -> dict:
|
||||
"""只显 provider + key_last4,不暴露余额/用量/encrypted_key(红线)。"""
|
||||
provider = "apiports" if k.provider == "openai" else k.provider
|
||||
return {
|
||||
"id": k.id,
|
||||
"provider": k.provider,
|
||||
"provider": provider,
|
||||
"key_last4": k.key_last4,
|
||||
"created_at": k.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _provider_aliases(provider: str) -> list[str]:
|
||||
if provider == "apiports":
|
||||
return ["apiports", "openai"]
|
||||
return [provider]
|
||||
|
||||
|
||||
def _upstream_error_message(provider: str, exc: Exception) -> str:
|
||||
if isinstance(exc, httpx.HTTPStatusError):
|
||||
status = exc.response.status_code
|
||||
try:
|
||||
detail = exc.response.json()
|
||||
except Exception:
|
||||
detail = (exc.response.text or "")[:160]
|
||||
if status in (401, 403):
|
||||
hint = "Key 无效、权限不足或模型未开通"
|
||||
elif status == 402:
|
||||
hint = "账户余额不足或未开通计费"
|
||||
elif status == 429:
|
||||
hint = "请求过频或额度受限"
|
||||
elif status >= 500:
|
||||
hint = "中转站服务暂时不可用"
|
||||
else:
|
||||
hint = "中转站返回异常"
|
||||
logger.warning("API key test failed provider=%s status=%s detail=%s", provider, status, detail)
|
||||
return f"Key 测试失败:{provider} 返回 {status},{hint}"
|
||||
if isinstance(exc, httpx.ReadTimeout):
|
||||
return f"Key 测试失败:{provider} 在测试超时时间内无响应,请稍后重试或先保存后跑任务验证"
|
||||
if isinstance(exc, httpx.ConnectError):
|
||||
return f"Key 测试失败:无法连接 {provider} 中转站,请检查服务器网络或 Base URL"
|
||||
return f"Key 测试失败:{type(exc).__name__}"
|
||||
|
||||
|
||||
# ── 路由 ───────────────────────────────────────────────────
|
||||
@router.get("")
|
||||
def list_api_keys(
|
||||
@@ -94,11 +129,12 @@ def create_api_key(
|
||||
.filter(
|
||||
UserApiKey.user_id == current_user.user_id,
|
||||
UserApiKey.workspace_id == current_user.workspace_id,
|
||||
UserApiKey.provider == body.provider,
|
||||
UserApiKey.provider.in_(_provider_aliases(body.provider)),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
existing.provider = body.provider
|
||||
existing.encrypted_key = encrypted
|
||||
existing.key_last4 = last4
|
||||
db.commit()
|
||||
@@ -126,13 +162,12 @@ async def test_api_key(
|
||||
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
|
||||
):
|
||||
"""用客户输入的明文 key 做一次轻量连通性测试,不落库。"""
|
||||
import httpx
|
||||
from app.core.config import get_settings
|
||||
from app.core.response import raise_business
|
||||
|
||||
settings = get_settings()
|
||||
provider = body.provider
|
||||
if provider in ("openai", "apiports"):
|
||||
if provider == "apiports":
|
||||
base = (settings.APIPORTS_BASE_URL or settings.IMAGE_API_BASE).rstrip("/")
|
||||
model = settings.MODEL_TEXT
|
||||
elif provider == "codeproxy":
|
||||
@@ -144,7 +179,7 @@ async def test_api_key(
|
||||
raise_business("服务端未配置该 Provider 的 Base URL")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=12) as client:
|
||||
async with httpx.AsyncClient(timeout=45) as client:
|
||||
resp = await client.post(
|
||||
f"{base}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {body.api_key}"},
|
||||
@@ -156,7 +191,7 @@ async def test_api_key(
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except Exception as exc:
|
||||
raise_business(f"Key 测试失败:{type(exc).__name__}")
|
||||
raise_business(_upstream_error_message(provider, exc))
|
||||
|
||||
logger.info("API key test ok: user=%s provider=%s", current_user.user_id, provider)
|
||||
return ok({"ok": True, "provider": provider})
|
||||
|
||||
Reference in New Issue
Block a user