mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-05 07:26:39 +08:00
```git
feat: 添加密钥检查调度器并重构前端UI
主要变更:
- **调度器功能:**
- 集成 APScheduler 实现定时任务,用于定期检查API密钥的有效性。
- 在 `.env.example` 和 `app/config/config.py` 中添加了 `CHECK_INTERVAL_HOURS` 和 `TIMEZONE` 配置项。
- 在应用生命周期 (`app/core/application.py`) 中添加了调度器的启动和停止逻辑。
- 新增 `app/scheduler/` 目录及相关实现 (`key_checker.py`)。
- 新增 `app/router/scheduler_routes.py` 用于调度器相关API (如果未来需要)。
- 在 `requirements.txt` 中添加 `apscheduler` 依赖。
- **前端重构与改进:**
- 引入 `app/templates/base.html` 作为基础模板,统一页面结构和样式引入。
- 使用新的样式(推测为Tailwind CSS)重构了 `auth.html`, `config_editor.html`, `error_logs.html`, `keys_status.html` 页面,提升了UI一致性和响应式布局。
- 删除了旧的CSS文件 (`auth.css`, `config_editor.css`, `error_logs.css`, `keys_status.css`)。
- 更新了对应的 JavaScript 文件 (`config_editor.js`, `error_logs.js`, `keys_status.js`) 以适应新的HTML结构和交互。
- 在 `keys_status.html` 页面增加了按失败次数过滤密钥、批量重置失败次数、确认模态框等功能。
- 添加了新的 Logo 图片 (`logo.png`, `logo1.png`)。
- **其他:**
- 更新了 `app/router/routes.py` 以包含新的路由。
- 对 `app/service/key/key_manager.py` 和 `app/database/services.py` 进行了相关调整以支持新功能。
```
This commit is contained in:
+258
-49
@@ -27,15 +27,21 @@ function copyToClipboard(text) {
|
||||
|
||||
function copyKeys(type) {
|
||||
const keys = Array.from(document.querySelectorAll(`#${type}Keys .key-text`)).map(span => span.dataset.fullKey);
|
||||
const jsonKeys = JSON.stringify(keys);
|
||||
|
||||
copyToClipboard(jsonKeys)
|
||||
if (keys.length === 0) {
|
||||
showCopyStatus('没有可复制的密钥', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const keysText = keys.join('\n');
|
||||
|
||||
copyToClipboard(keysText)
|
||||
.then(() => {
|
||||
showCopyStatus(`已成功复制${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
|
||||
showCopyStatus(`已成功复制${keys.length}个${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法复制文本: ', err);
|
||||
showCopyStatus('复制失败,请重试');
|
||||
showCopyStatus('复制失败,请重试', true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,21 +52,32 @@ function copyKey(key) {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法复制文本: ', err);
|
||||
showCopyStatus('复制失败,请重试');
|
||||
showCopyStatus('复制失败,请重试', true);
|
||||
});
|
||||
}
|
||||
|
||||
function showCopyStatus(message, type = 'success') {
|
||||
function showCopyStatus(message, isError = false) {
|
||||
const statusElement = document.getElementById('copyStatus');
|
||||
statusElement.textContent = message;
|
||||
statusElement.className = type; // 设置样式类
|
||||
statusElement.style.opacity = 1;
|
||||
|
||||
// 添加适当的样式类
|
||||
if (isError) {
|
||||
statusElement.classList.add('bg-danger-500');
|
||||
statusElement.classList.remove('bg-black');
|
||||
} else {
|
||||
statusElement.classList.remove('bg-danger-500');
|
||||
statusElement.classList.add('bg-black');
|
||||
}
|
||||
|
||||
// 应用过渡效果
|
||||
statusElement.style.opacity = "1";
|
||||
statusElement.style.transform = "translate(-50%, 0)";
|
||||
|
||||
// 设置自动消失
|
||||
setTimeout(() => {
|
||||
statusElement.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
statusElement.className = ''; // 清除样式类
|
||||
}, 300);
|
||||
}, 2000);
|
||||
statusElement.style.opacity = "0";
|
||||
statusElement.style.transform = "translate(-50%, 10px)";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function verifyKey(key, button) {
|
||||
@@ -70,59 +87,223 @@ async function verifyKey(key, button) {
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 验证中';
|
||||
|
||||
const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
|
||||
try {
|
||||
const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 根据验证结果更新UI并显示模态提示框
|
||||
if (data.success || data.status === 'valid') {
|
||||
// 验证成功,显示成功结果
|
||||
button.style.backgroundColor = '#27ae60';
|
||||
// 使用结果模态框显示成功消息
|
||||
showResultModal(true, '密钥验证成功');
|
||||
// 模态框关闭时会自动刷新页面
|
||||
} else {
|
||||
// 验证失败,显示失败结果
|
||||
const errorMsg = data.error || '密钥无效';
|
||||
button.style.backgroundColor = '#e74c3c';
|
||||
// 使用结果模态框显示失败消息,但不自动刷新页面
|
||||
showResultModal(false, '密钥验证失败: ' + errorMsg, true); // 改为true以在关闭时刷新
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('API请求失败:', fetchError);
|
||||
showResultModal(false, '验证请求失败: ' + fetchError.message, true); // 改为true以在关闭时刷新
|
||||
} finally {
|
||||
// 1秒后恢复按钮原始状态
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHtml;
|
||||
button.disabled = false;
|
||||
button.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
button.disabled = false;
|
||||
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
|
||||
showResultModal(false, '验证处理失败: ' + error.message, true); // 改为true以在关闭时刷新
|
||||
}
|
||||
}
|
||||
|
||||
async function resetKeyFailCount(key, button) {
|
||||
try {
|
||||
// 禁用按钮并显示加载状态
|
||||
button.disabled = true;
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
|
||||
|
||||
const response = await fetch(`/gemini/v1beta/reset-fail-count/${key}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 根据验证结果更新UI
|
||||
if (data.status === 'valid') {
|
||||
showCopyStatus('密钥验证成功', 'success');
|
||||
// 根据重置结果更新UI
|
||||
if (data.success) {
|
||||
showCopyStatus('失败计数重置成功');
|
||||
button.style.backgroundColor = '#27ae60';
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showCopyStatus('密钥验证失败', 'error');
|
||||
const errorMsg = data.message || '重置失败';
|
||||
showCopyStatus('重置失败: ' + errorMsg, true);
|
||||
button.style.backgroundColor = '#e74c3c';
|
||||
}
|
||||
|
||||
// 3秒后恢复按钮原始状态
|
||||
// 1秒后恢复按钮原始状态
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHtml;
|
||||
button.disabled = false;
|
||||
button.style.backgroundColor = '';
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
showCopyStatus('验证请求失败', 'error');
|
||||
console.error('重置失败:', error);
|
||||
showCopyStatus('重置请求失败: ' + error.message, true);
|
||||
button.disabled = false;
|
||||
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
|
||||
button.innerHTML = '<i class="fas fa-redo-alt"></i> 重置';
|
||||
}
|
||||
}
|
||||
|
||||
function showResetModal(type) {
|
||||
const modalElement = document.getElementById('resetModal');
|
||||
const titleElement = document.getElementById('resetModalTitle');
|
||||
const messageElement = document.getElementById('resetModalMessage');
|
||||
const confirmButton = document.getElementById('confirmResetBtn');
|
||||
|
||||
// 设置标题和消息
|
||||
titleElement.textContent = '批量重置失败次数';
|
||||
messageElement.textContent = `确定要批量重置${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`;
|
||||
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeResetAll(type);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeResetModal() {
|
||||
document.getElementById('resetModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 触发显示模态框
|
||||
function resetAllKeysFailCount(type, event) {
|
||||
// 阻止事件冒泡
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// 显示模态确认框
|
||||
showResetModal(type);
|
||||
}
|
||||
|
||||
// 执行批量重置
|
||||
// 关闭模态框并根据参数决定是否刷新页面
|
||||
function closeResultModal(reload = true) {
|
||||
document.getElementById('resultModal').classList.add('hidden');
|
||||
if (reload) {
|
||||
location.reload(); // 操作完成后刷新页面
|
||||
}
|
||||
}
|
||||
|
||||
// 显示操作结果模态框
|
||||
function showResultModal(success, message, autoReload = true) {
|
||||
const modalElement = document.getElementById('resultModal');
|
||||
const titleElement = document.getElementById('resultModalTitle');
|
||||
const messageElement = document.getElementById('resultModalMessage');
|
||||
const iconElement = document.getElementById('resultIcon');
|
||||
const confirmButton = document.getElementById('resultModalConfirmBtn');
|
||||
|
||||
// 设置标题
|
||||
titleElement.textContent = success ? '操作成功' : '操作失败';
|
||||
|
||||
// 设置图标
|
||||
if (success) {
|
||||
iconElement.innerHTML = '<i class="fas fa-check-circle text-success-500"></i>';
|
||||
iconElement.className = 'text-5xl mb-3 text-success-500';
|
||||
} else {
|
||||
iconElement.innerHTML = '<i class="fas fa-times-circle"></i>';
|
||||
iconElement.className = 'text-5xl mb-3 text-danger-500';
|
||||
}
|
||||
|
||||
// 设置消息
|
||||
messageElement.textContent = message;
|
||||
|
||||
// 设置确认按钮点击事件
|
||||
confirmButton.onclick = () => closeResultModal(autoReload);
|
||||
|
||||
// 显示模态框
|
||||
modalElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 禁用按钮并显示加载状态
|
||||
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'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器返回错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 根据重置结果显示模态框
|
||||
if (data.success) {
|
||||
const message = data.reset_count ?
|
||||
`成功重置${data.reset_count}个${type === 'valid' ? '有效' : '无效'}密钥的失败次数` :
|
||||
'所有失败次数重置成功';
|
||||
showResultModal(true, message);
|
||||
} else {
|
||||
const errorMsg = data.message || '批量重置失败';
|
||||
showResultModal(false, '批量重置失败: ' + errorMsg);
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('API请求失败:', fetchError);
|
||||
showResultModal(false, '批量重置请求失败: ' + fetchError.message);
|
||||
} finally {
|
||||
// 恢复按钮原始状态
|
||||
setTimeout(() => {
|
||||
resetButton.innerHTML = originalHtml;
|
||||
resetButton.disabled = false;
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量重置失败:', error);
|
||||
showResultModal(false, '批量重置处理失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 移除这个函数,因为它可能正在干扰按钮的显示
|
||||
// HTML中已经设置了滚动按钮为flex显示,不需要JavaScript额外控制
|
||||
function updateScrollButtons() {
|
||||
const container = document.querySelector('.container');
|
||||
const scrollButtons = document.querySelector('.scroll-buttons');
|
||||
if (container.scrollHeight > container.clientHeight) {
|
||||
scrollButtons.style.display = 'flex';
|
||||
} else {
|
||||
scrollButtons.style.display = 'none';
|
||||
}
|
||||
// 不执行任何操作
|
||||
}
|
||||
|
||||
function refreshPage(button) {
|
||||
@@ -142,22 +323,50 @@ function toggleSection(header, sectionId) {
|
||||
content.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
// 筛选有效密钥(根据失败次数阈值)
|
||||
function filterValidKeys() {
|
||||
const thresholdInput = document.getElementById('failCountThreshold');
|
||||
const validKeyItems = document.querySelectorAll('#validKeys li');
|
||||
// 读取阈值,如果输入无效或为空,则默认为0(不过滤)
|
||||
const threshold = parseInt(thresholdInput.value, 10);
|
||||
const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold;
|
||||
|
||||
validKeyItems.forEach(item => {
|
||||
const failCount = parseInt(item.dataset.failCount, 10);
|
||||
// 如果失败次数大于等于阈值,则显示,否则隐藏
|
||||
if (failCount >= filterThreshold) {
|
||||
item.style.display = ''; // 显示
|
||||
} else {
|
||||
item.style.display = 'none'; // 隐藏
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 检查滚动按钮
|
||||
updateScrollButtons();
|
||||
|
||||
// 移除对滚动按钮显示的控制,让它们由HTML/CSS控制
|
||||
|
||||
// 监听展开/折叠事件
|
||||
document.querySelectorAll('.key-list h2').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
setTimeout(updateScrollButtons, 300);
|
||||
// 不再调用updateScrollButtons
|
||||
});
|
||||
});
|
||||
|
||||
// 更新版权年份
|
||||
const copyrightYear = document.querySelector('.copyright script');
|
||||
if (copyrightYear) {
|
||||
copyrightYear.textContent = new Date().getFullYear();
|
||||
const copyrightYearElement = document.querySelector('.copyright script');
|
||||
if (copyrightYearElement && copyrightYearElement.parentNode.classList.contains('copyright')) {
|
||||
// 确保只更新版权部分的年份
|
||||
copyrightYearElement.textContent = new Date().getFullYear();
|
||||
}
|
||||
|
||||
// 添加筛选输入框事件监听
|
||||
const thresholdInput = document.getElementById('failCountThreshold');
|
||||
if (thresholdInput) {
|
||||
// 使用 'input' 事件实时响应输入变化
|
||||
thresholdInput.addEventListener('input', filterValidKeys);
|
||||
// 初始加载时应用一次筛选(基于默认值1)
|
||||
filterValidKeys();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -174,8 +383,8 @@ if ('serviceWorker' in navigator) {
|
||||
});
|
||||
}
|
||||
function toggleKeyVisibility(button) {
|
||||
const keyInfoDiv = button.closest('.key-info');
|
||||
const keyTextSpan = keyInfoDiv.querySelector('.key-text');
|
||||
const keyContainer = button.closest('.flex.items-center.gap-1');
|
||||
const keyTextSpan = keyContainer.querySelector('.key-text');
|
||||
const eyeIcon = button.querySelector('i');
|
||||
const fullKey = keyTextSpan.dataset.fullKey;
|
||||
const maskedKey = fullKey.substring(0, 4) + '...' + fullKey.substring(fullKey.length - 4);
|
||||
|
||||
Reference in New Issue
Block a user