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:
snaily
2025-04-11 03:16:51 +08:00
parent 69261e98de
commit 51bb71bdb5
25 changed files with 2090 additions and 2776 deletions
+258 -49
View File
@@ -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);