595 lines
20 KiB
Python
595 lines
20 KiB
Python
"""
|
|
管理接口模块
|
|
|
|
提供Token管理功能,包括登录验证、Token增删查等操作。
|
|
"""
|
|
|
|
import secrets
|
|
from typing import Dict, Any, List, Optional
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from fastapi import APIRouter, HTTPException, Depends, Header
|
|
from fastapi.responses import HTMLResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.config import setting
|
|
from app.core.logger import logger
|
|
from app.services.grok.token import token_manager
|
|
from app.models.grok_models import TokenType
|
|
|
|
|
|
# 创建路由器
|
|
router = APIRouter(tags=["管理"])
|
|
|
|
# 静态文件路径
|
|
STATIC_DIR = Path(__file__).parents[2] / "template"
|
|
|
|
# 简单的会话存储
|
|
_sessions: Dict[str, datetime] = {}
|
|
|
|
|
|
# === 请求/响应模型 ===
|
|
|
|
class LoginRequest(BaseModel):
|
|
"""登录请求"""
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
"""登录响应"""
|
|
success: bool
|
|
token: Optional[str] = None
|
|
message: str
|
|
|
|
|
|
class AddTokensRequest(BaseModel):
|
|
"""批量添加Token请求"""
|
|
tokens: List[str]
|
|
token_type: str # "sso" 或 "ssoSuper"
|
|
|
|
|
|
class DeleteTokensRequest(BaseModel):
|
|
"""批量删除Token请求"""
|
|
tokens: List[str]
|
|
token_type: str # "sso" 或 "ssoSuper"
|
|
|
|
|
|
class TokenInfo(BaseModel):
|
|
"""Token信息"""
|
|
token: str
|
|
token_type: str
|
|
created_time: Optional[int] = None
|
|
remaining_queries: int
|
|
heavy_remaining_queries: int
|
|
status: str # "未使用"、"限流中"、"失效"、"正常"
|
|
|
|
|
|
class TokenListResponse(BaseModel):
|
|
"""Token列表响应"""
|
|
success: bool
|
|
data: List[TokenInfo]
|
|
total: int
|
|
|
|
|
|
# === 辅助函数 ===
|
|
|
|
def validate_token_type(token_type_str: str) -> TokenType:
|
|
"""验证并转换Token类型字符串为枚举"""
|
|
if token_type_str not in ["sso", "ssoSuper"]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"error": "无效的Token类型,必须是 'sso' 或 'ssoSuper'", "code": "INVALID_TYPE"}
|
|
)
|
|
return TokenType.NORMAL if token_type_str == "sso" else TokenType.SUPER
|
|
|
|
|
|
def parse_created_time(created_time) -> Optional[int]:
|
|
"""解析创建时间,统一处理不同格式"""
|
|
if isinstance(created_time, str):
|
|
return int(created_time) if created_time else None
|
|
elif isinstance(created_time, int):
|
|
return created_time
|
|
return None
|
|
|
|
|
|
def calculate_token_stats(tokens: Dict[str, Any], token_type: str) -> Dict[str, int]:
|
|
"""计算Token统计信息"""
|
|
total = len(tokens)
|
|
expired = sum(1 for t in tokens.values() if t.get("status") == "expired")
|
|
|
|
if token_type == "normal":
|
|
unused = sum(1 for t in tokens.values()
|
|
if t.get("status") != "expired" and t.get("remainingQueries", -1) == -1)
|
|
limited = sum(1 for t in tokens.values()
|
|
if t.get("status") != "expired" and t.get("remainingQueries", -1) == 0)
|
|
active = sum(1 for t in tokens.values()
|
|
if t.get("status") != "expired" and t.get("remainingQueries", -1) > 0)
|
|
else: # super token
|
|
unused = sum(1 for t in tokens.values()
|
|
if t.get("status") != "expired" and
|
|
t.get("remainingQueries", -1) == -1 and t.get("heavyremainingQueries", -1) == -1)
|
|
limited = sum(1 for t in tokens.values()
|
|
if t.get("status") != "expired" and
|
|
(t.get("remainingQueries", -1) == 0 or t.get("heavyremainingQueries", -1) == 0))
|
|
active = sum(1 for t in tokens.values()
|
|
if t.get("status") != "expired" and
|
|
(t.get("remainingQueries", -1) > 0 or t.get("heavyremainingQueries", -1) > 0))
|
|
|
|
return {
|
|
"total": total,
|
|
"unused": unused,
|
|
"limited": limited,
|
|
"expired": expired,
|
|
"active": active
|
|
}
|
|
|
|
|
|
def verify_admin_session(authorization: Optional[str] = Header(None)) -> bool:
|
|
"""验证管理员会话"""
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": "未授权访问", "code": "UNAUTHORIZED"}
|
|
)
|
|
|
|
token = authorization[7:] # 移除 "Bearer " 前缀
|
|
|
|
# 检查token是否存在且未过期
|
|
if token not in _sessions:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": "会话已过期或无效", "code": "SESSION_INVALID"}
|
|
)
|
|
|
|
# 检查会话是否过期(24小时)
|
|
if datetime.now() > _sessions[token]:
|
|
del _sessions[token]
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": "会话已过期", "code": "SESSION_EXPIRED"}
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def get_token_status(token_data: Dict[str, Any], token_type: str) -> str:
|
|
"""获取Token状态"""
|
|
# 首先检查是否失效(来自 token.json 的 status 字段)
|
|
if token_data.get("status") == "expired":
|
|
return "失效"
|
|
|
|
# 获取剩余次数
|
|
remaining_queries = token_data.get("remainingQueries", -1)
|
|
heavy_remaining = token_data.get("heavyremainingQueries", -1)
|
|
|
|
# 根据token类型选择正确的字段
|
|
if token_type == "ssoSuper":
|
|
# Super token 可能使用 heavy 模型
|
|
relevant_remaining = max(remaining_queries, heavy_remaining)
|
|
else:
|
|
# 普通token主要看 remaining_queries
|
|
relevant_remaining = remaining_queries
|
|
|
|
if relevant_remaining == -1:
|
|
return "未使用"
|
|
elif relevant_remaining == 0:
|
|
return "限流中"
|
|
else:
|
|
return "正常"
|
|
|
|
|
|
# === 页面路由 ===
|
|
|
|
@router.get("/login", response_class=HTMLResponse)
|
|
async def login_page():
|
|
"""登录页面"""
|
|
login_html = STATIC_DIR / "login.html"
|
|
if login_html.exists():
|
|
return login_html.read_text(encoding="utf-8")
|
|
raise HTTPException(status_code=404, detail="登录页面不存在")
|
|
|
|
|
|
@router.get("/manage", response_class=HTMLResponse)
|
|
async def manage_page():
|
|
"""管理页面"""
|
|
admin_html = STATIC_DIR / "admin.html"
|
|
if admin_html.exists():
|
|
return admin_html.read_text(encoding="utf-8")
|
|
raise HTTPException(status_code=404, detail="管理页面不存在")
|
|
|
|
|
|
# === API端点 ===
|
|
|
|
@router.post("/api/login", response_model=LoginResponse)
|
|
async def admin_login(request: LoginRequest) -> LoginResponse:
|
|
"""
|
|
管理员登录
|
|
|
|
验证用户名和密码,成功后返回会话token。
|
|
"""
|
|
try:
|
|
logger.debug(f"[Admin] 管理员登录尝试 - 用户名: {request.username}")
|
|
|
|
# 验证用户名和密码
|
|
expected_username = setting.global_config.get("admin_username", "")
|
|
expected_password = setting.global_config.get("admin_password", "")
|
|
|
|
if request.username != expected_username or request.password != expected_password:
|
|
logger.warning(f"[Admin] 登录失败: 用户名或密码错误 - 用户名: {request.username}")
|
|
return LoginResponse(
|
|
success=False,
|
|
message="用户名或密码错误"
|
|
)
|
|
|
|
# 生成会话token
|
|
session_token = secrets.token_urlsafe(32)
|
|
|
|
# 设置会话过期时间(24小时)
|
|
expire_time = datetime.now() + timedelta(hours=24)
|
|
_sessions[session_token] = expire_time
|
|
|
|
logger.debug(f"[Admin] 管理员登录成功 - 用户名: {request.username}")
|
|
|
|
return LoginResponse(
|
|
success=True,
|
|
token=session_token,
|
|
message="登录成功"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 登录处理异常 - 用户名: {request.username}, 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"登录失败: {str(e)}", "code": "LOGIN_ERROR"}
|
|
)
|
|
|
|
|
|
@router.post("/api/logout")
|
|
async def admin_logout(authenticated: bool = Depends(verify_admin_session),
|
|
authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
|
|
"""
|
|
管理员登出
|
|
|
|
清除会话token。
|
|
"""
|
|
try:
|
|
if authorization and authorization.startswith("Bearer "):
|
|
token = authorization[7:]
|
|
if token in _sessions:
|
|
del _sessions[token]
|
|
logger.debug("[Admin] 管理员登出成功")
|
|
return {"success": True, "message": "登出成功"}
|
|
|
|
logger.warning("[Admin] 登出失败: 无效或缺失的会话Token")
|
|
return {"success": False, "message": "无效的会话"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 登出处理异常 - 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"登出失败: {str(e)}", "code": "LOGOUT_ERROR"}
|
|
)
|
|
|
|
|
|
@router.get("/api/tokens", response_model=TokenListResponse)
|
|
async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> TokenListResponse:
|
|
"""
|
|
获取所有Token列表
|
|
|
|
返回系统中所有Token及其状态信息。
|
|
"""
|
|
try:
|
|
logger.debug("[Admin] 开始获取Token列表")
|
|
|
|
all_tokens_data = token_manager.get_all_token()
|
|
token_list: List[TokenInfo] = []
|
|
|
|
# 处理普通Token
|
|
normal_tokens = all_tokens_data.get(TokenType.NORMAL.value, {})
|
|
for token, data in normal_tokens.items():
|
|
token_list.append(TokenInfo(
|
|
token=token,
|
|
token_type="sso",
|
|
created_time=parse_created_time(data.get("createdTime")),
|
|
remaining_queries=data.get("remainingQueries", -1),
|
|
heavy_remaining_queries=data.get("heavyremainingQueries", -1),
|
|
status=get_token_status(data, "sso")
|
|
))
|
|
|
|
# 处理Super Token
|
|
super_tokens = all_tokens_data.get(TokenType.SUPER.value, {})
|
|
for token, data in super_tokens.items():
|
|
token_list.append(TokenInfo(
|
|
token=token,
|
|
token_type="ssoSuper",
|
|
created_time=parse_created_time(data.get("createdTime")),
|
|
remaining_queries=data.get("remainingQueries", -1),
|
|
heavy_remaining_queries=data.get("heavyremainingQueries", -1),
|
|
status=get_token_status(data, "ssoSuper")
|
|
))
|
|
|
|
normal_count = len(normal_tokens)
|
|
super_count = len(super_tokens)
|
|
total_count = len(token_list)
|
|
|
|
logger.debug(f"[Admin] Token列表获取成功 - 普通Token: {normal_count}, Super Token: {super_count}, 总计: {total_count}")
|
|
|
|
return TokenListResponse(
|
|
success=True,
|
|
data=token_list,
|
|
total=total_count
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 获取Token列表异常 - 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"获取Token列表失败: {str(e)}", "code": "LIST_ERROR"}
|
|
)
|
|
|
|
|
|
@router.post("/api/tokens/add")
|
|
async def add_tokens(request: AddTokensRequest,
|
|
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""
|
|
批量添加Token
|
|
|
|
支持添加普通Token(sso)和Super Token(ssoSuper)。
|
|
"""
|
|
try:
|
|
logger.debug(f"[Admin] 批量添加Token - 类型: {request.token_type}, 数量: {len(request.tokens)}")
|
|
|
|
# 验证并转换token类型
|
|
token_type = validate_token_type(request.token_type)
|
|
|
|
# 添加Token
|
|
token_manager.add_token(request.tokens, token_type)
|
|
|
|
logger.debug(f"[Admin] Token添加成功 - 类型: {request.token_type}, 数量: {len(request.tokens)}")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"成功添加 {len(request.tokens)} 个Token",
|
|
"count": len(request.tokens)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[Admin] Token添加异常 - 类型: {request.token_type}, 数量: {len(request.tokens)}, 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"添加Token失败: {str(e)}", "code": "ADD_ERROR"}
|
|
)
|
|
|
|
|
|
@router.post("/api/tokens/delete")
|
|
async def delete_tokens(request: DeleteTokensRequest,
|
|
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""
|
|
批量删除Token
|
|
|
|
支持删除普通Token(sso)和Super Token(ssoSuper)。
|
|
"""
|
|
try:
|
|
logger.debug(f"[Admin] 批量删除Token - 类型: {request.token_type}, 数量: {len(request.tokens)}")
|
|
|
|
# 验证并转换token类型
|
|
token_type = validate_token_type(request.token_type)
|
|
|
|
# 删除Token
|
|
token_manager.delete_token(request.tokens, token_type)
|
|
|
|
logger.debug(f"[Admin] Token删除成功 - 类型: {request.token_type}, 数量: {len(request.tokens)}")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"成功删除 {len(request.tokens)} 个Token",
|
|
"count": len(request.tokens)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[Admin] Token删除异常 - 类型: {request.token_type}, 数量: {len(request.tokens)}, 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"删除Token失败: {str(e)}", "code": "DELETE_ERROR"}
|
|
)
|
|
|
|
|
|
@router.get("/api/settings")
|
|
async def get_settings(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""获取全局配置"""
|
|
try:
|
|
logger.debug("[Admin] 获取全局配置")
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"global": setting.global_config,
|
|
"grok": setting.grok_config
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 获取配置失败: {str(e)}")
|
|
raise HTTPException(status_code=500, detail={"error": f"获取配置失败: {str(e)}", "code": "GET_SETTINGS_ERROR"})
|
|
|
|
|
|
class UpdateSettingsRequest(BaseModel):
|
|
"""更新配置请求"""
|
|
global_config: Optional[Dict[str, Any]] = None
|
|
grok_config: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@router.post("/api/settings")
|
|
async def update_settings(request: UpdateSettingsRequest, authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""更新全局配置"""
|
|
try:
|
|
import toml
|
|
logger.debug("[Admin] 更新全局配置")
|
|
|
|
# 读取现有配置
|
|
with open(setting.config_path, "r", encoding="utf-8") as f:
|
|
config = toml.load(f)
|
|
|
|
# 更新配置
|
|
if request.global_config:
|
|
config["global"].update(request.global_config)
|
|
if request.grok_config:
|
|
config["grok"].update(request.grok_config)
|
|
|
|
# 写回配置文件
|
|
with open(setting.config_path, "w", encoding="utf-8") as f:
|
|
toml.dump(config, f)
|
|
|
|
# 重新加载配置
|
|
setting.global_config = setting.load("global")
|
|
setting.grok_config = setting.load("grok")
|
|
|
|
logger.debug("[Admin] 配置更新成功")
|
|
return {"success": True, "message": "配置更新成功"}
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 更新配置失败: {str(e)}")
|
|
raise HTTPException(status_code=500, detail={"error": f"更新配置失败: {str(e)}", "code": "UPDATE_SETTINGS_ERROR"})
|
|
|
|
|
|
@router.get("/api/cache/size")
|
|
async def get_cache_size(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""
|
|
获取缓存大小
|
|
|
|
计算/data/temp目录中所有文件的大小。
|
|
"""
|
|
try:
|
|
logger.debug("[Admin] 开始获取缓存大小")
|
|
|
|
# 获取temp目录路径
|
|
temp_dir = Path(__file__).parents[3] / "data" / "temp"
|
|
|
|
if not temp_dir.exists():
|
|
logger.warning(f"[Admin] 缓存目录不存在: {temp_dir}")
|
|
return {
|
|
"success": True,
|
|
"data": {"size": "0 MB"}
|
|
}
|
|
|
|
# 计算目录大小
|
|
total_size = 0
|
|
for file_path in temp_dir.iterdir():
|
|
if file_path.is_file():
|
|
try:
|
|
total_size += file_path.stat().st_size
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 获取文件大小失败: {file_path.name}, 错误: {str(e)}")
|
|
|
|
# 转换为MB并格式化
|
|
size_mb = total_size / (1024 * 1024)
|
|
if size_mb < 1:
|
|
size_kb = total_size / 1024
|
|
size_str = f"{size_kb:.1f} KB"
|
|
else:
|
|
size_str = f"{size_mb:.1f} MB"
|
|
|
|
logger.debug(f"[Admin] 缓存大小获取完成 - 大小: {size_str}")
|
|
return {
|
|
"success": True,
|
|
"data": {"size": size_str}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 获取缓存大小异常 - 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"获取缓存大小失败: {str(e)}", "code": "CACHE_SIZE_ERROR"}
|
|
)
|
|
|
|
|
|
@router.post("/api/cache/clear")
|
|
async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""
|
|
清理缓存
|
|
|
|
删除/data/temp目录中的所有文件。
|
|
"""
|
|
try:
|
|
logger.debug("[Admin] 开始清理缓存")
|
|
|
|
# 获取temp目录路径
|
|
temp_dir = Path(__file__).parents[3] / "data" / "temp"
|
|
|
|
if not temp_dir.exists():
|
|
logger.warning(f"[Admin] 缓存目录不存在: {temp_dir}")
|
|
return {
|
|
"success": True,
|
|
"message": "缓存目录不存在,无需清理",
|
|
"data": {"deleted_count": 0}
|
|
}
|
|
|
|
# 获取所有文件
|
|
deleted_count = 0
|
|
for file_path in temp_dir.iterdir():
|
|
if file_path.is_file():
|
|
try:
|
|
file_path.unlink()
|
|
deleted_count += 1
|
|
logger.debug(f"[Admin] 删除缓存文件: {file_path.name}")
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 删除缓存文件失败: {file_path.name}, 错误: {str(e)}")
|
|
|
|
logger.debug(f"[Admin] 缓存清理完成 - 删除文件数量: {deleted_count}")
|
|
return {
|
|
"success": True,
|
|
"message": f"成功清理缓存,删除 {deleted_count} 个文件",
|
|
"data": {"deleted_count": deleted_count}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 清理缓存异常 - 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"清理缓存失败: {str(e)}", "code": "CACHE_CLEAR_ERROR"}
|
|
)
|
|
|
|
|
|
@router.get("/api/stats")
|
|
async def get_stats(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
|
"""
|
|
获取统计信息
|
|
|
|
返回Token的统计数据。
|
|
"""
|
|
try:
|
|
logger.debug("[Admin] 开始获取统计信息")
|
|
|
|
all_tokens_data = token_manager.get_all_token()
|
|
|
|
# 统计普通Token
|
|
normal_tokens = all_tokens_data.get(TokenType.NORMAL.value, {})
|
|
normal_stats = calculate_token_stats(normal_tokens, "normal")
|
|
|
|
# 统计Super Token
|
|
super_tokens = all_tokens_data.get(TokenType.SUPER.value, {})
|
|
super_stats = calculate_token_stats(super_tokens, "super")
|
|
|
|
total_count = normal_stats["total"] + super_stats["total"]
|
|
|
|
stats = {
|
|
"success": True,
|
|
"data": {
|
|
"normal": normal_stats,
|
|
"super": super_stats,
|
|
"total": total_count
|
|
}
|
|
}
|
|
|
|
logger.debug(f"[Admin] 统计信息获取成功 - 普通Token: {normal_stats['total']}, Super Token: {super_stats['total']}, 总计: {total_count}")
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Admin] 获取统计信息异常 - 错误: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": f"获取统计信息失败: {str(e)}", "code": "STATS_ERROR"}
|
|
)
|