Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
API Token 路由
个人访问令牌管理
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List
from app.database import get_db
from app.base_schema import ResponseModel
from utils.security import get_current_user
from core.api_token.service import ApiTokenService
from core.api_token.schema import (
ApiTokenCreate,
ApiTokenResponse,
ApiTokenCreateResponse,
)
router = APIRouter(prefix="/api-tokens", tags=["API Token管理"])
@router.get("", response_model=List[ApiTokenResponse], summary="获取Token列表")
async def list_tokens(
current_user=Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""获取当前用户的所有API Token"""
tokens = await ApiTokenService.get_user_tokens(db, current_user.id)
return tokens
@router.post("", response_model=ApiTokenCreateResponse, summary="创建Token")
async def create_token(
data: ApiTokenCreate,
current_user=Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
创建新的API Token
注意:完整的Token值仅在创建时返回一次,请妥善保存。
"""
db_token, raw_token = await ApiTokenService.create_token(
db=db,
user_id=current_user.id,
name=data.name,
expires_at=data.expires_at,
description=data.description,
)
return ApiTokenCreateResponse(
id=db_token.id,
name=db_token.name,
token=raw_token,
token_prefix=db_token.token_prefix,
expires_at=db_token.expires_at,
description=db_token.description,
sys_create_datetime=db_token.sys_create_datetime,
)
@router.delete("/{token_id}", response_model=ResponseModel, summary="撤销Token")
async def revoke_token(
token_id: str,
current_user=Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""撤销指定的API Token"""
success = await ApiTokenService.revoke_token(db, token_id, current_user.id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Token不存在",
)
return ResponseModel(message="Token已撤销")
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
API Token 数据模型
个人访问令牌,用于API调用认证
"""
from sqlalchemy import Column, String, DateTime, Boolean, Text
from app.base_model import BaseModel
class ApiToken(BaseModel):
"""API Token 模型"""
__tablename__ = "core_api_token"
name = Column(String(100), nullable=False, comment="令牌名称")
token_hash = Column(String(64), nullable=False, unique=True, index=True, comment="令牌哈希值(SHA-256)")
token_prefix = Column(String(255), nullable=False, comment="令牌前缀(用于识别)")
user_id = Column(String(21), nullable=False, index=True, comment="所属用户ID")
expires_at = Column(DateTime, nullable=True, comment="过期时间(NULL表示永不过期)")
last_used_at = Column(DateTime, nullable=True, comment="最后使用时间")
description = Column(Text, nullable=True, comment="令牌描述")
is_active = Column(Boolean, default=True, comment="是否启用")
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
API Token Schema
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
from app.base_schema import CSTDatetime
class ApiTokenCreate(BaseModel):
"""创建API Token请求"""
name: str = Field(..., min_length=1, max_length=100, description="令牌名称")
expires_at: Optional[datetime] = Field(None, description="过期时间(不传表示永不过期)")
description: Optional[str] = Field(None, max_length=500, description="令牌描述")
class ApiTokenResponse(BaseModel):
"""API Token响应(列表展示用,不包含完整token)"""
id: str
name: str
token_prefix: str = Field(description="令牌前缀,用于识别")
expires_at: Optional[CSTDatetime] = None
last_used_at: Optional[CSTDatetime] = None
description: Optional[str] = None
is_active: bool
sys_create_datetime: Optional[CSTDatetime] = None
class Config:
from_attributes = True
class ApiTokenCreateResponse(BaseModel):
"""创建API Token的响应(仅创建时返回一次完整token)"""
id: str
name: str
token: str = Field(description="完整的API Token(仅此一次展示,请妥善保存)")
token_prefix: str
expires_at: Optional[CSTDatetime] = None
description: Optional[str] = None
sys_create_datetime: Optional[CSTDatetime] = None
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
API Token Service
"""
import hashlib
import secrets
from datetime import datetime, timezone
from typing import Optional, List, Tuple
from sqlalchemy import select, func, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.api_token.model import ApiToken
TOKEN_PREFIX = "zqpat_"
def generate_token() -> str:
"""生成随机API Token"""
return TOKEN_PREFIX + secrets.token_hex(32)
def hash_token(token: str) -> str:
"""对token进行SHA-256哈希"""
return hashlib.sha256(token.encode()).hexdigest()
def get_token_display_prefix(token: str) -> str:
"""获取token的展示前缀(前12位 + ..."""
return token[:12] + "..."
class ApiTokenService:
"""API Token 服务层"""
@classmethod
async def create_token(
cls,
db: AsyncSession,
user_id: str,
name: str,
expires_at: Optional[datetime] = None,
description: Optional[str] = None,
) -> Tuple[ApiToken, str]:
"""
创建API Token
:return: (数据库记录, 明文token) - 明文token仅返回一次
"""
raw_token = generate_token()
db_obj = ApiToken(
name=name,
token_hash=hash_token(raw_token),
token_prefix=get_token_display_prefix(raw_token),
user_id=user_id,
expires_at=expires_at,
description=description,
is_active=True,
sys_creator_id=user_id,
)
db.add(db_obj)
await db.commit()
await db.refresh(db_obj)
return db_obj, raw_token
@classmethod
async def get_user_tokens(
cls,
db: AsyncSession,
user_id: str,
) -> List[ApiToken]:
"""获取用户的所有API Token"""
result = await db.execute(
select(ApiToken)
.where(
ApiToken.user_id == user_id,
ApiToken.is_deleted == False, # noqa: E712
)
.order_by(desc(ApiToken.sys_create_datetime))
)
return list(result.scalars().all())
@classmethod
async def get_token_by_id(
cls,
db: AsyncSession,
token_id: str,
user_id: str,
) -> Optional[ApiToken]:
"""根据ID获取Token(限定用户)"""
result = await db.execute(
select(ApiToken).where(
ApiToken.id == token_id,
ApiToken.user_id == user_id,
ApiToken.is_deleted == False, # noqa: E712
)
)
return result.scalar_one_or_none()
@classmethod
async def revoke_token(
cls,
db: AsyncSession,
token_id: str,
user_id: str,
) -> bool:
"""撤销(软删除)Token"""
token = await cls.get_token_by_id(db, token_id, user_id)
if not token:
return False
token.is_deleted = True
token.is_active = False
await db.commit()
return True
@classmethod
async def verify_token(
cls,
db: AsyncSession,
raw_token: str,
) -> Optional[ApiToken]:
"""
验证API Token
:return: 有效则返回Token记录,否则返回None
"""
token_hash_value = hash_token(raw_token)
result = await db.execute(
select(ApiToken).where(
ApiToken.token_hash == token_hash_value,
ApiToken.is_deleted == False, # noqa: E712
ApiToken.is_active == True, # noqa: E712
)
)
token = result.scalar_one_or_none()
if not token:
return None
now_utc = datetime.now(timezone.utc)
if token.expires_at:
expires = token.expires_at if token.expires_at.tzinfo else token.expires_at.replace(tzinfo=timezone.utc)
if expires < now_utc:
return None
token.last_used_at = now_utc.replace(tzinfo=None)
await db.commit()
return token