feat: 添加密钥检查调度器并重构前端UI

主要变更:

- **调度器功能:**
    - 集成 APScheduler 实现定时任务,用于定期检查API密钥的有效性。
    - 在 `.env.example` 和 `app/config/config.py` 中添加了 `CHECK_INTERVAL_HOURS` 和 `TIMEZONE` 配置项。
    - 在应用生命周期 (`app/core/application.py`) 中添加了调度器的启动和停止逻辑。
    - 新增 `app/scheduler/` 目录及相关实现 (`key_checker.py`)。
    - 新增 `app/router/scheduler_routes.py` 用于调度器相关API (如果未来需要)。
    - 在 `requirements.txt` 中添加 `apscheduler` 依赖。

- **前端重构与改进:**
    - 引入 `app/templates/base.html` 作为基础模板,统一页面结构和样式引入。
    - 使用新的样式(推测为Tailwind CSS)重构了 `auth.html`, `config_editor.html`, `error_logs.html`, `keys_status.html` 页面,提升了UI一致性和响应式布局。
    - 删除了旧的CSS文件 (`auth.css`, `config_editor.css`, `error_logs.css`, `keys_status.css`)。
    - 更新了对应的 JavaScript 文件 (`config_editor.js`, `error_logs.js`, `keys_status.js`) 以适应新的HTML结构和交互。
    - 在 `keys_status.html` 页面增加了按失败次数过滤密钥、批量重置失败次数、确认模态框等功能。
    - 添加了新的 Logo 图片 (`logo.png`, `logo1.png`)。

- **其他:**
    - 更新了 `app/router/routes.py` 以包含新的路由。
    - 对 `app/service/key/key_manager.py` 和 `app/database/services.py` 进行了相关调整以支持新功能。
