mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-06 20:32:47 +08:00
- 添加API密钥分页显示功能,每页显示20个密钥
- 实现分页控件和搜索功能的集成 - 优化API密钥的数据处理逻辑,从DOM操作改为数组操作 - 修改登录成功后重定向路径从/config改为/keys - 重构routes.py的import语句,按字母顺序排列 - 改进代码格式和缩进风格
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
'<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
|
||||
@@ -961,6 +961,31 @@ endblock %} {% block head_extra_styles %}
|
||||
<div class="array-container" id="API_KEYS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<!-- API密钥分页控件 -->
|
||||
<div id="apiKeyPagination" class="flex items-center justify-between mt-2 mb-2" style="display: none;">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
id="apiKeyPrevBtn"
|
||||
onclick="prevApiKeyPage()"
|
||||
class="px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
|
||||
>
|
||||
<i class="fas fa-chevron-left"></i> 上一页
|
||||
</button>
|
||||
<span id="apiKeyPageInfo" class="text-sm text-gray-600">第 1 页,共 1 页</span>
|
||||
<button
|
||||
type="button"
|
||||
id="apiKeyNextBtn"
|
||||
onclick="nextApiKeyPage()"
|
||||
class="px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
|
||||
>
|
||||
下一页 <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
每页显示 20 个密钥
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user