From 0693a5c24552919095ccc1dd8607656c18486693 Mon Sep 17 00:00:00 2001 From: snaily Date: Tue, 15 Apr 2025 23:15:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(keys=5Fstatus):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E9=AA=8C=E8=AF=81=E5=AF=86=E9=92=A5=E4=B8=8E?= =?UTF-8?q?=E9=80=89=E5=AE=9A=E5=AF=86=E9=92=A5=E5=A4=B1=E8=B4=A5=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E9=87=8D=E7=BD=AE=EF=BC=8C=E5=A2=9E=E5=BC=BA=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 ResetSelectedKeysRequest、VerifySelectedKeysRequest 数据模型及相关 API 路由,实现批量重置选定密钥失败计数功能 - 前端 keys_status.js/keys_status.html 新增批量验证按钮、批量验证弹窗及交互逻辑,支持对筛选后密钥进行批量验证 - 自动刷新功能支持开关,优化用户体验 - UI 细节优化,提升密钥管理便捷性 --- app/domain/gemini_models.py | 9 + app/router/gemini_routes.py | 150 ++++++++++++- app/static/js/keys_status.js | 374 +++++++++++++++++++++++++++++---- app/templates/keys_status.html | 84 ++++++-- 4 files changed, 564 insertions(+), 53 deletions(-) diff --git a/app/domain/gemini_models.py b/app/domain/gemini_models.py index 9da1820..1249a8c 100644 --- a/app/domain/gemini_models.py +++ b/app/domain/gemini_models.py @@ -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] diff --git a/app/router/gemini_routes.py b/app/router/gemini_routes.py index d7e4ac4..0d0cc7a 100644 --- a/app/router/gemini_routes.py +++ b/app/router/gemini_routes.py @@ -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)}) \ No newline at end of file + 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 + }) \ No newline at end of file diff --git a/app/static/js/keys_status.js b/app/static/js/keys_status.js index b931253..c860dd4 100644 --- a/app/static/js/keys_status.js +++ b/app/static/js/keys_status.js @@ -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 = ' 重置中'; 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 diff --git a/app/templates/keys_status.html b/app/templates/keys_status.html index 0e4eb58..4c27ef5 100644 --- a/app/templates/keys_status.html +++ b/app/templates/keys_status.html @@ -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; + } {% endblock %} @@ -210,9 +220,20 @@ {% block content %}
- +
+ +
+ 自动刷新 +
+ + +
+
+ + +

Gemini Balance Logo @@ -300,10 +321,14 @@

有效密钥列表 ({{ valid_key_count }})

- +
+
+
- - -