diff --git a/app/router/routes.py b/app/router/routes.py index ae319f3..990a4f7 100644 --- a/app/router/routes.py +++ b/app/router/routes.py @@ -6,10 +6,22 @@ from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates -from app.core.security import verify_auth_token from app.config.config import settings +from app.core.security import verify_auth_token from app.log.logger import get_routes_logger -from app.router import error_log_routes, gemini_routes, openai_routes, config_routes, scheduler_routes, stats_routes, version_routes, openai_compatiable_routes, vertex_express_routes, files_routes, key_routes +from app.router import ( + config_routes, + error_log_routes, + files_routes, + gemini_routes, + key_routes, + openai_compatiable_routes, + openai_routes, + scheduler_routes, + stats_routes, + version_routes, + vertex_express_routes, +) from app.service.key.key_manager import get_key_manager_instance from app.service.stats.stats_service import StatsService @@ -69,9 +81,12 @@ def setup_page_routes(app: FastAPI) -> None: if verify_auth_token(auth_token): logger.info("Successful authentication") - response = RedirectResponse(url="/config", status_code=302) + response = RedirectResponse(url="/keys", status_code=302) response.set_cookie( - key="auth_token", value=auth_token, httponly=True, max_age=settings.ADMIN_SESSION_EXPIRE + key="auth_token", + value=auth_token, + httponly=True, + max_age=settings.ADMIN_SESSION_EXPIRE, ) return response logger.warning("Failed authentication attempt with invalid token") @@ -91,7 +106,9 @@ def setup_page_routes(app: FastAPI) -> None: key_manager = await get_key_manager_instance() keys_status = await key_manager.get_keys_by_status() - total_keys = len(keys_status["valid_keys"]) + len(keys_status["invalid_keys"]) + total_keys = len(keys_status["valid_keys"]) + len( + keys_status["invalid_keys"] + ) valid_key_count = len(keys_status["valid_keys"]) invalid_key_count = len(keys_status["invalid_keys"]) @@ -133,7 +150,7 @@ def setup_page_routes(app: FastAPI) -> None: }, }, ) - + @app.get("/config", response_class=HTMLResponse) async def config_page(request: Request): """配置编辑页面""" @@ -142,13 +159,15 @@ def setup_page_routes(app: FastAPI) -> None: 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) - + logger.info("Config page accessed successfully") - return templates.TemplateResponse("config_editor.html", {"request": request}) + return templates.TemplateResponse( + "config_editor.html", {"request": request} + ) except Exception as e: logger.error(f"Error accessing config page: {str(e)}") raise - + @app.get("/logs", response_class=HTMLResponse) async def logs_page(request: Request): """错误日志页面""" @@ -157,7 +176,7 @@ def setup_page_routes(app: FastAPI) -> None: if not auth_token or not verify_auth_token(auth_token): logger.warning("Unauthorized access attempt to logs page") return RedirectResponse(url="/", status_code=302) - + logger.info("Logs page accessed successfully") return templates.TemplateResponse("error_logs.html", {"request": request}) except Exception as e: @@ -187,6 +206,7 @@ def setup_api_stats_routes(app: FastAPI) -> None: Args: app: FastAPI应用程序实例 """ + @app.get("/api/stats/details") async def api_stats_details(request: Request, period: str): """获取指定时间段内的 API 调用详情""" @@ -201,8 +221,12 @@ def setup_api_stats_routes(app: FastAPI) -> None: details = await stats_service.get_api_call_details(period) return details except ValueError as e: - logger.warning(f"Invalid period requested for API stats details: {period} - {str(e)}") + logger.warning( + f"Invalid period requested for API stats details: {period} - {str(e)}" + ) return {"error": str(e)}, 400 except Exception as e: - logger.error(f"Error fetching API stats details for period {period}: {str(e)}") + logger.error( + f"Error fetching API stats details for period {period}: {str(e)}" + ) return {"error": "Internal server error"}, 500 diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 938406f..54e3432 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -16,6 +16,13 @@ const PROXY_REGEX = const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_\-]{50}/g; // 新增 Vertex Express API Key 正则 const MASKED_VALUE = "••••••••"; +// API Keys Pagination Constants +const API_KEYS_PER_PAGE = 20; // 每页显示的API密钥数量 +let currentApiKeyPage = 1; +let totalApiKeyPages = 1; +let allApiKeys = []; // 存储所有API密钥数据 +let filteredApiKeys = []; // 存储过滤后的API密钥数据 + // DOM Elements - Global Scope for frequently accessed elements const safetySettingsContainer = document.getElementById( "SAFETY_SETTINGS_container" @@ -147,6 +154,17 @@ document.addEventListener("DOMContentLoaded", function () { if (apiKeySearchInput) apiKeySearchInput.addEventListener("input", handleApiKeySearch); + // API Key Pagination Event Listeners + const apiKeyPrevBtn = document.getElementById("apiKeyPrevBtn"); + const apiKeyNextBtn = document.getElementById("apiKeyNextBtn"); + + if (apiKeyPrevBtn) { + apiKeyPrevBtn.addEventListener("click", prevApiKeyPage); + } + if (apiKeyNextBtn) { + apiKeyNextBtn.addEventListener("click", nextApiKeyPage); + } + // Bulk Delete API Key Modal Elements and Events const bulkDeleteApiKeyBtn = document.getElementById("bulkDeleteApiKeyBtn"); const closeBulkDeleteModalBtn = document.getElementById( @@ -924,9 +942,9 @@ function populateForm(config) { '
添加自定义请求头,例如 X-Api-Key: your-key
'; } - // 4. Populate other array fields (excluding THINKING_MODELS) + // 4. Populate other array fields (excluding THINKING_MODELS and API_KEYS) for (const [key, value] of Object.entries(config)) { - if (Array.isArray(value) && key !== "THINKING_MODELS") { + if (Array.isArray(value) && key !== "THINKING_MODELS" && key !== "API_KEYS") { const container = document.getElementById(`${key}_container`); if (container) { value.forEach((itemValue) => { @@ -940,6 +958,17 @@ function populateForm(config) { } } + // 4.1. 特殊处理API_KEYS - 使用分页 + if (Array.isArray(config.API_KEYS)) { + allApiKeys = config.API_KEYS.filter(key => + typeof key === "string" && key.trim() !== "" + ); + filteredApiKeys = [...allApiKeys]; + currentApiKeyPage = 1; + renderApiKeyPage(); + updateApiKeyPagination(); + } + // 5. Populate non-array/non-budget fields for (const [key, value] of Object.entries(config)) { if ( @@ -1062,44 +1091,31 @@ function populateForm(config) { * Handles the bulk addition of API keys from the modal input. */ function handleBulkAddApiKeys() { - const apiKeyContainer = document.getElementById("API_KEYS_container"); - if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return; + if (!apiKeyBulkInput || !apiKeyModal) return; const bulkText = apiKeyBulkInput.value; const extractedKeys = bulkText.match(API_KEY_REGEX) || []; - const currentKeyInputs = apiKeyContainer.querySelectorAll( - `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` - ); - let currentKeys = Array.from(currentKeyInputs) - .map((input) => { - return input.hasAttribute("data-real-value") - ? input.getAttribute("data-real-value") - : input.value; - }) - .filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE); - - const combinedKeys = new Set([...currentKeys, ...extractedKeys]); + // 合并现有密钥和新密钥,去重 + const combinedKeys = new Set([...allApiKeys, ...extractedKeys]); const uniqueKeys = Array.from(combinedKeys); - apiKeyContainer.innerHTML = ""; // Clear existing items more directly + // 更新全局密钥数组 + allApiKeys = uniqueKeys; + + // 更新过滤后的数组 + const searchTerm = apiKeySearchInput ? apiKeySearchInput.value.toLowerCase() : ""; + if (!searchTerm) { + filteredApiKeys = [...allApiKeys]; + } else { + filteredApiKeys = allApiKeys.filter(key => + key.toLowerCase().includes(searchTerm) + ); + } - uniqueKeys.forEach((key) => { - addArrayItemWithValue("API_KEYS", key); - }); - - const newKeyInputs = apiKeyContainer.querySelectorAll( - `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` - ); - newKeyInputs.forEach((input) => { - if (configForm && typeof initializeSensitiveFields === "function") { - const focusoutEvent = new Event("focusout", { - bubbles: true, - cancelable: true, - }); - input.dispatchEvent(focusoutEvent); - } - }); + // 重新渲染当前页 + renderApiKeyPage(); + updateApiKeyPagination(); closeModal(apiKeyModal); showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, "success"); @@ -1109,32 +1125,139 @@ function handleBulkAddApiKeys() { * Handles searching/filtering of API keys in the list. */ function handleApiKeySearch() { - const apiKeyContainer = document.getElementById("API_KEYS_container"); - if (!apiKeySearchInput || !apiKeyContainer) return; + if (!apiKeySearchInput) return; const searchTerm = apiKeySearchInput.value.toLowerCase(); - const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); - - keyItems.forEach((item) => { - const input = item.querySelector( - `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` + + // 过滤API密钥 + if (!searchTerm) { + filteredApiKeys = [...allApiKeys]; + } else { + filteredApiKeys = allApiKeys.filter(key => + key.toLowerCase().includes(searchTerm) ); - if (input) { - const realValue = input.hasAttribute("data-real-value") - ? input.getAttribute("data-real-value").toLowerCase() - : input.value.toLowerCase(); - item.style.display = realValue.includes(searchTerm) ? "flex" : "none"; - } + } + + // 重置到第一页 + currentApiKeyPage = 1; + + // 重新渲染当前页 + renderApiKeyPage(); + updateApiKeyPagination(); +} + +/** + * 渲染当前页的API密钥 + */ +function renderApiKeyPage() { + const apiKeyContainer = document.getElementById("API_KEYS_container"); + if (!apiKeyContainer) return; + + // 清空容器 + apiKeyContainer.innerHTML = ""; + + // 计算当前页的数据范围 + const startIndex = (currentApiKeyPage - 1) * API_KEYS_PER_PAGE; + const endIndex = Math.min(startIndex + API_KEYS_PER_PAGE, filteredApiKeys.length); + const pageKeys = filteredApiKeys.slice(startIndex, endIndex); + + // 渲染当前页的密钥 + pageKeys.forEach((key) => { + addArrayItemWithValue("API_KEYS", key); }); + + // 如果没有密钥,显示提示信息 + if (pageKeys.length === 0) { + const emptyMessage = document.createElement("div"); + emptyMessage.className = "text-gray-500 text-sm italic text-center py-4"; + emptyMessage.textContent = filteredApiKeys.length === 0 ? + (allApiKeys.length === 0 ? "暂无API密钥" : "未找到匹配的密钥") : + "当前页无数据"; + apiKeyContainer.appendChild(emptyMessage); + } +} + +/** + * 更新分页控件 + */ +function updateApiKeyPagination() { + totalApiKeyPages = Math.max(1, Math.ceil(filteredApiKeys.length / API_KEYS_PER_PAGE)); + + // 确保当前页在有效范围内 + if (currentApiKeyPage > totalApiKeyPages) { + currentApiKeyPage = totalApiKeyPages; + } + + const paginationContainer = document.getElementById("apiKeyPagination"); + if (!paginationContainer) return; + + // 如果只有一页或没有数据,隐藏分页控件 + if (totalApiKeyPages <= 1) { + paginationContainer.style.display = "none"; + return; + } + + paginationContainer.style.display = "flex"; + + // 更新页码信息 + const pageInfo = document.getElementById("apiKeyPageInfo"); + if (pageInfo) { + pageInfo.textContent = `第 ${currentApiKeyPage} 页,共 ${totalApiKeyPages} 页 (${filteredApiKeys.length} 个密钥)`; + } + + // 更新按钮状态 + const prevBtn = document.getElementById("apiKeyPrevBtn"); + const nextBtn = document.getElementById("apiKeyNextBtn"); + + if (prevBtn) { + prevBtn.disabled = currentApiKeyPage <= 1; + prevBtn.className = currentApiKeyPage <= 1 ? + "px-3 py-1 rounded bg-gray-300 text-gray-500 cursor-not-allowed" : + "px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"; + } + + if (nextBtn) { + nextBtn.disabled = currentApiKeyPage >= totalApiKeyPages; + nextBtn.className = currentApiKeyPage >= totalApiKeyPages ? + "px-3 py-1 rounded bg-gray-300 text-gray-500 cursor-not-allowed" : + "px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"; + } +} + +/** + * 跳转到指定页 + */ +function goToApiKeyPage(page) { + if (page < 1 || page > totalApiKeyPages) return; + + currentApiKeyPage = page; + renderApiKeyPage(); + updateApiKeyPagination(); +} + +/** + * 上一页 + */ +function prevApiKeyPage() { + if (currentApiKeyPage > 1) { + goToApiKeyPage(currentApiKeyPage - 1); + } +} + +/** + * 下一页 + */ +function nextApiKeyPage() { + if (currentApiKeyPage < totalApiKeyPages) { + goToApiKeyPage(currentApiKeyPage + 1); + } } /** * Handles the bulk deletion of API keys based on input from the modal. */ function handleBulkDeleteApiKeys() { - const apiKeyContainer = document.getElementById("API_KEYS_container"); - if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal) - return; + if (!bulkDeleteApiKeyInput || !bulkDeleteApiKeyModal) return; const bulkText = bulkDeleteApiKeyInput.value; if (!bulkText.trim()) { @@ -1149,24 +1272,30 @@ function handleBulkDeleteApiKeys() { return; } - const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); + // 从allApiKeys数组中删除匹配的密钥 let deleteCount = 0; - - keyItems.forEach((item) => { - const input = item.querySelector( - `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` - ); - const realValue = - input && - (input.hasAttribute("data-real-value") - ? input.getAttribute("data-real-value") - : input.value); - if (realValue && keysToDelete.has(realValue)) { - item.remove(); + allApiKeys = allApiKeys.filter(key => { + if (keysToDelete.has(key)) { deleteCount++; + return false; } + return true; }); + // 更新过滤后的数组 + const searchTerm = apiKeySearchInput ? apiKeySearchInput.value.toLowerCase() : ""; + if (!searchTerm) { + filteredApiKeys = [...allApiKeys]; + } else { + filteredApiKeys = allApiKeys.filter(key => + key.toLowerCase().includes(searchTerm) + ); + } + + // 重新渲染当前页 + renderApiKeyPage(); + updateApiKeyPagination(); + closeModal(bulkDeleteApiKeyModal); if (deleteCount > 0) { @@ -1782,6 +1911,15 @@ function collectFormData() { const arrayContainers = document.querySelectorAll(".array-container"); arrayContainers.forEach((container) => { const key = container.id.replace("_container", ""); + + // 特殊处理API_KEYS - 使用全局数组而不是DOM元素 + if (key === "API_KEYS") { + formData[key] = allApiKeys.filter( + (value) => value && value.trim() !== "" && value !== MASKED_VALUE + ); + return; + } + const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`); formData[key] = Array.from(arrayInputs) .map((input) => { diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index 113a216..61136a8 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -961,6 +961,31 @@ endblock %} {% block head_extra_styles %}
+ +