mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-07-08 08:11:36 +08:00
feat(keys_status): 支持批量验证密钥与选定密钥失败计数重置,增强自动刷新
- 后端新增 ResetSelectedKeysRequest、VerifySelectedKeysRequest 数据模型及相关 API 路由,实现批量重置选定密钥失败计数功能 - 前端 keys_status.js/keys_status.html 新增批量验证按钮、批量验证弹窗及交互逻辑,支持对筛选后密钥进行批量验证 - 自动刷新功能支持开关,优化用户体验 - UI 细节优化,提升密钥管理便捷性
This commit is contained in:
@@ -40,3 +40,12 @@ class GeminiRequest(BaseModel):
|
||||
safetySettings: Optional[List[SafetySetting]] = None
|
||||
generationConfig: Optional[GenerationConfig] = None
|
||||
systemInstruction: Optional[SystemInstruction] = None
|
||||
|
||||
|
||||
class ResetSelectedKeysRequest(BaseModel):
|
||||
keys: List[str]
|
||||
key_type: str
|
||||
|
||||
|
||||
class VerifySelectedKeysRequest(BaseModel):
|
||||
keys: List[str]
|
||||
|
||||
@@ -4,7 +4,8 @@ from copy import deepcopy
|
||||
from app.config.config import settings
|
||||
from app.log.logger import get_gemini_logger
|
||||
from app.core.security import SecurityService
|
||||
from app.domain.gemini_models import GeminiContent, GeminiRequest
|
||||
import asyncio # 导入 asyncio
|
||||
from app.domain.gemini_models import GeminiContent, GeminiRequest, ResetSelectedKeysRequest, VerifySelectedKeysRequest # 添加导入
|
||||
from app.service.chat.gemini_chat_service import GeminiChatService
|
||||
from app.service.key.key_manager import KeyManager, get_key_manager_instance
|
||||
from app.service.model.model_service import ModelService
|
||||
@@ -183,6 +184,62 @@ async def reset_all_key_fail_counts(key_type: str = None, key_manager: KeyManage
|
||||
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-selected-fail-counts")
|
||||
async def reset_selected_key_fail_counts(
|
||||
request: ResetSelectedKeysRequest,
|
||||
key_manager: KeyManager = Depends(get_key_manager)
|
||||
):
|
||||
"""批量重置选定Gemini API密钥的失败计数"""
|
||||
logger.info("-" * 50 + "reset_selected_gemini_key_fail_counts" + "-" * 50)
|
||||
keys_to_reset = request.keys
|
||||
key_type = request.key_type # 获取类型用于日志记录和响应消息
|
||||
logger.info(f"Received reset request for {len(keys_to_reset)} selected {key_type} keys.")
|
||||
|
||||
if not keys_to_reset:
|
||||
return JSONResponse({"success": False, "message": "没有提供需要重置的密钥"}, status_code=400)
|
||||
|
||||
reset_count = 0
|
||||
errors = []
|
||||
|
||||
try:
|
||||
for key in keys_to_reset:
|
||||
try:
|
||||
result = await key_manager.reset_key_failure_count(key)
|
||||
if result:
|
||||
reset_count += 1
|
||||
else:
|
||||
# 记录未找到的密钥,但不视为致命错误
|
||||
logger.warning(f"Key not found during selective reset: {key}")
|
||||
except Exception as key_error:
|
||||
# 记录单个密钥重置时的错误
|
||||
logger.error(f"Error resetting key {key}: {str(key_error)}")
|
||||
errors.append(f"Key {key}: {str(key_error)}")
|
||||
|
||||
if errors:
|
||||
# 如果有错误,报告部分成功或完全失败
|
||||
error_message = f"批量重置完成,但出现错误: {'; '.join(errors)}"
|
||||
# 确定最终状态码和成功标志
|
||||
final_success = reset_count > 0
|
||||
status_code = 207 if final_success and errors else 500 # 207 Multi-Status if partially successful, 500 if completely failed
|
||||
return JSONResponse({
|
||||
"success": final_success,
|
||||
"message": error_message,
|
||||
"reset_count": reset_count
|
||||
}, status_code=status_code)
|
||||
|
||||
# 完全成功的情况
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": f"成功重置 {reset_count} 个选定 {key_type} 密钥的失败计数",
|
||||
"reset_count": reset_count
|
||||
})
|
||||
except Exception as e:
|
||||
# 捕获循环外的意外错误
|
||||
logger.error(f"Failed to process reset selected key failure counts request: {str(e)}")
|
||||
return JSONResponse({"success": False, "message": f"批量重置处理失败: {str(e)}"}, status_code=500)
|
||||
|
||||
|
||||
|
||||
@router.post("/reset-fail-count/{api_key}")
|
||||
@@ -234,4 +291,93 @@ async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get
|
||||
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)})
|
||||
return JSONResponse({"status": "invalid", "error": str(e)})
|
||||
|
||||
|
||||
@router.post("/verify-selected-keys")
|
||||
async def verify_selected_keys(
|
||||
request: VerifySelectedKeysRequest,
|
||||
chat_service: GeminiChatService = Depends(get_chat_service),
|
||||
key_manager: KeyManager = Depends(get_key_manager)
|
||||
):
|
||||
"""批量验证选定Gemini API密钥的有效性"""
|
||||
logger.info("-" * 50 + "verify_selected_gemini_keys" + "-" * 50)
|
||||
keys_to_verify = request.keys
|
||||
logger.info(f"Received verification request for {len(keys_to_verify)} selected keys.")
|
||||
|
||||
if not keys_to_verify:
|
||||
return JSONResponse({"success": False, "message": "没有提供需要验证的密钥"}, status_code=400)
|
||||
|
||||
valid_count = 0
|
||||
invalid_count = 0
|
||||
verification_errors = {} # 存储验证过程中的错误
|
||||
|
||||
async def _verify_single_key(api_key: str):
|
||||
"""内部函数,用于验证单个密钥并处理异常"""
|
||||
nonlocal valid_count, invalid_count # 允许修改外部计数器
|
||||
try:
|
||||
# 重用单密钥验证逻辑的核心部分
|
||||
gemini_request = GeminiRequest(
|
||||
contents=[GeminiContent(role="user", parts=[{"text": "hi"}])]
|
||||
)
|
||||
# 注意:这里直接调用 chat_service.generate_content,不依赖于 key_manager 获取密钥
|
||||
await chat_service.generate_content(
|
||||
settings.TEST_MODEL,
|
||||
gemini_request,
|
||||
api_key
|
||||
)
|
||||
# 如果上面没有抛出异常,则认为密钥有效
|
||||
valid_count += 1
|
||||
return api_key, "valid", None
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
logger.warning(f"Key verification failed for {api_key}: {error_message}")
|
||||
# 验证失败时增加失败计数 (使用与 /verify-key 一致的逻辑)
|
||||
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"Bulk verification exception for key: {api_key}, incrementing failure count")
|
||||
else:
|
||||
# 如果密钥不在计数中(可能刚添加或从未失败),初始化为1
|
||||
key_manager.key_failure_counts[api_key] = 1
|
||||
logger.warning(f"Bulk verification exception for key: {api_key}, initializing failure count to 1")
|
||||
invalid_count += 1
|
||||
return api_key, "invalid", error_message
|
||||
|
||||
# 并发执行所有密钥的验证
|
||||
tasks = [_verify_single_key(key) for key in keys_to_verify]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True) # return_exceptions=True 捕获任务本身的异常
|
||||
|
||||
# 处理并发执行的结果
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
# 捕获 asyncio.gather 可能遇到的异常(例如任务被取消)
|
||||
logger.error(f"An unexpected error occurred during bulk verification task: {result}")
|
||||
# 可以选择如何处理这种任务级别的错误,这里我们简单记录
|
||||
# 也可以将其计入 invalid_count 或单独记录
|
||||
elif result:
|
||||
key, status, error = result
|
||||
if status == "invalid" and error:
|
||||
verification_errors[key] = error # 记录具体的验证错误信息
|
||||
|
||||
logger.info(f"Bulk verification finished. Valid: {valid_count}, Invalid: {invalid_count}")
|
||||
|
||||
# 根据是否有错误决定最终消息和状态
|
||||
if verification_errors or valid_count + invalid_count != len(keys_to_verify): # 检查是否有错误或任务异常
|
||||
error_summary = "; ".join([f"{k}: {v}" for k, v in verification_errors.items()])
|
||||
message = f"批量验证完成,但出现问题。有效: {valid_count}, 无效: {invalid_count}。错误详情: {error_summary or '任务执行异常'}"
|
||||
return JSONResponse({
|
||||
"success": False, # 标记为失败,因为有错误
|
||||
"message": message,
|
||||
"valid_count": valid_count,
|
||||
"invalid_count": invalid_count,
|
||||
"errors": verification_errors
|
||||
}, status_code=207) # 207 Multi-Status 表示部分成功/失败
|
||||
else:
|
||||
# 完全成功
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": f"批量验证成功完成。有效: {valid_count}, 无效: {invalid_count}",
|
||||
"valid_count": valid_count,
|
||||
"invalid_count": invalid_count
|
||||
})
|
||||
@@ -52,18 +52,20 @@ function initStatItemAnimations() {
|
||||
}
|
||||
|
||||
function copyKeys(type) {
|
||||
const keys = Array.from(document.querySelectorAll(`#${type}Keys .key-text`)).map(span => span.dataset.fullKey);
|
||||
|
||||
// 选择对应区域内所有可见的 li 元素下的 key-text span
|
||||
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-text`);
|
||||
const keys = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
|
||||
|
||||
if (keys.length === 0) {
|
||||
showNotification('没有可复制的密钥', 'error');
|
||||
showNotification('没有可复制的筛选后密钥', 'warning'); // 修改提示信息
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const keysText = keys.join('\n');
|
||||
|
||||
|
||||
copyToClipboard(keysText)
|
||||
.then(() => {
|
||||
showNotification(`已成功复制${keys.length}个${type === 'valid' ? '有效' : '无效'}密钥`);
|
||||
showNotification(`已成功复制 ${keys.length} 个筛选后的${type === 'valid' ? '有效' : '无效'}密钥`); // 修改提示信息
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法复制文本: ', err);
|
||||
@@ -186,14 +188,35 @@ function showResetModal(type) {
|
||||
const titleElement = document.getElementById('resetModalTitle');
|
||||
const messageElement = document.getElementById('resetModalMessage');
|
||||
const confirmButton = document.getElementById('confirmResetBtn');
|
||||
|
||||
|
||||
// 获取当前筛选后可见的、且包含 data-fail-count 属性的密钥数量
|
||||
// 根据密钥类型选择合适的选择器
|
||||
let keySelector;
|
||||
if (type === 'valid') {
|
||||
// 对于有效密钥,可能需要基于失败次数筛选,保留 data-fail-count (虽然批量重置通常不需要筛选)
|
||||
// 如果批量重置有效密钥也应重置所有可见的,可以将此行改为下面 else 中的选择器
|
||||
keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"])`;
|
||||
} else {
|
||||
// 对于无效密钥,我们想要重置所有可见的无效密钥,不依赖 data-fail-count
|
||||
keySelector = `#${type}Keys li:not([style*="display: none"])`;
|
||||
}
|
||||
const visibleKeyItems = document.querySelectorAll(keySelector);
|
||||
const count = visibleKeyItems.length;
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = '批量重置失败次数';
|
||||
messageElement.textContent = `确定要批量重置${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`;
|
||||
|
||||
if (count > 0) {
|
||||
messageElement.textContent = `确定要批量重置筛选出的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`;
|
||||
confirmButton.disabled = false; // 确保按钮可用
|
||||
} else {
|
||||
messageElement.textContent = `当前没有筛选出可重置的${type === 'valid' ? '有效' : '无效'}密钥。`;
|
||||
confirmButton.disabled = true; // 没有可重置的密钥时禁用确认按钮
|
||||
}
|
||||
|
||||
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeResetAll(type);
|
||||
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
@@ -243,7 +266,17 @@ function showResultModal(success, message, autoReload = true) {
|
||||
}
|
||||
|
||||
// 设置消息
|
||||
messageElement.textContent = message;
|
||||
// 支持长文本和换行,内容插入到div而不是p
|
||||
if (typeof message === 'string') {
|
||||
// 如果内容包含换行或长文本,自动转为可滚动
|
||||
messageElement.textContent = '';
|
||||
messageElement.innerText = message;
|
||||
} else if (message instanceof Node) {
|
||||
messageElement.innerHTML = '';
|
||||
messageElement.appendChild(message);
|
||||
} else {
|
||||
messageElement.textContent = String(message);
|
||||
}
|
||||
|
||||
// 设置确认按钮点击事件
|
||||
confirmButton.onclick = () => closeResultModal(autoReload);
|
||||
@@ -256,54 +289,75 @@ async function executeResetAll(type) {
|
||||
try {
|
||||
// 关闭确认模态框
|
||||
closeResetModal();
|
||||
|
||||
// 使用data-reset-type属性直接找到对应的重置按钮
|
||||
|
||||
// 找到对应的重置按钮以显示加载状态
|
||||
const resetButton = document.querySelector(`button[data-reset-type="${type}"]`);
|
||||
|
||||
if (!resetButton) {
|
||||
// 如果找不到按钮,显示错误并返回
|
||||
showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 获取筛选后可见的密钥
|
||||
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-text`);
|
||||
const keysToReset = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
|
||||
|
||||
if (keysToReset.length === 0) {
|
||||
showNotification(`没有需要重置的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮并显示加载状态
|
||||
resetButton.disabled = true;
|
||||
const originalHtml = resetButton.innerHTML;
|
||||
resetButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
|
||||
|
||||
try {
|
||||
// 调用API,传递类型参数
|
||||
const response = await fetch(`/gemini/v1beta/reset-all-fail-counts?key_type=${type}`, {
|
||||
method: 'POST'
|
||||
// 调用新的后端 API 来重置选定的密钥
|
||||
const response = await fetch(`/gemini/v1beta/reset-selected-fail-counts`, { // 假设的新 API 端点
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ keys: keysToReset, key_type: type }) // 发送密钥列表和类型
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器返回错误: ${response.status}`);
|
||||
// 尝试解析错误信息
|
||||
let errorMsg = `服务器返回错误: ${response.status}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.message || errorMsg;
|
||||
} catch (e) {
|
||||
// 如果解析失败,使用原始错误信息
|
||||
}
|
||||
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 根据重置结果显示模态框
|
||||
if (data.success) {
|
||||
const message = data.reset_count ?
|
||||
`成功重置${data.reset_count}个${type === 'valid' ? '有效' : '无效'}密钥的失败次数` :
|
||||
'所有失败次数重置成功';
|
||||
showResultModal(true, message);
|
||||
const message = data.reset_count !== undefined ? // 检查 reset_count 是否存在
|
||||
`成功重置 ${data.reset_count} 个筛选后的${type === 'valid' ? '有效' : '无效'}密钥的失败次数` :
|
||||
`成功重置 ${keysToReset.length} 个筛选后的密钥`; // 如果后端没返回数量,使用前端计算的数量
|
||||
showResultModal(true, message); // 成功后刷新页面
|
||||
} else {
|
||||
const errorMsg = data.message || '批量重置失败';
|
||||
showResultModal(false, '批量重置失败: ' + errorMsg);
|
||||
// 失败后不自动刷新页面,让用户看到错误信息
|
||||
showResultModal(false, '批量重置失败: ' + errorMsg, false);
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('API请求失败:', fetchError);
|
||||
showResultModal(false, '批量重置请求失败: ' + fetchError.message);
|
||||
showResultModal(false, '批量重置请求失败: ' + fetchError.message, false); // 失败后不自动刷新
|
||||
} finally {
|
||||
// 立即恢复按钮状态
|
||||
// 恢复按钮状态
|
||||
resetButton.innerHTML = originalHtml;
|
||||
resetButton.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量重置失败:', error);
|
||||
showResultModal(false, '批量重置处理失败: ' + error.message);
|
||||
console.error('批量重置处理失败:', error);
|
||||
showResultModal(false, '批量重置处理失败: ' + error.message, false); // 失败后不自动刷新
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,6 +473,111 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 恢复为原始值,以确保准确性
|
||||
valueElement.textContent = valueElement.dataset.originalValue;
|
||||
}
|
||||
|
||||
|
||||
window.showVerifyModal = function(type, event) {
|
||||
// 阻止事件冒泡(如果从按钮点击触发)
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('verifyModal');
|
||||
const titleElement = document.getElementById('verifyModalTitle');
|
||||
const messageElement = document.getElementById('verifyModalMessage');
|
||||
const confirmButton = document.getElementById('confirmVerifyBtn');
|
||||
|
||||
// 获取当前筛选后可见的、且包含 data-fail-count 属性的密钥数量
|
||||
// 注意:对于验证,我们可能想验证所有筛选出的密钥,无论其 data-fail-count 如何,
|
||||
// 但为了与重置保持一致,并且通常只验证有效/无效列表中的项,我们保留 data-fail-count 检查。
|
||||
// 如果要验证所有可见项(包括没有 data-fail-count 的),可以移除 [data-fail-count] 选择器。
|
||||
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li[data-fail-count]:not([style*="display: none"])`);
|
||||
const count = visibleKeyItems.length;
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = '批量验证密钥';
|
||||
if (count > 0) {
|
||||
messageElement.textContent = `确定要批量验证筛选出的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥吗?此操作可能需要一些时间。`;
|
||||
confirmButton.disabled = false; // 确保按钮可用
|
||||
} else {
|
||||
messageElement.textContent = `当前没有筛选出可验证的${type === 'valid' ? '有效' : '无效'}密钥。`;
|
||||
confirmButton.disabled = true; // 没有可验证的密钥时禁用确认按钮
|
||||
}
|
||||
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeVerifyAll(type);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
window.closeVerifyModal = function() {
|
||||
document.getElementById('verifyModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
window.executeVerifyAll = async function(type) {
|
||||
try {
|
||||
// 关闭确认模态框
|
||||
closeVerifyModal();
|
||||
|
||||
// 找到对应的验证按钮以显示加载状态 (需要给按钮添加 data-verify-type 属性)
|
||||
// 或者,我们可以暂时禁用所有按钮或显示一个全局加载指示器
|
||||
// 这里我们暂时只记录日志,实际UI反馈可以后续增强
|
||||
console.log(`Starting bulk verification for ${type} keys...`);
|
||||
|
||||
// 获取筛选后可见的密钥
|
||||
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li[data-fail-count]:not([style*="display: none"]) .key-text`);
|
||||
const keysToVerify = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
|
||||
|
||||
if (keysToVerify.length === 0) {
|
||||
showNotification(`没有需要验证的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示一个通用的加载提示
|
||||
showNotification('开始批量验证,请稍候...', 'info');
|
||||
|
||||
// 调用新的后端 API 来验证选定的密钥
|
||||
const response = await fetch(`/gemini/v1beta/verify-selected-keys`, { // 假设的新 API 端点
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ keys: keysToVerify }) // 只发送密钥列表
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMsg = `服务器返回错误: ${response.status}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.message || errorMsg;
|
||||
} catch (e) { /*忽略解析错误*/ }
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 根据验证结果显示模态框
|
||||
if (data.success) {
|
||||
// 可以在这里构建更详细的消息,例如显示多少有效多少无效
|
||||
const message = `批量验证完成。有效: ${data.valid_count}, 无效: ${data.invalid_count}。页面即将刷新。`;
|
||||
// 验证成功后通常需要刷新页面以更新状态
|
||||
showResultModal(true, message, true); // autoReload = true
|
||||
} else {
|
||||
const errorMsg = data.message || '批量验证失败';
|
||||
// 失败后不自动刷新
|
||||
showResultModal(false, '批量验证失败: ' + errorMsg, false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量验证处理失败:', error);
|
||||
// 失败后不自动刷新
|
||||
showResultModal(false, '批量验证处理失败: ' + error.message, false);
|
||||
} finally {
|
||||
// 可以在这里移除加载指示器
|
||||
console.log("Bulk verification process finished.");
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
@@ -460,13 +619,154 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 初始加载时应用一次筛选
|
||||
filterValidKeys();
|
||||
}
|
||||
|
||||
// --- 批量验证相关函数 (明确挂载到 window) ---
|
||||
|
||||
window.showVerifyModal = function(type, event) {
|
||||
// 阻止事件冒泡(如果从按钮点击触发)
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('verifyModal');
|
||||
const titleElement = document.getElementById('verifyModalTitle');
|
||||
const messageElement = document.getElementById('verifyModalMessage');
|
||||
const confirmButton = document.getElementById('confirmVerifyBtn');
|
||||
|
||||
// 获取当前筛选后可见的、且包含 data-fail-count 属性的密钥数量
|
||||
// 注意:对于验证,我们可能想验证所有筛选出的密钥,无论其 data-fail-count 如何,
|
||||
// 但为了与重置保持一致,并且通常只验证有效/无效列表中的项,我们保留 data-fail-count 检查。
|
||||
// 如果要验证所有可见项(包括没有 data-fail-count 的),可以移除 [data-fail-count] 选择器。
|
||||
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li[data-fail-count]:not([style*="display: none"])`);
|
||||
const count = visibleKeyItems.length;
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = '批量验证密钥';
|
||||
if (count > 0) {
|
||||
messageElement.textContent = `确定要批量验证筛选出的 ${count} 个${type === 'valid' ? '有效' : '无效'}密钥吗?此操作可能需要一些时间。`;
|
||||
confirmButton.disabled = false; // 确保按钮可用
|
||||
} else {
|
||||
messageElement.textContent = `当前没有筛选出可验证的${type === 'valid' ? '有效' : '无效'}密钥。`;
|
||||
confirmButton.disabled = true; // 没有可验证的密钥时禁用确认按钮
|
||||
}
|
||||
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeVerifyAll(type);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
window.closeVerifyModal = function() {
|
||||
document.getElementById('verifyModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
window.executeVerifyAll = async function(type) {
|
||||
try {
|
||||
// 关闭确认模态框
|
||||
closeVerifyModal();
|
||||
|
||||
// 找到对应的验证按钮以显示加载状态 (需要给按钮添加 data-verify-type 属性)
|
||||
// 或者,我们可以暂时禁用所有按钮或显示一个全局加载指示器
|
||||
// 这里我们暂时只记录日志,实际UI反馈可以后续增强
|
||||
console.log(`Starting bulk verification for ${type} keys...`);
|
||||
|
||||
// 获取筛选后可见的密钥
|
||||
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li[data-fail-count]:not([style*="display: none"]) .key-text`);
|
||||
const keysToVerify = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
|
||||
|
||||
if (keysToVerify.length === 0) {
|
||||
showNotification(`没有需要验证的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示一个通用的加载提示
|
||||
showNotification('开始批量验证,请稍候...', 'info');
|
||||
|
||||
// 调用新的后端 API 来验证选定的密钥
|
||||
const response = await fetch(`/gemini/v1beta/verify-selected-keys`, { // 假设的新 API 端点
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ keys: keysToVerify }) // 只发送密钥列表
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMsg = `服务器返回错误: ${response.status}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.message || errorMsg;
|
||||
} catch (e) { /*忽略解析错误*/ }
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 根据验证结果显示模态框
|
||||
if (data.success) {
|
||||
// 可以在这里构建更详细的消息,例如显示多少有效多少无效
|
||||
const message = `批量验证完成。有效: ${data.valid_count}, 无效: ${data.invalid_count}。页面即将刷新。`;
|
||||
// 验证成功后通常需要刷新页面以更新状态
|
||||
showResultModal(true, message, true); // autoReload = true
|
||||
} else {
|
||||
const errorMsg = data.message || '批量验证失败';
|
||||
// 失败后不自动刷新
|
||||
showResultModal(false, '批量验证失败: ' + errorMsg, false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量验证处理失败:', error);
|
||||
// 失败后不自动刷新
|
||||
showResultModal(false, '批量验证处理失败: ' + error.message, false);
|
||||
} finally {
|
||||
// 可以在这里移除加载指示器
|
||||
console.log("Bulk verification process finished.");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自动刷新功能,每60秒刷新一次
|
||||
const autoRefreshInterval = 60000; // 60秒
|
||||
setInterval(() => {
|
||||
console.log('自动刷新 keys_status 页面...');
|
||||
location.reload();
|
||||
}, autoRefreshInterval);
|
||||
// --- 滚动和页面控制 ---
|
||||
// --- 自动刷新控制 ---
|
||||
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
|
||||
const autoRefreshIntervalTime = 60000; // 60秒
|
||||
let autoRefreshTimer = null;
|
||||
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshTimer) return; // 防止重复启动
|
||||
console.log('启动自动刷新...');
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
console.log('自动刷新 keys_status 页面...');
|
||||
location.reload();
|
||||
}, autoRefreshIntervalTime);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshTimer) {
|
||||
console.log('停止自动刷新...');
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (autoRefreshToggle) {
|
||||
// 从 localStorage 读取状态并初始化
|
||||
const isAutoRefreshEnabled = localStorage.getItem('autoRefreshEnabled') === 'true';
|
||||
autoRefreshToggle.checked = isAutoRefreshEnabled;
|
||||
if (isAutoRefreshEnabled) {
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
// 添加事件监听器
|
||||
autoRefreshToggle.addEventListener('change', () => {
|
||||
if (autoRefreshToggle.checked) {
|
||||
localStorage.setItem('autoRefreshEnabled', 'true');
|
||||
startAutoRefresh();
|
||||
} else {
|
||||
localStorage.setItem('autoRefreshEnabled', 'false');
|
||||
stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Service Worker registration
|
||||
|
||||
@@ -198,6 +198,16 @@
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
/* Tailwind Toggle Switch Helper CSS from config_editor.html */
|
||||
.toggle-checkbox:checked {
|
||||
@apply: right-0 border-primary-600;
|
||||
right: 0;
|
||||
border-color: #4F46E5;
|
||||
}
|
||||
.toggle-checkbox:checked + .toggle-label {
|
||||
@apply: bg-primary-600;
|
||||
background-color: #4F46E5;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -210,9 +220,20 @@
|
||||
{% block content %}
|
||||
<div class="container max-w-6xl mx-auto px-4"> <!-- Increased max-width -->
|
||||
<div class="glass-card rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<button class="absolute top-6 right-6 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full w-8 h-8 flex items-center justify-center text-primary-600 transition-all duration-300" onclick="refreshPage(this)">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<div class="absolute top-6 right-6 flex items-center gap-3">
|
||||
<!-- 自动刷新开关 -->
|
||||
<div class="flex items-center text-sm text-gray-600 select-none">
|
||||
<span class="mr-2">自动刷新</span>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
||||
<input type="checkbox" name="autoRefreshToggle" id="autoRefreshToggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
|
||||
<label for="autoRefreshToggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 手动刷新按钮 -->
|
||||
<button class="bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full w-8 h-8 flex items-center justify-center text-primary-600 transition-all duration-300" onclick="refreshPage(this)" title="手动刷新">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-primary-700 mb-4">
|
||||
<img src="/static/icons/logo.png" alt="Gemini Balance Logo" class="h-9 inline-block align-middle mr-2">
|
||||
@@ -300,10 +321,14 @@
|
||||
<h2 class="text-lg font-semibold">有效密钥列表 ({{ valid_key_count }})</h2>
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<label for="failCountThreshold" class="text-sm text-gray-600 select-none">失败次数≥</label>
|
||||
<input type="number" id="failCountThreshold" value="0" min="0" class="form-input h-7 w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500">
|
||||
<input type="number" id="failCountThreshold" value="0" min="0" class="form-input h-7 w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500" onclick="event.stopPropagation();">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('valid', event)"> <!-- 新增批量验证按钮 -->
|
||||
<i class="fas fa-check-double"></i>
|
||||
批量验证
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('valid', event)" data-reset-type="valid">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
批量重置
|
||||
@@ -369,6 +394,10 @@
|
||||
<h2 class="text-lg font-semibold">无效密钥列表 ({{ invalid_key_count }})</h2>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('invalid', event)"> <!-- 新增批量验证按钮 -->
|
||||
<i class="fas fa-check-double"></i>
|
||||
批量验证
|
||||
</button>
|
||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('invalid', event)" data-reset-type="invalid">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
批量重置
|
||||
@@ -463,22 +492,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作结果模态框 -->
|
||||
<div id="resultModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<!-- 验证确认模态框移到 resetModal 外部,避免嵌套导致显示异常 -->
|
||||
<div id="verifyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white rounded-lg p-6 shadow-xl max-w-md w-full animate-fade-in">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800" id="resultModalTitle">操作结果</h3>
|
||||
<button onclick="closeResultModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||
<h3 class="text-lg font-semibold text-gray-800" id="verifyModalTitle">批量验证密钥</h3>
|
||||
<button onclick="closeVerifyModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-6 text-center">
|
||||
<div id="resultIcon" class="text-5xl mb-3"></div>
|
||||
<p class="text-gray-600" id="resultModalMessage"></p>
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-600" id="verifyModalMessage"></p>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<button id="resultModalConfirmBtn" onclick="closeResultModal()" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg transition-colors">
|
||||
<div class="flex justify-end gap-3">
|
||||
<button onclick="closeVerifyModal()" class="px-4 py-2 bg-gray-300 hover:bg-gray-400 text-gray-800 rounded-lg transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="confirmVerifyBtn" class="px-4 py-2 bg-teal-500 hover:bg-teal-600 text-white rounded-lg transition-colors">
|
||||
确认验证
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作结果模态框 -->
|
||||
<div id="resultModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white rounded-2xl p-0 shadow-2xl max-w-lg w-full animate-fade-in border border-gray-200">
|
||||
<div class="flex items-center justify-between px-6 pt-6 pb-2 border-b">
|
||||
<h3 class="text-xl font-bold text-gray-800 text-center w-full" id="resultModalTitle" style="letter-spacing:0.05em;">操作结果</h3>
|
||||
<button onclick="closeResultModal()" class="absolute right-6 top-6 text-gray-400 hover:text-gray-700 focus:outline-none text-2xl">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center px-8 pt-6 pb-2">
|
||||
<div id="resultIcon" class="text-6xl mb-3"></div>
|
||||
</div>
|
||||
<div class="px-8 pb-2 w-full">
|
||||
<div id="resultModalMessage"
|
||||
class="text-gray-700 text-base leading-relaxed break-all whitespace-pre-line max-h-60 overflow-y-auto border border-gray-100 rounded-lg bg-gray-50 p-4 shadow-inner"
|
||||
style="font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center px-8 pb-6 pt-2">
|
||||
<button id="resultModalConfirmBtn" onclick="closeResultModal()" class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-semibold text-base shadow transition-colors">
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user