Files
gemini-balance/app/router/config_routes.py
snaily 920228d3aa feat: 实现API密钥的单独和批量删除功能
本次更新引入了删除API密钥的功能,包括前端界面和后端逻辑。

主要变更:

- **API路由 (`app/router/config_routes.py`):**
    - 添加了新的API端点 `/keys/{key_to_delete}` 用于删除单个密钥。
    - 添加了新的API端点 `/keys/delete-selected` 用于批量删除选定的密钥。
    - 增加了对请求体 `DeleteKeysRequest` 的Pydantic模型定义。
    - 在删除操作前进行身份验证。

- **配置服务 (`app/service/config/config_service.py`):**
    - 实现了 `delete_key` 方法来处理单个密钥的删除逻辑。
    - 实现了 `delete_selected_keys` 方法来处理批量密钥的删除逻辑。
    - 确保在删除操作后更新配置。

- **密钥管理器 (`app/service/key/key_manager.py`):**
    - 更新了 `remove_key` 方法,以确保从活动密钥列表中正确移除密钥。
    - 改进了 `reset_instance` 方法,在重置时保留下一个密钥提示(`_preserved_next_key_in_cycle`),以防止在配置重载后立即丢失轮换状态。

- **前端JavaScript (`app/static/js/keys_status.js`):**
    - 添加了 `showSingleKeyDeleteConfirmModal` 函数,用于显示单个密钥删除的确认模态框。
    - 添加了 `executeSingleKeyDelete` 函数,用于执行单个密钥的删除请求。
    - 添加了 `showDeleteConfirmationModal` 函数,用于显示批量删除密钥的确认模态框。
    - 添加了 `executeDeleteSelectedKeys` 函数,用于执行批量删除密钥的请求。
    - 更新了UI交互,包括按钮状态(加载中、禁用)和结果通知。

- **HTML模板 (`app/templates/keys_status.html`):**
    - 为有效密钥和无效密钥列表中的每个密钥添加了“删除”按钮。
    - 为有效密钥和无效密钥列表添加了“批量删除”按钮。
    - 添加了用于单个密钥删除和批量删除的确认模态框HTML结构。
    - 调整了现有模态框的样式,以提高视觉一致性。

这些更改增强了密钥管理功能,允许用户更灵活地管理其API密钥。
2025-05-08 21:58:26 +08:00

143 lines
5.9 KiB
Python

"""
配置路由模块
"""
from typing import Any, Dict, List
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, Field
from app.core.security import verify_auth_token
from app.log.logger import Logger, get_config_routes_logger
from app.service.config.config_service import ConfigService
router = APIRouter(prefix="/api/config", tags=["config"])
logger = get_config_routes_logger()
@router.get("", response_model=Dict[str, Any])
async def get_config(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 config page")
return RedirectResponse(url="/", status_code=302)
return await ConfigService.get_config()
@router.put("", response_model=Dict[str, Any])
async def update_config(config_data: Dict[str, Any], 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 config page")
return RedirectResponse(url="/", status_code=302)
try:
result = await ConfigService.update_config(config_data)
# 配置更新成功后,立即更新所有 logger 的级别
Logger.update_log_levels(config_data["LOG_LEVEL"])
logger.info("Log levels updated after configuration change.")
return result
except Exception as e:
logger.error(f"Error updating config or log levels: {e}", exc_info=True)
raise HTTPException(status_code=400, detail=str(e))
@router.post("/reset", response_model=Dict[str, Any])
async def reset_config(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 config page")
return RedirectResponse(url="/", status_code=302)
try:
return await ConfigService.reset_config()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# Pydantic model for bulk delete request
class DeleteKeysRequest(BaseModel):
keys: List[str] = Field(..., description="List of API keys to delete")
@router.delete("/keys/{key_to_delete}", response_model=Dict[str, Any])
async def delete_single_key(key_to_delete: str, request: Request):
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning(f"Unauthorized attempt to delete key: {key_to_delete}")
return RedirectResponse(url="/", status_code=302)
try:
logger.info(f"Attempting to delete key: {key_to_delete}")
result = await ConfigService.delete_key(key_to_delete)
if not result.get("success"):
# Optionally, translate specific errors to HTTP status codes
# For now, let's assume 400 for any failure from service if not found,
# or 500 if it was an unexpected error (though service should handle that)
raise HTTPException(
status_code=(
404 if "not found" in result.get("message", "").lower() else 400
),
detail=result.get("message"),
)
return result
except HTTPException as e:
# Re-raise HTTPExceptions directly
raise e
except Exception as e:
logger.error(f"Error deleting key '{key_to_delete}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Error deleting key: {str(e)}")
@router.post("/keys/delete-selected", response_model=Dict[str, Any])
async def delete_selected_keys_route(
delete_request: DeleteKeysRequest, request: Request
):
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning("Unauthorized attempt to bulk delete keys")
return RedirectResponse(url="/", status_code=302)
if not delete_request.keys:
logger.warning("Attempt to bulk delete keys with an empty list.")
raise HTTPException(status_code=400, detail="No keys provided for deletion.")
try:
logger.info(f"Attempting to bulk delete {len(delete_request.keys)} keys.")
result = await ConfigService.delete_selected_keys(delete_request.keys)
# Similar to single delete, we can check result["success"]
if not result.get("success") and result.get("deleted_count", 0) == 0:
# If no keys were actually deleted, it might be a client error (e.g., all keys not found)
# or an empty list was somehow passed despite the check above.
raise HTTPException(
status_code=400, detail=result.get("message", "Failed to delete keys.")
)
# If some keys were deleted but others not found, it's still a partial success, return 200 with details.
return result
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Error bulk deleting keys: {e}", exc_info=True)
raise HTTPException(
status_code=500, detail=f"Error bulk deleting keys: {str(e)}"
)
@router.get("/ui/models")
async def get_ui_models(request: Request):
auth_token_cookie = request.cookies.get("auth_token")
if not auth_token_cookie or not verify_auth_token(auth_token_cookie):
logger.warning("Unauthorized access attempt to /api/config/ui/models")
raise HTTPException(status_code=403, detail="Not authenticated")
try:
models = await ConfigService.fetch_ui_models()
return models
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Unexpected error in /ui/models endpoint: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"An unexpected error occurred while fetching UI models: {str(e)}",
)