```
This commit is contained in:
snaily
2025-04-11 03:16:51 +08:00
parent 69261e98de
commit 51bb71bdb5
25 changed files with 2090 additions and 2776 deletions
+63 -5
View File
@@ -146,15 +146,67 @@ async def stream_generate_content(
logger.error(f"Streaming request failed: {str(e)}")
raise HTTPException(status_code=500, detail="Streaming request failed") from e
@router.post("/reset-all-fail-counts")
async def reset_all_key_fail_counts(key_type: str = None, key_manager: KeyManager = Depends(get_key_manager)):
"""批量重置Gemini API密钥的失败计数,可选择性地仅重置有效或无效密钥"""
logger.info("-" * 50 + "reset_all_gemini_key_fail_counts" + "-" * 50)
logger.info(f"Received reset request with key_type: {key_type}")
try:
# 获取分类后的密钥
keys_by_status = await key_manager.get_keys_by_status()
valid_keys = keys_by_status.get("valid_keys", {})
invalid_keys = keys_by_status.get("invalid_keys", {})
# 根据类型选择要重置的密钥
keys_to_reset = []
if key_type == "valid":
keys_to_reset = list(valid_keys.keys())
logger.info(f"Resetting only valid keys, count: {len(keys_to_reset)}")
elif key_type == "invalid":
keys_to_reset = list(invalid_keys.keys())
logger.info(f"Resetting only invalid keys, count: {len(keys_to_reset)}")
else:
# 重置所有密钥
await key_manager.reset_failure_counts()
return JSONResponse({"success": True, "message": "所有密钥的失败计数已重置"})
# 批量重置指定类型的密钥
for key in keys_to_reset:
await key_manager.reset_key_failure_count(key)
return JSONResponse({
"success": True,
"message": f"{key_type}密钥的失败计数已重置",
"reset_count": len(keys_to_reset)
})
except Exception as e:
logger.error(f"Failed to reset key failure counts: {str(e)}")
return JSONResponse({"success": False, "message": f"批量重置失败: {str(e)}"}, status_code=500)
@router.post("/reset-fail-count/{api_key}")
async def reset_key_fail_count(api_key: str, key_manager: KeyManager = Depends(get_key_manager)):
"""重置指定Gemini API密钥的失败计数"""
logger.info("-" * 50 + "reset_gemini_key_fail_count" + "-" * 50)
logger.info(f"Resetting failure count for API key: {api_key}")
try:
result = await key_manager.reset_key_failure_count(api_key)
if result:
return JSONResponse({"success": True, "message": "失败计数已重置"})
return JSONResponse({"success": False, "message": "未找到指定密钥"}, status_code=404)
except Exception as e:
logger.error(f"Failed to reset key failure count: {str(e)}")
return JSONResponse({"success": False, "message": f"重置失败: {str(e)}"}, status_code=500)
@router.post("/verify-key/{api_key}")
async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get_chat_service)):
async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get_chat_service), key_manager: KeyManager = Depends(get_key_manager)):
"""验证Gemini API密钥的有效性"""
logger.info("-" * 50 + "verify_gemini_key" + "-" * 50)
logger.info("Verifying API key validity")
try:
# 使用generate_content接口测试key的有效性
gemini_request = GeminiRequest(
contents=[
@@ -167,13 +219,19 @@ async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get
response = await chat_service.generate_content(
settings.TEST_MODEL,
gemini_request,
gemini_request,
api_key
)
if response:
return JSONResponse({"status": "valid"})
return JSONResponse({"status": "invalid"})
return JSONResponse({"status": "valid"})
except Exception as e:
logger.error(f"Key verification failed: {str(e)}")
# 验证出现异常时增加失败计数
async with key_manager.failure_count_lock:
if api_key in key_manager.key_failure_counts:
key_manager.key_failure_counts[api_key] += 1
logger.warning(f"Verification exception for key: {api_key}, incrementing failure count")
return JSONResponse({"status": "invalid", "error": str(e)})
+3 -3
View File
@@ -2,7 +2,7 @@
日志路由模块
"""
from typing import Any, Dict, List, Optional
from datetime import date
from datetime import datetime
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, Request, Query
from fastapi.responses import RedirectResponse
@@ -29,8 +29,8 @@ async def get_error_logs_api(
offset: int = Query(0, ge=0),
key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"),
error_search: Optional[str] = Query(None, description="Search term for error type or log message"),
start_date: Optional[date] = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: Optional[date] = Query(None, description="End date for filtering (YYYY-MM-DD)")
start_date: Optional[datetime] = Query(None, description="Start datetime for filtering (YYYY-MM-DDTHH:MM)"),
end_date: Optional[datetime] = Query(None, description="End datetime for filtering (YYYY-MM-DDTHH:MM)")
):
"""
获取错误日志
+2 -1
View File
@@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates
from app.core.security import verify_auth_token
from app.log.logger import get_routes_logger
from app.router import gemini_routes, openai_routes, config_routes, log_routes
from app.router import gemini_routes, openai_routes, config_routes, log_routes, scheduler_routes # 新增导入
from app.service.key.key_manager import get_key_manager_instance
logger = get_routes_logger()
@@ -30,6 +30,7 @@ def setup_routers(app: FastAPI) -> None:
app.include_router(gemini_routes.router_v1beta)
app.include_router(config_routes.router)
app.include_router(log_routes.router)
app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由
# 添加页面路由
setup_page_routes(app)
+63
View File
@@ -0,0 +1,63 @@
"""
定时任务控制路由模块
"""
from fastapi import APIRouter, Request, HTTPException, status # 移除 Depends, 添加 Request
from fastapi.responses import JSONResponse
from app.core.security import verify_auth_token # 导入 verify_auth_token
from app.scheduler.key_checker import start_scheduler, stop_scheduler
from app.log.logger import get_routes_logger # 使用路由日志记录器
logger = get_routes_logger()
router = APIRouter(
prefix="/api/scheduler",
tags=["Scheduler"]
# 移除全局依赖
)
# 认证检查的辅助函数
async def verify_token(request: Request):
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning("Unauthorized access attempt to scheduler API")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post("/start", summary="启动定时任务")
async def start_scheduler_endpoint(request: Request): # 添加 request 参数
"""Start the background scheduler task"""
"""
await verify_token(request) # 在函数开始处进行认证检查
"""
try:
logger.info("Received request to start scheduler.")
start_scheduler() # 调用 key_checker 中的函数
return JSONResponse(content={"message": "Scheduler started successfully."}, status_code=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error starting scheduler: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start scheduler: {str(e)}"
)
@router.post("/stop", summary="停止定时任务")
async def stop_scheduler_endpoint(request: Request): # 添加 request 参数
"""Stop the background scheduler task"""
"""
await verify_token(request) # 在函数开始处进行认证检查
"""
try:
logger.info("Received request to stop scheduler.")
stop_scheduler() # 调用 key_checker 中的函数
return JSONResponse(content={"message": "Scheduler stopped successfully."}, status_code=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error stopping scheduler: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to stop scheduler: {str(e)}"
)