feat: 添加访问令牌管理功能并升级至 0.2.4

- 新增 Management Token(访问令牌)功能,支持创建、更新、删除和管理
- 前端添加访问令牌管理页面,支持普通用户和管理员
- 后端实现完整的令牌生命周期管理 API
- 添加数据库迁移脚本创建 management_tokens 表
- Nginx 配置添加 gzip 压缩,优化响应传输
- Dialog 组件添加 persistent 属性,防止意外关闭
- 为管理后台 API 添加详细的中文文档注释
- 简化多处类型注解,统一代码风格
This commit is contained in:
fawney19
2026-01-07 14:55:07 +08:00
parent f6a6410626
commit 0061fc04b7
59 changed files with 6265 additions and 648 deletions

View File

@@ -2,9 +2,11 @@
from fastapi import APIRouter
from .management_tokens import router as management_tokens_router
from .routes import router as me_router
router = APIRouter()
router.include_router(me_router)
router.include_router(management_tokens_router)
__all__ = ["router"]

View File

@@ -0,0 +1,577 @@
"""用户 Management Token 管理端点"""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session
from src.api.base.authenticated_adapter import AuthenticatedApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import ApiRequestPipeline
from src.core.exceptions import InvalidRequestException, NotFoundException
from src.database import get_db
from src.models.database import AuditEventType
from src.services.management_token import (
ManagementTokenService,
parse_expires_at,
token_to_dict,
validate_ip_list,
)
router = APIRouter(prefix="/api/me/management-tokens", tags=["Management Tokens"])
pipeline = ApiRequestPipeline()
# ============== 安全基类 ==============
class ManagementTokenApiAdapter(AuthenticatedApiAdapter):
"""Management Token 管理 API 的基类
安全限制:禁止使用 Management Token 调用这些接口,
防止用户通过已有的 Token 再创建/修改/删除其他 Token。
"""
def authorize(self, context: ApiRequestContext):
# 先调用父类的认证检查
super().authorize(context)
# 禁止使用 Management Token 调用 management-tokens 相关接口
if context.management_token is not None:
raise HTTPException(
status_code=403,
detail="不允许使用 Management Token 管理其他 Token请使用 Web 界面或 JWT 认证",
)
# ============== 请求/响应模型 ==============
class CreateManagementTokenRequest(BaseModel):
"""创建 Management Token 请求"""
name: str = Field(..., min_length=1, max_length=100, description="Token 名称")
description: Optional[str] = Field(None, max_length=500, description="描述")
allowed_ips: Optional[list[str]] = Field(None, description="IP 白名单")
expires_at: Optional[datetime] = Field(None, description="过期时间")
@field_validator("allowed_ips")
@classmethod
def validate_allowed_ips(cls, v: Optional[list[str]]) -> Optional[list[str]]:
return validate_ip_list(v)
@field_validator("expires_at", mode="before")
@classmethod
def parse_expires(cls, v):
return parse_expires_at(v)
class UpdateManagementTokenRequest(BaseModel):
"""更新 Management Token 请求
对于 allowed_ips 和 expires_at 字段:
- 未提供(字段不在请求中): 不修改
- 显式设为 null: 清空该字段
- 提供有效值: 更新为新值
"""
model_config = {"extra": "allow"} # 允许额外字段以便检测哪些字段被显式提供
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
allowed_ips: Optional[list[str]] = None
expires_at: Optional[datetime] = None
# 用于追踪哪些字段被显式提供(包括显式设为 null 的情况)
_provided_fields: set[str] = set()
def __init__(self, **data):
# 记录实际传入的字段(包括值为 None 的)
provided = set(data.keys())
super().__init__(**data)
object.__setattr__(self, "_provided_fields", provided)
def is_field_provided(self, field_name: str) -> bool:
"""检查字段是否被显式提供(区分未提供和显式设为 null"""
return field_name in self._provided_fields
@field_validator("allowed_ips")
@classmethod
def validate_allowed_ips(cls, v: Optional[list[str]]) -> Optional[list[str]]:
# 如果是 None表示要清空直接返回
if v is None:
return None
return validate_ip_list(v)
@field_validator("expires_at", mode="before")
@classmethod
def parse_expires(cls, v):
# 如果是 None 或空字符串,表示要清空
if v is None or (isinstance(v, str) and not v.strip()):
return None
return parse_expires_at(v)
# ============== 路由 ==============
@router.get("")
async def list_my_management_tokens(
request: Request,
is_active: Optional[bool] = Query(None, description="筛选激活状态"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db),
):
"""列出当前用户的 Management Tokens
获取当前登录用户创建的所有 Management Tokens支持按激活状态筛选和分页。
**查询参数**
- is_active (Optional[bool]): 筛选激活状态true/false不传则返回全部
- skip (int): 分页偏移量,默认 0
- limit (int): 每页数量,范围 1-100默认 50
**返回字段**
- items (List[dict]): Token 列表
- id (str): Token ID
- user_id (str): 所属用户 ID
- name (str): Token 名称
- description (Optional[str]): 描述
- token_hash (str): Token 哈希值(不返回明文)
- is_active (bool): 是否激活
- allowed_ips (Optional[List[str]]): IP 白名单
- expires_at (Optional[str]): 过期时间ISO 8601 格式)
- last_used_at (Optional[str]): 最后使用时间
- created_at (str): 创建时间
- updated_at (str): 更新时间
- total (int): 总数量
- skip (int): 当前偏移量
- limit (int): 当前每页数量
- quota (dict): 配额信息
- used (int): 已使用数量
- max (int): 最大允许数量
"""
adapter = ListMyManagementTokensAdapter(is_active=is_active, skip=skip, limit=limit)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.post("")
async def create_my_management_token(request: Request, db: Session = Depends(get_db)):
"""创建 Management Token
为当前用户创建一个新的 Management Token。
**请求体字段**
- name (str): Token 名称,必填,长度 1-100
- description (Optional[str]): 描述,可选,最大长度 500
- allowed_ips (Optional[List[str]]): IP 白名单,可选,支持 IPv4/IPv6 和 CIDR 格式
- expires_at (Optional[datetime]): 过期时间,可选,支持 ISO 8601 格式字符串或 datetime 对象
**返回字段**
- message (str): 操作结果消息
- token (str): 生成的 Token 明文(仅在创建时返回一次,请妥善保存)
- data (dict): Token 信息
- id (str): Token ID
- user_id (str): 所属用户 ID
- name (str): Token 名称
- description (Optional[str]): 描述
- token_hash (str): Token 哈希值
- is_active (bool): 是否激活(新创建默认为 true
- allowed_ips (Optional[List[str]]): IP 白名单
- expires_at (Optional[str]): 过期时间
- last_used_at (Optional[str]): 最后使用时间
- created_at (str): 创建时间
- updated_at (str): 更新时间
"""
adapter = CreateMyManagementTokenAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{token_id}")
async def get_my_management_token(
token_id: str,
request: Request,
db: Session = Depends(get_db),
):
"""获取 Management Token 详情
获取当前用户指定 Token 的详细信息。
**路径参数**
- token_id (str): Token ID
**返回字段**
- id (str): Token ID
- user_id (str): 所属用户 ID
- name (str): Token 名称
- description (Optional[str]): 描述
- token_hash (str): Token 哈希值(不返回明文)
- is_active (bool): 是否激活
- allowed_ips (Optional[List[str]]): IP 白名单
- expires_at (Optional[str]): 过期时间ISO 8601 格式)
- last_used_at (Optional[str]): 最后使用时间
- created_at (str): 创建时间
- updated_at (str): 更新时间
"""
adapter = GetMyManagementTokenAdapter(token_id=token_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("/{token_id}")
async def update_my_management_token(
token_id: str, request: Request, db: Session = Depends(get_db)
):
"""更新 Management Token
更新当前用户指定 Token 的信息。支持部分字段更新。
**路径参数**
- token_id (str): Token ID
**请求体字段**(所有字段均可选)
- name (Optional[str]): Token 名称,长度 1-100
- description (Optional[str]): 描述,最大长度 500传空字符串或 null 可清空
- allowed_ips (Optional[List[str]]): IP 白名单,传 null 可清空
- expires_at (Optional[datetime]): 过期时间,传 null 可清空
注意:未提供的字段不会被修改,显式传 null 表示清空该字段。
**返回字段**
- message (str): 操作结果消息
- data (dict): 更新后的 Token 信息
- id (str): Token ID
- user_id (str): 所属用户 ID
- name (str): Token 名称
- description (Optional[str]): 描述
- token_hash (str): Token 哈希值
- is_active (bool): 是否激活
- allowed_ips (Optional[List[str]]): IP 白名单
- expires_at (Optional[str]): 过期时间
- last_used_at (Optional[str]): 最后使用时间
- created_at (str): 创建时间
- updated_at (str): 更新时间
"""
adapter = UpdateMyManagementTokenAdapter(token_id=token_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/{token_id}")
async def delete_my_management_token(
token_id: str, request: Request, db: Session = Depends(get_db)
):
"""删除 Management Token
删除当前用户指定的 Token。
**路径参数**
- token_id (str): 要删除的 Token ID
**返回字段**
- message (str): 操作结果消息
"""
adapter = DeleteMyManagementTokenAdapter(token_id=token_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.patch("/{token_id}/status")
async def toggle_my_management_token(
token_id: str, request: Request, db: Session = Depends(get_db)
):
"""切换 Management Token 状态
启用或禁用当前用户指定的 Token。
**路径参数**
- token_id (str): Token ID
**返回字段**
- message (str): 操作结果消息("Token 已启用""Token 已禁用"
- data (dict): 更新后的 Token 信息
- id (str): Token ID
- user_id (str): 所属用户 ID
- name (str): Token 名称
- description (Optional[str]): 描述
- token_hash (str): Token 哈希值
- is_active (bool): 是否激活(已切换后的状态)
- allowed_ips (Optional[List[str]]): IP 白名单
- expires_at (Optional[str]): 过期时间
- last_used_at (Optional[str]): 最后使用时间
- created_at (str): 创建时间
- updated_at (str): 更新时间
"""
adapter = ToggleMyManagementTokenAdapter(token_id=token_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.post("/{token_id}/regenerate")
async def regenerate_my_management_token(
token_id: str, request: Request, db: Session = Depends(get_db)
):
"""重新生成 Management Token
重新生成当前用户指定 Token 的值,旧 Token 将立即失效。
**路径参数**
- token_id (str): Token ID
**返回字段**
- message (str): 操作结果消息
- token (str): 新生成的 Token 明文(仅在重新生成时返回一次,请妥善保存)
- data (dict): Token 信息
- id (str): Token ID
- user_id (str): 所属用户 ID
- name (str): Token 名称
- description (Optional[str]): 描述
- token_hash (str): 新的 Token 哈希值
- is_active (bool): 是否激活
- allowed_ips (Optional[List[str]]): IP 白名单
- expires_at (Optional[str]): 过期时间
- last_used_at (Optional[str]): 最后使用时间(重置为 null
- created_at (str): 创建时间
- updated_at (str): 更新时间
"""
adapter = RegenerateMyManagementTokenAdapter(token_id=token_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
# ============== 适配器 ==============
@dataclass
class ListMyManagementTokensAdapter(ManagementTokenApiAdapter):
"""列出用户的 Management Tokens"""
name: str = "list_my_management_tokens"
is_active: Optional[bool] = None
skip: int = 0
limit: int = 50
async def handle(self, context: ApiRequestContext):
from src.config.settings import config
tokens, total = ManagementTokenService.list_tokens(
db=context.db,
user_id=context.user.id,
is_active=self.is_active,
skip=self.skip,
limit=self.limit,
)
# 获取用户 Token 总数(用于配额显示)
max_tokens = config.management_token_max_per_user
return JSONResponse(
content={
"items": [token_to_dict(t) for t in tokens],
"total": total,
"skip": self.skip,
"limit": self.limit,
"quota": {
"used": total,
"max": max_tokens,
},
}
)
@dataclass
class CreateMyManagementTokenAdapter(ManagementTokenApiAdapter):
"""创建 Management Token"""
name: str = "create_my_management_token"
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_CREATED
async def handle(self, context: ApiRequestContext):
body = context.ensure_json_body()
try:
req = CreateManagementTokenRequest(**body)
except Exception as e:
raise InvalidRequestException(str(e))
try:
token, raw_token = ManagementTokenService.create_token(
db=context.db,
user_id=context.user.id,
name=req.name,
description=req.description,
allowed_ips=req.allowed_ips,
expires_at=req.expires_at,
)
except ValueError as e:
raise InvalidRequestException(str(e))
context.add_audit_metadata(token_id=token.id, token_name=token.name)
return JSONResponse(
status_code=201,
content={
"message": "Management Token 创建成功",
"token": raw_token, # 仅在创建时返回一次
"data": token_to_dict(token),
},
)
@dataclass
class GetMyManagementTokenAdapter(ManagementTokenApiAdapter):
"""获取 Management Token 详情"""
name: str = "get_my_management_token"
token_id: str = ""
async def handle(self, context: ApiRequestContext):
token = ManagementTokenService.get_token_by_id(
db=context.db, token_id=self.token_id, user_id=context.user.id
)
if not token:
raise NotFoundException("Management Token 不存在")
return JSONResponse(content=token_to_dict(token))
@dataclass
class UpdateMyManagementTokenAdapter(ManagementTokenApiAdapter):
"""更新 Management Token"""
name: str = "update_my_management_token"
token_id: str = ""
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_UPDATED
async def handle(self, context: ApiRequestContext):
body = context.ensure_json_body()
try:
req = UpdateManagementTokenRequest(**body)
except Exception as e:
raise InvalidRequestException(str(e))
# 构建更新参数,只包含显式提供的字段
update_kwargs: dict = {
"db": context.db,
"token_id": self.token_id,
"user_id": context.user.id,
}
# 对于普通字段,只有提供了才更新
if req.is_field_provided("name"):
update_kwargs["name"] = req.name
if req.is_field_provided("description"):
update_kwargs["description"] = req.description
update_kwargs["clear_description"] = req.description is None or req.description == ""
# 对于可清空字段,需要传递特殊标记
if req.is_field_provided("allowed_ips"):
update_kwargs["allowed_ips"] = req.allowed_ips
update_kwargs["clear_allowed_ips"] = req.allowed_ips is None
if req.is_field_provided("expires_at"):
update_kwargs["expires_at"] = req.expires_at
update_kwargs["clear_expires_at"] = req.expires_at is None
try:
token = ManagementTokenService.update_token(**update_kwargs)
except ValueError as e:
raise InvalidRequestException(str(e))
if not token:
raise NotFoundException("Management Token 不存在")
context.add_audit_metadata(token_id=token.id, token_name=token.name)
return JSONResponse(
content={"message": "更新成功", "data": token_to_dict(token)}
)
@dataclass
class DeleteMyManagementTokenAdapter(ManagementTokenApiAdapter):
"""删除 Management Token"""
name: str = "delete_my_management_token"
token_id: str = ""
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_DELETED
async def handle(self, context: ApiRequestContext):
# 先获取 token 信息用于审计
token = ManagementTokenService.get_token_by_id(
db=context.db, token_id=self.token_id, user_id=context.user.id
)
if not token:
raise NotFoundException("Management Token 不存在")
context.add_audit_metadata(token_id=token.id, token_name=token.name)
success = ManagementTokenService.delete_token(
db=context.db, token_id=self.token_id, user_id=context.user.id
)
if not success:
raise NotFoundException("Management Token 不存在")
return JSONResponse(content={"message": "删除成功"})
@dataclass
class ToggleMyManagementTokenAdapter(ManagementTokenApiAdapter):
"""切换 Management Token 状态"""
name: str = "toggle_my_management_token"
token_id: str = ""
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_UPDATED
async def handle(self, context: ApiRequestContext):
token = ManagementTokenService.toggle_status(
db=context.db, token_id=self.token_id, user_id=context.user.id
)
if not token:
raise NotFoundException("Management Token 不存在")
context.add_audit_metadata(
token_id=token.id, token_name=token.name, is_active=token.is_active
)
return JSONResponse(
content={
"message": f"Token 已{'启用' if token.is_active else '禁用'}",
"data": token_to_dict(token),
}
)
@dataclass
class RegenerateMyManagementTokenAdapter(ManagementTokenApiAdapter):
"""重新生成 Management Token"""
name: str = "regenerate_my_management_token"
token_id: str = ""
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_UPDATED
async def handle(self, context: ApiRequestContext):
token, raw_token, old_token_hash = ManagementTokenService.regenerate_token(
db=context.db, token_id=self.token_id, user_id=context.user.id
)
if not token:
raise NotFoundException("Management Token 不存在")
context.add_audit_metadata(
token_id=token.id,
token_name=token.name,
regenerated=True,
)
return JSONResponse(
content={
"message": "Token 已重新生成",
"token": raw_token, # 仅在重新生成时返回一次
"data": token_to_dict(token),
}
)

View File

@@ -35,20 +35,43 @@ pipeline = ApiRequestPipeline()
@router.get("")
async def get_my_profile(request: Request, db: Session = Depends(get_db)):
"""获取当前用户完整信息(包含偏好设置)"""
"""
获取当前用户信息
返回当前登录用户的完整信息,包括基本信息和偏好设置。
**返回字段**: id, email, username, role, is_active, quota_usd, used_usd, preferences 等
"""
adapter = MeProfileAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("")
async def update_my_profile(request: Request, db: Session = Depends(get_db)):
"""
更新个人信息
更新当前用户的邮箱或用户名。
**请求体**:
- `email`: 新邮箱地址(可选)
- `username`: 新用户名(可选)
"""
adapter = UpdateProfileAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.patch("/password")
async def change_my_password(request: Request, db: Session = Depends(get_db)):
"""Change current user's password"""
"""
修改密码
修改当前用户的登录密码。
**请求体**:
- `old_password`: 当前密码
- `new_password`: 新密码(至少 6 位)
"""
adapter = ChangePasswordAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -58,12 +81,30 @@ async def change_my_password(request: Request, db: Session = Depends(get_db)):
@router.get("/api-keys")
async def list_my_api_keys(request: Request, db: Session = Depends(get_db)):
"""
获取 API 密钥列表
返回当前用户的所有 API 密钥,包含使用统计信息。
密钥值仅显示前后几位,完整密钥需通过详情接口获取。
**返回字段**: id, name, key_display, is_active, total_requests, total_cost_usd, last_used_at 等
"""
adapter = ListMyApiKeysAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.post("/api-keys")
async def create_my_api_key(request: Request, db: Session = Depends(get_db)):
"""
创建 API 密钥
为当前用户创建新的 API 密钥。创建成功后会返回完整的密钥值,请妥善保存。
**请求体**:
- `name`: 密钥名称
**返回**: 包含完整密钥值的响应(仅此一次显示完整密钥)
"""
adapter = CreateMyApiKeyAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -72,10 +113,20 @@ async def create_my_api_key(request: Request, db: Session = Depends(get_db)):
async def get_my_api_key(
key_id: str,
request: Request,
include_key: bool = Query(False, description="Include full decrypted key in response"),
include_key: bool = Query(False, description="是否返回完整密钥"),
db: Session = Depends(get_db),
):
"""Get API key detail, optionally include full key"""
"""
获取 API 密钥详情
获取指定 API 密钥的详细信息。
**路径参数**:
- `key_id`: 密钥 ID
**查询参数**:
- `include_key`: 设为 true 时返回完整解密后的密钥值
"""
if include_key:
adapter = GetMyFullKeyAdapter(key_id=key_id)
else:
@@ -85,13 +136,28 @@ async def get_my_api_key(
@router.delete("/api-keys/{key_id}")
async def delete_my_api_key(key_id: str, request: Request, db: Session = Depends(get_db)):
"""
删除 API 密钥
永久删除指定的 API 密钥,删除后无法恢复。
**路径参数**:
- `key_id`: 密钥 ID
"""
adapter = DeleteMyApiKeyAdapter(key_id=key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.patch("/api-keys/{key_id}")
async def toggle_my_api_key(key_id: str, request: Request, db: Session = Depends(get_db)):
"""Toggle API key active status"""
"""
切换 API 密钥状态
启用或禁用指定的 API 密钥。禁用后该密钥将无法用于 API 调用。
**路径参数**:
- `key_id`: 密钥 ID
"""
adapter = ToggleMyApiKeyAdapter(key_id=key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -102,13 +168,27 @@ async def toggle_my_api_key(key_id: str, request: Request, db: Session = Depends
@router.get("/usage")
async def get_my_usage(
request: Request,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
search: Optional[str] = None, # 通用搜索:密钥名、模型名
start_date: Optional[datetime] = Query(None, description="开始时间ISO 格式)"),
end_date: Optional[datetime] = Query(None, description="结束时间ISO 格式)"),
search: Optional[str] = Query(None, description="搜索关键词(密钥名、模型名)"),
limit: int = Query(100, ge=1, le=200, description="每页记录数默认100最大200"),
offset: int = Query(0, ge=0, le=2000, description="偏移量用于分页最大2000"),
db: Session = Depends(get_db),
):
"""
获取使用统计
获取当前用户的 API 使用统计数据,包括总量汇总、按模型/提供商分组统计及详细记录。
**返回字段**:
- `total_requests`: 总请求数
- `total_tokens`: 总 Token 数
- `total_cost`: 总成本USD
- `summary_by_model`: 按模型分组统计
- `summary_by_provider`: 按提供商分组统计
- `records`: 详细使用记录列表
- `pagination`: 分页信息
"""
adapter = GetUsageAdapter(
start_date=start_date, end_date=end_date, search=search, limit=limit, offset=offset
)
@@ -118,10 +198,17 @@ async def get_my_usage(
@router.get("/usage/active")
async def get_my_active_requests(
request: Request,
ids: Optional[str] = Query(None, description="Comma-separated request IDs to query"),
ids: Optional[str] = Query(None, description="请求 ID 列表,逗号分隔"),
db: Session = Depends(get_db),
):
"""获取用户活跃请求状态(用于轮询更新)"""
"""
获取活跃请求状态
查询正在进行中的请求状态,用于前端轮询更新流式请求的进度。
**查询参数**:
- `ids`: 要查询的请求 ID 列表,逗号分隔
"""
adapter = GetActiveRequestsAdapter(ids=ids)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -133,7 +220,13 @@ async def get_my_interval_timeline(
limit: int = Query(5000, ge=100, le=20000, description="最大返回数据点数量"),
db: Session = Depends(get_db),
):
"""获取当前用户的请求间隔时间线数据,用于散点图展示"""
"""
获取请求间隔时间线
获取请求间隔时间线数据,用于散点图展示请求分布情况。
**返回**: 包含时间戳和间隔时间的数据点列表
"""
adapter = GetMyIntervalTimelineAdapter(hours=hours, limit=limit)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -144,9 +237,12 @@ async def get_my_activity_heatmap(
db: Session = Depends(get_db),
):
"""
Get user's activity heatmap data for the past 365 days.
获取活动热力图数据
This endpoint is cached for 5 minutes to reduce database load.
获取过去 365 天的活动热力图数据,用于展示每日使用频率。
此接口有 5 分钟缓存。
**返回**: 包含日期和请求数量的数据列表
"""
adapter = GetMyActivityHeatmapAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -154,13 +250,26 @@ async def get_my_activity_heatmap(
@router.get("/providers")
async def list_available_providers(request: Request, db: Session = Depends(get_db)):
"""
获取可用提供商列表
获取当前用户可用的所有提供商及其模型信息。
**返回字段**: id, name, display_name, endpoints, models 等
"""
adapter = ListAvailableProvidersAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/endpoint-status")
async def get_endpoint_status(request: Request, db: Session = Depends(get_db)):
"""获取端点状态(简化版,不包含敏感信息)"""
"""
获取端点健康状态
获取各 API 格式端点的健康状态(简化版,不包含敏感信息)。
**返回**: 按 API 格式分组的端点健康状态
"""
adapter = GetEndpointStatusAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -177,6 +286,17 @@ async def update_api_key_providers(
request: Request,
db: Session = Depends(get_db),
):
"""
更新 API 密钥可用提供商
设置指定 API 密钥可以使用哪些提供商。未设置时使用用户默认权限。
**路径参数**:
- `api_key_id`: API 密钥 ID
**请求体**:
- `allowed_providers`: 允许的提供商 ID 列表
"""
adapter = UpdateApiKeyProvidersAdapter(api_key_id=api_key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -187,7 +307,17 @@ async def update_api_key_capabilities(
request: Request,
db: Session = Depends(get_db),
):
"""更新 API Key 的强制能力配置"""
"""
更新 API 密钥能力配置
设置指定 API 密钥的强制能力配置(如是否启用代码执行等)。
**路径参数**:
- `api_key_id`: API 密钥 ID
**请求体**:
- `force_capabilities`: 能力配置字典,如 `{"code_execution": true}`
"""
adapter = UpdateApiKeyCapabilitiesAdapter(api_key_id=api_key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -197,26 +327,59 @@ async def update_api_key_capabilities(
@router.get("/preferences")
async def get_my_preferences(request: Request, db: Session = Depends(get_db)):
"""
获取偏好设置
获取当前用户的偏好设置,包括主题、语言、通知配置等。
**返回字段**: avatar_url, bio, theme, language, timezone, notifications 等
"""
adapter = GetPreferencesAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("/preferences")
async def update_my_preferences(request: Request, db: Session = Depends(get_db)):
"""
更新偏好设置
更新当前用户的偏好设置。
**请求体**:
- `theme`: 主题light/dark
- `language`: 语言
- `timezone`: 时区
- `email_notifications`: 邮件通知开关
- `usage_alerts`: 用量告警开关
- 等
"""
adapter = UpdatePreferencesAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/model-capabilities")
async def get_model_capability_settings(request: Request, db: Session = Depends(get_db)):
"""获取用户的模型能力配置"""
"""
获取模型能力配置
获取用户针对各模型的能力配置(如是否启用特定功能)。
**返回**: model_capability_settings 字典
"""
adapter = GetModelCapabilitySettingsAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("/model-capabilities")
async def update_model_capability_settings(request: Request, db: Session = Depends(get_db)):
"""更新用户的模型能力配置"""
"""
更新模型能力配置
更新用户针对各模型的能力配置。
**请求体**:
- `model_capability_settings`: 模型能力配置字典,格式为 `{"model_name": {"capability": true}}`
"""
adapter = UpdateModelCapabilitySettingsAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@@ -225,11 +388,15 @@ async def update_model_capability_settings(request: Request, db: Session = Depen
class MeProfileAdapter(AuthenticatedApiAdapter):
"""获取当前用户信息的适配器"""
async def handle(self, context): # type: ignore[override]
return PreferenceService.get_user_with_preferences(context.db, context.user.id)
class UpdateProfileAdapter(AuthenticatedApiAdapter):
"""更新用户个人信息的适配器"""
async def handle(self, context): # type: ignore[override]
db = context.db
user = context.user
@@ -265,6 +432,8 @@ class UpdateProfileAdapter(AuthenticatedApiAdapter):
class ChangePasswordAdapter(AuthenticatedApiAdapter):
"""修改用户密码的适配器"""
async def handle(self, context): # type: ignore[override]
db = context.db
user = context.user
@@ -290,6 +459,8 @@ class ChangePasswordAdapter(AuthenticatedApiAdapter):
class ListMyApiKeysAdapter(AuthenticatedApiAdapter):
"""获取用户 API 密钥列表的适配器"""
async def handle(self, context): # type: ignore[override]
db = context.db
user = context.user
@@ -359,6 +530,8 @@ class ListMyApiKeysAdapter(AuthenticatedApiAdapter):
class CreateMyApiKeyAdapter(AuthenticatedApiAdapter):
"""创建 API 密钥的适配器"""
async def handle(self, context): # type: ignore[override]
payload = context.ensure_json_body()
try:
@@ -388,6 +561,8 @@ class CreateMyApiKeyAdapter(AuthenticatedApiAdapter):
@dataclass
class GetMyFullKeyAdapter(AuthenticatedApiAdapter):
"""获取 API 密钥完整密钥值的适配器"""
key_id: str
async def handle(self, context): # type: ignore[override]
@@ -420,7 +595,8 @@ class GetMyFullKeyAdapter(AuthenticatedApiAdapter):
@dataclass
class GetMyApiKeyDetailAdapter(AuthenticatedApiAdapter):
"""Get API key detail without full key"""
"""获取 API 密钥详情的适配器(不包含完整密钥值)"""
key_id: str
async def handle(self, context): # type: ignore[override]
@@ -449,6 +625,8 @@ class GetMyApiKeyDetailAdapter(AuthenticatedApiAdapter):
@dataclass
class DeleteMyApiKeyAdapter(AuthenticatedApiAdapter):
"""删除 API 密钥的适配器"""
key_id: str
async def handle(self, context): # type: ignore[override]
@@ -466,6 +644,8 @@ class DeleteMyApiKeyAdapter(AuthenticatedApiAdapter):
@dataclass
class ToggleMyApiKeyAdapter(AuthenticatedApiAdapter):
"""切换 API 密钥启用/禁用状态的适配器"""
key_id: str
async def handle(self, context): # type: ignore[override]
@@ -488,6 +668,8 @@ class ToggleMyApiKeyAdapter(AuthenticatedApiAdapter):
@dataclass
class GetUsageAdapter(AuthenticatedApiAdapter):
"""获取用户使用统计的适配器"""
start_date: Optional[datetime]
end_date: Optional[datetime]
search: Optional[str] = None
@@ -766,7 +948,7 @@ class GetMyIntervalTimelineAdapter(AuthenticatedApiAdapter):
class GetMyActivityHeatmapAdapter(AuthenticatedApiAdapter):
"""Activity heatmap adapter with Redis caching for user."""
"""获取用户活动热力图数据的适配器(带 Redis 缓存)"""
async def handle(self, context): # type: ignore[override]
user = context.user
@@ -780,6 +962,8 @@ class GetMyActivityHeatmapAdapter(AuthenticatedApiAdapter):
class ListAvailableProvidersAdapter(AuthenticatedApiAdapter):
"""获取可用提供商列表的适配器"""
async def handle(self, context): # type: ignore[override]
from sqlalchemy.orm import selectinload
@@ -851,6 +1035,8 @@ class ListAvailableProvidersAdapter(AuthenticatedApiAdapter):
@dataclass
class UpdateApiKeyProvidersAdapter(AuthenticatedApiAdapter):
"""更新 API 密钥可用提供商的适配器"""
api_key_id: str
async def handle(self, context): # type: ignore[override]
@@ -962,6 +1148,8 @@ class UpdateApiKeyCapabilitiesAdapter(AuthenticatedApiAdapter):
class GetPreferencesAdapter(AuthenticatedApiAdapter):
"""获取用户偏好设置的适配器"""
async def handle(self, context): # type: ignore[override]
preferences = PreferenceService.get_or_create_preferences(context.db, context.user.id)
return {
@@ -983,6 +1171,8 @@ class GetPreferencesAdapter(AuthenticatedApiAdapter):
class UpdatePreferencesAdapter(AuthenticatedApiAdapter):
"""更新用户偏好设置的适配器"""
async def handle(self, context): # type: ignore[override]
payload = context.ensure_json_body()
try: