2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
API端点请求/响应模型定义
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import re
|
|
|
|
|
|
from datetime import datetime
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from typing import Any, Literal
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-04 11:18:28 +08:00
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
from ..core.enums import UserRole
|
2026-03-12 01:11:35 +08:00
|
|
|
|
from ..core.validators import PasswordValidator
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 认证相关 ==========
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
|
|
|
|
"""登录请求"""
|
|
|
|
|
|
|
2026-01-04 11:18:28 +08:00
|
|
|
|
email: str = Field(..., min_length=1, max_length=255, description="邮箱/用户名")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
password: str = Field(..., description="密码")
|
2026-01-02 16:17:24 +08:00
|
|
|
|
auth_type: Literal["local", "ldap"] = Field(default="local", description="认证类型")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
@field_validator("password")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_password(cls, v: Any) -> Any:
|
2026-03-17 16:34:09 +08:00
|
|
|
|
"""验证密码输入合法,保留原始内容。"""
|
|
|
|
|
|
valid, error_msg = PasswordValidator.validate_login_input(v)
|
|
|
|
|
|
if not valid:
|
|
|
|
|
|
raise ValueError(error_msg or "密码格式无效")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return v
|
|
|
|
|
|
|
2026-01-04 11:18:28 +08:00
|
|
|
|
@model_validator(mode="after")
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_login(self) -> Any:
|
2026-01-04 11:18:28 +08:00
|
|
|
|
"""根据认证类型校验并规范化登录标识"""
|
|
|
|
|
|
identifier = self.email.strip()
|
|
|
|
|
|
|
2026-01-06 14:38:42 +08:00
|
|
|
|
if not identifier:
|
|
|
|
|
|
raise ValueError("用户名/邮箱不能为空")
|
|
|
|
|
|
|
|
|
|
|
|
# 本地和 LDAP 登录都支持用户名或邮箱
|
|
|
|
|
|
# 如果是邮箱格式,转换为小写
|
|
|
|
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
if re.match(email_pattern, identifier):
|
2026-01-04 11:18:28 +08:00
|
|
|
|
self.email = identifier.lower()
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.email = identifier
|
|
|
|
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
|
|
|
|
"""登录响应"""
|
|
|
|
|
|
|
|
|
|
|
|
access_token: str
|
|
|
|
|
|
token_type: str = "bearer"
|
|
|
|
|
|
expires_in: int = 86400 # Token有效期(秒),默认24小时
|
|
|
|
|
|
user_id: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
username: str
|
|
|
|
|
|
role: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RefreshTokenResponse(BaseModel):
|
|
|
|
|
|
"""刷新令牌响应"""
|
|
|
|
|
|
|
|
|
|
|
|
access_token: str
|
|
|
|
|
|
token_type: str = "bearer"
|
|
|
|
|
|
expires_in: int = 86400 # Token有效期(秒),默认24小时
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
|
|
|
|
"""注册请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = Field(None, max_length=255, description="邮箱地址(可选)")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
|
|
|
|
|
password: str = Field(..., description="密码")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
@field_validator("email")
|
2026-01-19 03:19:17 +08:00
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_email(cls, v: Any) -> Any:
|
2026-01-19 03:19:17 +08:00
|
|
|
|
"""验证邮箱格式(如果提供)"""
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
v = v.strip()
|
|
|
|
|
|
if not v:
|
|
|
|
|
|
return None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
if not re.match(email_pattern, v):
|
|
|
|
|
|
raise ValueError("邮箱格式无效")
|
|
|
|
|
|
return v.lower()
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
@field_validator("username")
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_username(cls, v: Any) -> Any:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""验证用户名格式"""
|
|
|
|
|
|
v = v.strip()
|
|
|
|
|
|
if not v:
|
|
|
|
|
|
raise ValueError("用户名不能为空")
|
2026-01-27 12:17:19 +08:00
|
|
|
|
if not re.match(r"^[a-zA-Z0-9_.\-]+$", v):
|
|
|
|
|
|
raise ValueError("用户名只能包含字母、数字、下划线、连字符和点号")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("password")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_password(cls, v: Any) -> Any:
|
2026-03-17 16:34:09 +08:00
|
|
|
|
"""基础校验(非空和算法限制),策略级别校验在服务层按系统配置执行。"""
|
|
|
|
|
|
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
2026-03-12 01:11:35 +08:00
|
|
|
|
if not valid:
|
|
|
|
|
|
raise ValueError(error_msg or "密码格式无效")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RegisterResponse(BaseModel):
|
|
|
|
|
|
"""注册响应"""
|
|
|
|
|
|
|
|
|
|
|
|
user_id: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
username: str
|
|
|
|
|
|
message: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogoutResponse(BaseModel):
|
|
|
|
|
|
"""登出响应"""
|
|
|
|
|
|
|
|
|
|
|
|
message: str
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
|
class SendVerificationCodeRequest(BaseModel):
|
|
|
|
|
|
"""发送验证码请求"""
|
|
|
|
|
|
|
|
|
|
|
|
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("email")
|
|
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_email(cls, v: Any) -> Any:
|
2025-12-30 17:15:48 +08:00
|
|
|
|
"""验证邮箱格式"""
|
|
|
|
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
if not re.match(email_pattern, v):
|
|
|
|
|
|
raise ValueError("邮箱格式无效")
|
|
|
|
|
|
return v.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendVerificationCodeResponse(BaseModel):
|
|
|
|
|
|
"""发送验证码响应"""
|
|
|
|
|
|
|
|
|
|
|
|
message: str
|
|
|
|
|
|
success: bool
|
2026-01-30 03:10:21 +08:00
|
|
|
|
expire_minutes: int | None = None
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VerifyEmailRequest(BaseModel):
|
|
|
|
|
|
"""验证邮箱请求"""
|
|
|
|
|
|
|
|
|
|
|
|
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
|
|
|
|
|
code: str = Field(..., min_length=6, max_length=6, description="6位验证码")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("email")
|
|
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_email(cls, v: Any) -> Any:
|
2025-12-30 17:15:48 +08:00
|
|
|
|
"""验证邮箱格式"""
|
|
|
|
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
if not re.match(email_pattern, v):
|
|
|
|
|
|
raise ValueError("邮箱格式无效")
|
|
|
|
|
|
return v.lower()
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("code")
|
2026-01-01 02:10:19 +08:00
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_code(cls, v: Any) -> Any:
|
2025-12-30 17:15:48 +08:00
|
|
|
|
"""验证验证码格式"""
|
|
|
|
|
|
v = v.strip()
|
|
|
|
|
|
if not v.isdigit():
|
|
|
|
|
|
raise ValueError("验证码必须是6位数字")
|
|
|
|
|
|
if len(v) != 6:
|
|
|
|
|
|
raise ValueError("验证码必须是6位数字")
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VerifyEmailResponse(BaseModel):
|
|
|
|
|
|
"""验证邮箱响应"""
|
|
|
|
|
|
|
|
|
|
|
|
message: str
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-01 02:10:19 +08:00
|
|
|
|
class VerificationStatusRequest(BaseModel):
|
|
|
|
|
|
"""验证状态查询请求"""
|
|
|
|
|
|
|
|
|
|
|
|
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("email")
|
|
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_email(cls, v: Any) -> Any:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
"""验证邮箱格式"""
|
|
|
|
|
|
v = v.strip().lower()
|
|
|
|
|
|
if not v:
|
|
|
|
|
|
raise ValueError("邮箱不能为空")
|
|
|
|
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
if not re.match(email_pattern, v):
|
|
|
|
|
|
raise ValueError("邮箱格式无效")
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VerificationStatusResponse(BaseModel):
|
|
|
|
|
|
"""验证状态响应"""
|
|
|
|
|
|
|
|
|
|
|
|
email: str
|
|
|
|
|
|
has_pending_code: bool = Field(description="是否有待验证的验证码")
|
|
|
|
|
|
is_verified: bool = Field(description="邮箱是否已验证")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
cooldown_remaining: int | None = Field(None, description="发送冷却剩余秒数")
|
|
|
|
|
|
code_expires_in: int | None = Field(None, description="验证码剩余有效秒数")
|
2026-01-01 02:10:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
|
class RegistrationSettingsResponse(BaseModel):
|
|
|
|
|
|
"""注册设置响应(公开接口返回)"""
|
|
|
|
|
|
|
|
|
|
|
|
enable_registration: bool
|
|
|
|
|
|
require_email_verification: bool
|
2026-01-19 03:19:17 +08:00
|
|
|
|
email_configured: bool = Field(description="是否配置了邮箱服务")
|
2026-03-12 01:11:35 +08:00
|
|
|
|
password_policy_level: str = Field(description="密码策略等级:weak/medium/strong")
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# ========== 用户管理 ==========
|
|
|
|
|
|
class CreateUserRequest(BaseModel):
|
|
|
|
|
|
"""创建用户请求"""
|
|
|
|
|
|
|
2026-03-17 16:34:09 +08:00
|
|
|
|
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
|
|
|
|
|
password: str = Field(..., description="密码")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = Field(None, max_length=255, description="邮箱地址(可选)")
|
|
|
|
|
|
role: UserRole | None = Field(UserRole.USER, description="用户角色")
|
2026-03-08 00:05:48 +08:00
|
|
|
|
initial_gift_usd: float | None = Field(
|
|
|
|
|
|
default=None, description="初始赠款(USD),null 表示使用系统默认初始赠款"
|
|
|
|
|
|
)
|
|
|
|
|
|
unlimited: bool = Field(default=False, description="是否无限制")
|
2026-01-19 17:56:02 +08:00
|
|
|
|
# 访问限制字段
|
2026-02-01 17:28:00 +08:00
|
|
|
|
allowed_providers: list[str] | None = Field(
|
|
|
|
|
|
default=None, description="允许使用的提供商ID列表,null表示无限制"
|
|
|
|
|
|
)
|
|
|
|
|
|
allowed_api_formats: list[str] | None = Field(
|
|
|
|
|
|
default=None, description="允许使用的API格式列表,null表示无限制"
|
|
|
|
|
|
)
|
|
|
|
|
|
allowed_models: list[str] | None = Field(
|
|
|
|
|
|
default=None, description="允许使用的模型名称列表,null表示无限制"
|
|
|
|
|
|
)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
rate_limit: int | None = Field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
ge=0,
|
|
|
|
|
|
description="每分钟请求限制;null 表示继承系统默认,0 表示不限制",
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
@field_validator("initial_gift_usd", mode="before")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@classmethod
|
2026-03-08 00:05:48 +08:00
|
|
|
|
def validate_initial_gift_usd(cls, v: Any) -> Any:
|
|
|
|
|
|
"""验证初始赠款金额,null 表示使用系统默认初始赠款。"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
|
|
|
|
|
|
return float(v)
|
|
|
|
|
|
if isinstance(v, (int, float)):
|
2026-03-08 00:05:48 +08:00
|
|
|
|
raise ValueError("初始赠款必须在 0-10000 范围内")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("email")
|
2026-01-19 18:38:02 +08:00
|
|
|
|
@classmethod
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def validate_email(cls, v: str | None) -> str | None:
|
2026-01-19 18:38:02 +08:00
|
|
|
|
"""验证邮箱格式(如果提供)"""
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
v = v.strip()
|
|
|
|
|
|
if not v:
|
2026-01-19 18:38:02 +08:00
|
|
|
|
return None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
if not re.match(email_pattern, v):
|
|
|
|
|
|
raise ValueError("邮箱格式无效")
|
|
|
|
|
|
return v.lower()
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("username")
|
2026-01-19 18:38:02 +08:00
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_username(cls, v: Any) -> Any:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""验证用户名格式"""
|
|
|
|
|
|
v = v.strip()
|
|
|
|
|
|
if not v:
|
|
|
|
|
|
raise ValueError("用户名不能为空")
|
2026-01-27 12:17:19 +08:00
|
|
|
|
if not re.match(r"^[a-zA-Z0-9_.\-]+$", v):
|
|
|
|
|
|
raise ValueError("用户名只能包含字母、数字、下划线、连字符和点号")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return v
|
|
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
@field_validator("allowed_api_formats")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_allowed_api_formats(cls, v: list[str] | None) -> list[str] | None:
|
|
|
|
|
|
"""校验并规范化 allowed_api_formats(endpoint signature: family:kind)。"""
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
from src.core.api_format import list_endpoint_definitions, resolve_endpoint_definition
|
|
|
|
|
|
from src.core.api_format.signature import normalize_signature_key
|
|
|
|
|
|
|
|
|
|
|
|
allowed = [d.signature_key for d in list_endpoint_definitions()]
|
|
|
|
|
|
out: list[str] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for fmt in v:
|
|
|
|
|
|
if not fmt:
|
|
|
|
|
|
continue
|
|
|
|
|
|
norm = normalize_signature_key(fmt)
|
|
|
|
|
|
if resolve_endpoint_definition(norm) is None:
|
|
|
|
|
|
raise ValueError(f"allowed_api_formats 必须是以下之一: {allowed},当前值: {fmt}")
|
|
|
|
|
|
if norm in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(norm)
|
|
|
|
|
|
out.append(norm)
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@field_validator("password")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
@classmethod
|
2026-01-30 14:30:57 +08:00
|
|
|
|
def validate_password(cls, v: Any) -> Any:
|
2026-03-17 16:34:09 +08:00
|
|
|
|
"""基础校验(非空和算法限制),策略级别校验在服务层按系统配置执行。"""
|
|
|
|
|
|
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
2026-03-12 01:11:35 +08:00
|
|
|
|
if not valid:
|
|
|
|
|
|
raise ValueError(error_msg or "密码格式无效")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateUserRequest(BaseModel):
|
|
|
|
|
|
"""更新用户请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = None
|
|
|
|
|
|
username: str | None = None
|
2026-03-17 16:34:09 +08:00
|
|
|
|
password: str | None = Field(None, description="新密码(留空保持不变)")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
role: UserRole | None = None
|
2026-03-08 00:05:48 +08:00
|
|
|
|
unlimited: bool | None = None
|
2026-01-30 03:10:21 +08:00
|
|
|
|
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
|
|
|
|
|
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
|
|
|
|
|
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
2026-03-15 14:22:59 +08:00
|
|
|
|
rate_limit: int | None = Field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
ge=0,
|
|
|
|
|
|
description="每分钟请求限制;null 表示继承系统默认,0 表示不限制",
|
|
|
|
|
|
)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
is_active: bool | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
@field_validator("allowed_api_formats")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_allowed_api_formats(cls, v: list[str] | None) -> list[str] | None:
|
|
|
|
|
|
# 与 CreateUserRequest 保持一致
|
|
|
|
|
|
return CreateUserRequest.validate_allowed_api_formats(v)
|
|
|
|
|
|
|
2026-03-17 16:34:09 +08:00
|
|
|
|
@field_validator("password")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_password(cls, v: str | None) -> str | None:
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return CreateUserRequest.validate_password(v)
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class CreateApiKeyRequest(BaseModel):
|
|
|
|
|
|
"""创建API密钥请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
name: str | None = None
|
|
|
|
|
|
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
|
|
|
|
|
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
|
|
|
|
|
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
2026-03-15 14:22:59 +08:00
|
|
|
|
rate_limit: int | None = Field(
|
|
|
|
|
|
None,
|
|
|
|
|
|
ge=0,
|
|
|
|
|
|
description="每分钟请求限制;独立Key: null=继承系统默认,0=不限制;普通Key: 0=不限制",
|
|
|
|
|
|
)
|
2026-03-08 00:05:48 +08:00
|
|
|
|
expire_days: int | None = None # None = 永不过期,数字 = 多少天后过期
|
2026-01-30 03:10:21 +08:00
|
|
|
|
expires_at: str | None = None # ISO 日期字符串,如 "2025-12-31",优先于 expire_days
|
|
|
|
|
|
initial_balance_usd: float | None = Field(
|
2025-12-10 20:52:44 +08:00
|
|
|
|
None, description="初始余额(USD),仅用于独立Key,None = 无限制"
|
|
|
|
|
|
)
|
2026-03-08 00:05:48 +08:00
|
|
|
|
unlimited_balance: bool | None = Field(
|
|
|
|
|
|
None, description="是否无限余额(编辑独立Key时用于切换额度模式)"
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_standalone: bool = Field(False, description="是否为独立余额Key(给非注册用户使用)")
|
|
|
|
|
|
auto_delete_on_expiry: bool = Field(
|
|
|
|
|
|
False, description="过期后是否自动删除(True=物理删除,False=仅禁用)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
@field_validator("allowed_api_formats")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_allowed_api_formats(cls, v: list[str] | None) -> list[str] | None:
|
|
|
|
|
|
# 与 CreateUserRequest 保持一致
|
|
|
|
|
|
return CreateUserRequest.validate_allowed_api_formats(v)
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
|
|
|
|
"""用户响应"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
username: str
|
|
|
|
|
|
role: UserRole
|
2026-01-30 03:10:21 +08:00
|
|
|
|
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
|
|
|
|
|
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
|
|
|
|
|
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
2026-03-15 14:22:59 +08:00
|
|
|
|
rate_limit: int | None = None
|
2026-03-08 00:05:48 +08:00
|
|
|
|
unlimited: bool = False
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool
|
|
|
|
|
|
created_at: datetime
|
|
|
|
|
|
updated_at: datetime
|
2026-01-30 03:10:21 +08:00
|
|
|
|
last_login_at: datetime | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApiKeyResponse(BaseModel):
|
|
|
|
|
|
"""API密钥响应"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
user_id: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
key: str | None = None # 仅在创建时返回完整密钥
|
|
|
|
|
|
key_display: str | None = None # 脱敏后的密钥显示
|
|
|
|
|
|
name: str | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
total_requests: int
|
|
|
|
|
|
total_tokens: int
|
|
|
|
|
|
total_cost_usd: float
|
2026-01-30 03:10:21 +08:00
|
|
|
|
allowed_providers: list[str] | None
|
|
|
|
|
|
allowed_models: list[str] | None
|
2026-03-15 14:22:59 +08:00
|
|
|
|
rate_limit: int | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool
|
2026-01-30 03:10:21 +08:00
|
|
|
|
expires_at: datetime | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_standalone: bool = False
|
2026-01-30 03:10:21 +08:00
|
|
|
|
force_capabilities: dict[str, bool] | None = None # 强制开启的能力
|
2025-12-10 20:52:44 +08:00
|
|
|
|
created_at: datetime
|
2026-01-30 03:10:21 +08:00
|
|
|
|
last_used_at: datetime | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 提供商管理 ==========
|
|
|
|
|
|
class ProviderCreate(BaseModel):
|
|
|
|
|
|
"""创建提供商请求
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
架构说明:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
- Provider 仅包含提供商的元数据和计费配置
|
|
|
|
|
|
- API格式、URL、认证等配置应在 ProviderEndpoint 中设置
|
|
|
|
|
|
- API密钥应在 ProviderAPIKey 中设置
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
name: str = Field(..., min_length=1, max_length=100, description="提供商名称(唯一)")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
description: str | None = Field(None, description="提供商描述")
|
|
|
|
|
|
website: str | None = Field(None, max_length=500, description="主站网站")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# Provider 级别的配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
rate_limit: int | None = Field(None, description="每分钟请求限制")
|
|
|
|
|
|
concurrent_limit: int | None = Field(None, description="并发请求限制")
|
|
|
|
|
|
config: dict | None = Field(None, description="额外配置")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool = Field(False, description="是否启用(默认false,需要配置API密钥后才能启用)")
|
|
|
|
|
|
|
2026-01-28 00:04:47 +08:00
|
|
|
|
# 超时配置(秒),为空时使用全局配置
|
2026-02-01 17:28:00 +08:00
|
|
|
|
stream_first_byte_timeout: float | None = Field(
|
|
|
|
|
|
None, ge=1, le=300, description="流式请求首字节超时(秒)"
|
|
|
|
|
|
)
|
|
|
|
|
|
request_timeout: float | None = Field(
|
|
|
|
|
|
None, ge=1, le=600, description="非流式请求整体超时(秒)"
|
|
|
|
|
|
)
|
2026-01-28 00:04:47 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class ProviderUpdate(BaseModel):
|
|
|
|
|
|
"""更新提供商请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
name: str | None = Field(None, min_length=1, max_length=100)
|
|
|
|
|
|
description: str | None = None
|
|
|
|
|
|
website: str | None = Field(None, max_length=500)
|
|
|
|
|
|
api_format: str | None = None
|
|
|
|
|
|
base_url: str | None = None
|
|
|
|
|
|
headers: dict | None = None
|
|
|
|
|
|
max_retries: int | None = Field(None, ge=0, le=10)
|
|
|
|
|
|
priority: int | None = None
|
|
|
|
|
|
weight: float | None = Field(None, gt=0)
|
|
|
|
|
|
rate_limit: int | None = None
|
|
|
|
|
|
concurrent_limit: int | None = None
|
|
|
|
|
|
config: dict | None = None
|
|
|
|
|
|
is_active: bool | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-28 00:04:47 +08:00
|
|
|
|
# 超时配置(秒),为空时使用全局配置
|
2026-02-01 17:28:00 +08:00
|
|
|
|
stream_first_byte_timeout: float | None = Field(
|
|
|
|
|
|
None, ge=1, le=300, description="流式请求首字节超时(秒)"
|
|
|
|
|
|
)
|
|
|
|
|
|
request_timeout: float | None = Field(
|
|
|
|
|
|
None, ge=1, le=600, description="非流式请求整体超时(秒)"
|
|
|
|
|
|
)
|
2026-01-28 00:04:47 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class ProviderResponse(BaseModel):
|
|
|
|
|
|
"""提供商响应"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
description: str | None
|
|
|
|
|
|
website: str | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
api_format: str
|
|
|
|
|
|
base_url: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
max_retries: int
|
|
|
|
|
|
priority: int
|
|
|
|
|
|
weight: float
|
2026-01-30 03:10:21 +08:00
|
|
|
|
rate_limit: int | None
|
|
|
|
|
|
concurrent_limit: int | None
|
|
|
|
|
|
config: dict | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool
|
|
|
|
|
|
created_at: datetime
|
|
|
|
|
|
updated_at: datetime
|
|
|
|
|
|
models_count: int = 0
|
|
|
|
|
|
active_models_count: int = 0
|
|
|
|
|
|
api_keys_count: int = 0
|
|
|
|
|
|
|
2026-01-28 00:04:47 +08:00
|
|
|
|
# 超时配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
stream_first_byte_timeout: float | None = None
|
|
|
|
|
|
request_timeout: float | None = None
|
2026-01-28 00:04:47 +08:00
|
|
|
|
|
2025-12-18 02:20:53 +08:00
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 模型管理 ==========
|
|
|
|
|
|
class ModelCreate(BaseModel):
|
|
|
|
|
|
"""创建模型请求 - 价格和能力字段可选,为空时使用 GlobalModel 默认值"""
|
|
|
|
|
|
|
|
|
|
|
|
provider_model_name: str = Field(
|
2025-12-15 14:30:53 +08:00
|
|
|
|
..., min_length=1, max_length=200, description="Provider 侧的主模型名称"
|
|
|
|
|
|
)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_model_mappings: list[dict] | None = Field(
|
2025-12-15 14:30:53 +08:00
|
|
|
|
None,
|
2025-12-20 02:39:10 +08:00
|
|
|
|
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
global_model_id: str = Field(..., description="关联的 GlobalModel ID(必填)")
|
|
|
|
|
|
# 按次计费配置 - 可选,为空时使用 GlobalModel 默认值
|
2026-01-30 03:10:21 +08:00
|
|
|
|
price_per_request: float | None = Field(
|
2025-12-10 20:52:44 +08:00
|
|
|
|
None, ge=0, description="每次请求固定费用,为空使用默认值"
|
|
|
|
|
|
)
|
|
|
|
|
|
# 阶梯计费配置 - 可选,为空时使用 GlobalModel 默认值
|
2026-01-30 03:10:21 +08:00
|
|
|
|
tiered_pricing: dict | None = Field(
|
2025-12-10 20:52:44 +08:00
|
|
|
|
None, description="阶梯计费配置,为空使用 GlobalModel 默认值"
|
|
|
|
|
|
)
|
|
|
|
|
|
# 能力配置 - 可选,为空时使用 GlobalModel 默认值
|
2026-01-30 03:10:21 +08:00
|
|
|
|
supports_vision: bool | None = Field(None, description="是否支持图像输入,为空使用默认值")
|
|
|
|
|
|
supports_function_calling: bool | None = Field(
|
2025-12-10 20:52:44 +08:00
|
|
|
|
None, description="是否支持函数调用,为空使用默认值"
|
|
|
|
|
|
)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
supports_streaming: bool | None = Field(None, description="是否支持流式输出,为空使用默认值")
|
|
|
|
|
|
supports_extended_thinking: bool | None = Field(
|
2025-12-10 20:52:44 +08:00
|
|
|
|
None, description="是否支持扩展思考,为空使用默认值"
|
|
|
|
|
|
)
|
|
|
|
|
|
is_active: bool = Field(True, description="是否启用")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
config: dict | None = Field(None, description="额外配置")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelUpdate(BaseModel):
|
|
|
|
|
|
"""更新模型请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_model_name: str | None = Field(None, min_length=1, max_length=200)
|
|
|
|
|
|
provider_model_mappings: list[dict] | None = Field(
|
2025-12-15 14:30:53 +08:00
|
|
|
|
None,
|
2025-12-20 02:39:10 +08:00
|
|
|
|
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
2025-12-15 14:30:53 +08:00
|
|
|
|
)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
global_model_id: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 按次计费配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
price_per_request: float | None = Field(None, ge=0, description="每次请求固定费用")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 阶梯计费配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
tiered_pricing: dict | None = Field(None, description="阶梯计费配置")
|
|
|
|
|
|
supports_vision: bool | None = None
|
|
|
|
|
|
supports_function_calling: bool | None = None
|
|
|
|
|
|
supports_streaming: bool | None = None
|
|
|
|
|
|
supports_extended_thinking: bool | None = None
|
|
|
|
|
|
is_active: bool | None = None
|
|
|
|
|
|
is_available: bool | None = None
|
|
|
|
|
|
config: dict | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelResponse(BaseModel):
|
|
|
|
|
|
"""模型响应 - 包含 Model 配置和关联的 GlobalModel 信息
|
|
|
|
|
|
|
|
|
|
|
|
注意:价格和能力字段返回的是有效值(优先使用 Model 配置,否则使用 GlobalModel 默认值)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
provider_id: str
|
2026-02-28 11:44:08 +08:00
|
|
|
|
global_model_id: str
|
2025-12-10 20:52:44 +08:00
|
|
|
|
provider_model_name: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_model_mappings: list[dict] | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 按次计费配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
price_per_request: float | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 阶梯计费配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
tiered_pricing: dict | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# Provider 能力配置 - 可选,为空表示使用 GlobalModel 默认值
|
2026-01-30 03:10:21 +08:00
|
|
|
|
supports_vision: bool | None
|
|
|
|
|
|
supports_function_calling: bool | None
|
|
|
|
|
|
supports_streaming: bool | None
|
|
|
|
|
|
supports_extended_thinking: bool | None
|
|
|
|
|
|
supports_image_generation: bool | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 有效值(合并 Model 配置和 GlobalModel 默认值后的结果)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
effective_tiered_pricing: dict | None = None
|
|
|
|
|
|
effective_input_price: float | None = None
|
|
|
|
|
|
effective_output_price: float | None = None
|
|
|
|
|
|
effective_price_per_request: float | None = None
|
|
|
|
|
|
effective_supports_vision: bool | None = None
|
|
|
|
|
|
effective_supports_function_calling: bool | None = None
|
|
|
|
|
|
effective_supports_streaming: bool | None = None
|
|
|
|
|
|
effective_supports_extended_thinking: bool | None = None
|
|
|
|
|
|
effective_supports_image_generation: bool | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 状态
|
|
|
|
|
|
is_active: bool
|
|
|
|
|
|
is_available: bool
|
|
|
|
|
|
|
|
|
|
|
|
# 时间戳
|
|
|
|
|
|
created_at: datetime
|
|
|
|
|
|
updated_at: datetime
|
|
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
# 关联的 GlobalModel 信息
|
2026-01-30 03:10:21 +08:00
|
|
|
|
global_model_name: str | None = None
|
|
|
|
|
|
global_model_display_name: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-03 18:48:39 +08:00
|
|
|
|
# 有效配置(合并 Model 和 GlobalModel 的 config)
|
|
|
|
|
|
effective_config: dict | None = None
|
|
|
|
|
|
|
2025-12-18 02:20:53 +08:00
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelDetailResponse(BaseModel):
|
|
|
|
|
|
"""模型详细响应 - 包含所有字段(用于需要完整信息的场景)"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
display_name: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
description: str | None
|
|
|
|
|
|
icon_url: str | None
|
|
|
|
|
|
tags: list[str] | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
input_price_per_1m: float
|
|
|
|
|
|
output_price_per_1m: float
|
2026-01-30 03:10:21 +08:00
|
|
|
|
cache_creation_price_per_1m: float | None
|
|
|
|
|
|
cache_read_price_per_1m: float | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
supports_vision: bool
|
|
|
|
|
|
supports_function_calling: bool
|
|
|
|
|
|
supports_streaming: bool
|
|
|
|
|
|
is_active: bool
|
|
|
|
|
|
is_available: bool
|
2026-01-30 03:10:21 +08:00
|
|
|
|
config: dict | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
created_at: datetime
|
|
|
|
|
|
updated_at: datetime
|
|
|
|
|
|
|
2025-12-18 02:20:53 +08:00
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 系统设置 ==========
|
|
|
|
|
|
class SystemSettingsRequest(BaseModel):
|
|
|
|
|
|
"""系统设置请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
default_provider: str | None = None
|
|
|
|
|
|
default_model: str | None = None
|
|
|
|
|
|
enable_usage_tracking: bool | None = None
|
2026-03-12 01:11:35 +08:00
|
|
|
|
password_policy_level: Literal["weak", "medium", "strong"] | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SystemSettingsResponse(BaseModel):
|
|
|
|
|
|
"""系统设置响应"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
default_provider: str | None
|
|
|
|
|
|
default_model: str | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
enable_usage_tracking: bool
|
2026-03-12 01:11:35 +08:00
|
|
|
|
password_policy_level: str
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 使用统计 ==========
|
|
|
|
|
|
class UsageStatsResponse(BaseModel):
|
|
|
|
|
|
"""使用统计响应"""
|
|
|
|
|
|
|
|
|
|
|
|
total_requests: int
|
|
|
|
|
|
total_tokens: int
|
|
|
|
|
|
total_cost_usd: float
|
|
|
|
|
|
daily_requests: int
|
|
|
|
|
|
daily_tokens: int
|
|
|
|
|
|
daily_cost_usd: float
|
2026-01-30 03:10:21 +08:00
|
|
|
|
model_usage: dict[str, dict[str, Any]]
|
|
|
|
|
|
provider_usage: dict[str, dict[str, Any]]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 公开API响应模型 ==========
|
|
|
|
|
|
class PublicProviderResponse(BaseModel):
|
|
|
|
|
|
"""公开的提供商信息响应"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
description: str | None
|
|
|
|
|
|
website: str | None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool
|
|
|
|
|
|
provider_priority: int # 提供商优先级(数字越小越优先)
|
|
|
|
|
|
# 统计信息
|
|
|
|
|
|
models_count: int
|
|
|
|
|
|
active_models_count: int
|
|
|
|
|
|
endpoints_count: int # 端点总数
|
|
|
|
|
|
active_endpoints_count: int # 活跃端点数
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicModelResponse(BaseModel):
|
|
|
|
|
|
"""公开的模型信息响应"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
|
provider_name: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
display_name: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
description: str | None = None
|
|
|
|
|
|
tags: list[str] | None = None
|
|
|
|
|
|
icon_url: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 价格信息
|
2026-01-30 03:10:21 +08:00
|
|
|
|
input_price_per_1m: float | None = None
|
|
|
|
|
|
output_price_per_1m: float | None = None
|
|
|
|
|
|
cache_creation_price_per_1m: float | None = None
|
|
|
|
|
|
cache_read_price_per_1m: float | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 功能支持
|
2026-01-30 03:10:21 +08:00
|
|
|
|
supports_vision: bool | None = None
|
|
|
|
|
|
supports_function_calling: bool | None = None
|
|
|
|
|
|
supports_streaming: bool | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProviderStatsResponse(BaseModel):
|
|
|
|
|
|
"""提供商统计信息响应"""
|
|
|
|
|
|
|
|
|
|
|
|
total_providers: int
|
|
|
|
|
|
active_providers: int
|
|
|
|
|
|
total_models: int
|
|
|
|
|
|
active_models: int
|
2026-01-30 03:10:21 +08:00
|
|
|
|
supported_formats: list[str]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicGlobalModelResponse(BaseModel):
|
|
|
|
|
|
"""公开的 GlobalModel 信息响应(用户可见)"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
display_name: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
is_active: bool = True
|
|
|
|
|
|
# 按次计费配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
default_price_per_request: float | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 阶梯计费配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
default_tiered_pricing: dict | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# Key 能力配置
|
2026-01-30 03:10:21 +08:00
|
|
|
|
supported_capabilities: list[str] | None = None
|
2025-12-16 12:21:21 +08:00
|
|
|
|
# 模型配置(JSON)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
config: dict | None = None
|
2026-02-28 11:44:08 +08:00
|
|
|
|
# 调用次数
|
|
|
|
|
|
usage_count: int = 0
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicGlobalModelListResponse(BaseModel):
|
|
|
|
|
|
"""公开的 GlobalModel 列表响应"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
models: list[PublicGlobalModelResponse]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
total: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 个人中心相关模型 ==========
|
|
|
|
|
|
class UpdateProfileRequest(BaseModel):
|
|
|
|
|
|
"""更新个人信息请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
email: str | None = None
|
|
|
|
|
|
username: str | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdatePreferencesRequest(BaseModel):
|
|
|
|
|
|
"""更新偏好设置请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
avatar_url: str | None = None
|
|
|
|
|
|
bio: str | None = None
|
|
|
|
|
|
default_provider_id: int | None = None
|
|
|
|
|
|
theme: str | None = None
|
|
|
|
|
|
language: str | None = None
|
|
|
|
|
|
timezone: str | None = None
|
|
|
|
|
|
email_notifications: bool | None = None
|
|
|
|
|
|
usage_alerts: bool | None = None
|
|
|
|
|
|
announcement_notifications: bool | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChangePasswordRequest(BaseModel):
|
|
|
|
|
|
"""修改密码请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
old_password: str | None = None # 可选:首次设置密码时不需要
|
2026-03-17 16:34:09 +08:00
|
|
|
|
new_password: str = Field(..., description="新密码")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("new_password")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_new_password(cls, v: Any) -> Any:
|
|
|
|
|
|
"""基础校验(非空和算法限制),策略级别校验在服务层按系统配置执行。"""
|
|
|
|
|
|
valid, error_msg = PasswordValidator.validate_basic_input(v)
|
|
|
|
|
|
if not valid:
|
|
|
|
|
|
raise ValueError(error_msg or "密码格式无效")
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserSessionResponse(BaseModel):
|
|
|
|
|
|
"""用户会话响应"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
device_label: str
|
|
|
|
|
|
device_type: str
|
|
|
|
|
|
browser_name: str | None = None
|
|
|
|
|
|
browser_version: str | None = None
|
|
|
|
|
|
os_name: str | None = None
|
|
|
|
|
|
os_version: str | None = None
|
|
|
|
|
|
device_model: str | None = None
|
|
|
|
|
|
ip_address: str | None = None
|
|
|
|
|
|
last_seen_at: str | None = None
|
|
|
|
|
|
created_at: str
|
|
|
|
|
|
is_current: bool = False
|
|
|
|
|
|
revoked_at: str | None = None
|
|
|
|
|
|
revoke_reason: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def from_db(cls, session: Any, *, current_session_id: str | None = None) -> dict[str, Any]:
|
|
|
|
|
|
return cls(
|
|
|
|
|
|
id=session.id,
|
|
|
|
|
|
device_label=session.device_label or "未知设备",
|
|
|
|
|
|
device_type=session.device_type or "unknown",
|
|
|
|
|
|
browser_name=session.browser_name,
|
|
|
|
|
|
browser_version=session.browser_version,
|
|
|
|
|
|
os_name=session.os_name,
|
|
|
|
|
|
os_version=session.os_version,
|
|
|
|
|
|
device_model=session.device_model,
|
|
|
|
|
|
ip_address=session.ip_address,
|
|
|
|
|
|
last_seen_at=session.last_seen_at.isoformat() if session.last_seen_at else None,
|
|
|
|
|
|
created_at=session.created_at.isoformat(),
|
|
|
|
|
|
is_current=bool(current_session_id and session.id == current_session_id),
|
|
|
|
|
|
revoked_at=session.revoked_at.isoformat() if session.revoked_at else None,
|
|
|
|
|
|
revoke_reason=session.revoke_reason,
|
|
|
|
|
|
).model_dump()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateSessionLabelRequest(BaseModel):
|
|
|
|
|
|
"""更新会话显示名称"""
|
|
|
|
|
|
|
|
|
|
|
|
device_label: str = Field(..., min_length=1, max_length=120, description="设备名称")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("device_label")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_device_label(cls, v: Any) -> Any:
|
|
|
|
|
|
normalized = str(v).strip()
|
|
|
|
|
|
if not normalized:
|
|
|
|
|
|
raise ValueError("设备名称不能为空")
|
|
|
|
|
|
return normalized
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateMyApiKeyRequest(BaseModel):
|
|
|
|
|
|
"""创建我的API密钥请求"""
|
|
|
|
|
|
|
|
|
|
|
|
name: str
|
2026-03-15 14:22:59 +08:00
|
|
|
|
rate_limit: int = Field(0, ge=0, description="该 Key 的每分钟请求限制,0 表示不限制")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateMyApiKeyRequest(BaseModel):
|
|
|
|
|
|
"""更新我的 API 密钥请求"""
|
|
|
|
|
|
|
|
|
|
|
|
name: str | None = None
|
|
|
|
|
|
rate_limit: int | None = Field(
|
|
|
|
|
|
None,
|
|
|
|
|
|
ge=0,
|
|
|
|
|
|
description="该 Key 的每分钟请求限制;0 表示不限制,null 表示不修改",
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProviderConfig(BaseModel):
|
|
|
|
|
|
"""提供商配置"""
|
|
|
|
|
|
|
|
|
|
|
|
provider_id: str = Field(..., description="提供商ID")
|
|
|
|
|
|
priority: int = Field(100, description="优先级(越高越优先)")
|
|
|
|
|
|
weight: float = Field(1.0, description="负载均衡权重")
|
|
|
|
|
|
enabled: bool = Field(True, description="是否启用")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateApiKeyProvidersRequest(BaseModel):
|
|
|
|
|
|
"""更新API密钥可用提供商请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
allowed_providers: list[ProviderConfig] | None = None # 提供商配置列表
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 公告相关模型 ==========
|
|
|
|
|
|
class CreateAnnouncementRequest(BaseModel):
|
|
|
|
|
|
"""创建公告请求"""
|
|
|
|
|
|
|
|
|
|
|
|
title: str
|
|
|
|
|
|
content: str # 支持Markdown
|
|
|
|
|
|
type: str = "info" # info, warning, maintenance, important
|
|
|
|
|
|
priority: int = 0
|
|
|
|
|
|
is_pinned: bool = False
|
2026-01-30 03:10:21 +08:00
|
|
|
|
start_time: datetime | None = None
|
|
|
|
|
|
end_time: datetime | None = None
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateAnnouncementRequest(BaseModel):
|
|
|
|
|
|
"""更新公告请求"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
title: str | None = None
|
|
|
|
|
|
content: str | None = None
|
|
|
|
|
|
type: str | None = None
|
|
|
|
|
|
priority: int | None = None
|
|
|
|
|
|
is_active: bool | None = None
|
|
|
|
|
|
is_pinned: bool | None = None
|
|
|
|
|
|
start_time: datetime | None = None
|
|
|
|
|
|
end_time: datetime | None = None
|