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 %}
+
+