// ===================================================================== // GCLI2API 控制面板公共JavaScript模块 // ===================================================================== // ===================================================================== // 全局状态管理 // ===================================================================== const AppState = { // 认证相关 authToken: '', authInProgress: false, currentProjectId: '', // Antigravity认证 antigravityAuthState: null, antigravityAuthInProgress: false, // 凭证管理 creds: createCredsManager('normal'), antigravityCreds: createCredsManager('antigravity'), // 文件上传 uploadFiles: createUploadManager('normal'), antigravityUploadFiles: createUploadManager('antigravity'), // 配置管理 currentConfig: {}, envLockedFields: new Set(), // 日志管理 logWebSocket: null, allLogs: [], filteredLogs: [], currentLogFilter: 'all', // 使用统计 usageStatsData: {}, // 冷却倒计时 cooldownTimerInterval: null }; // ===================================================================== // 凭证管理器工厂 // ===================================================================== function createCredsManager(type) { const modeParam = type === 'antigravity' ? 'mode=antigravity' : 'mode=geminicli'; return { type: type, data: {}, filteredData: {}, currentPage: 1, pageSize: 20, selectedFiles: new Set(), totalCount: 0, currentStatusFilter: 'all', currentErrorCodeFilter: 'all', currentCooldownFilter: 'all', statsData: { total: 0, normal: 0, disabled: 0 }, // API端点 getEndpoint: (action) => { const endpoints = { status: `./creds/status`, action: `./creds/action`, batchAction: `./creds/batch-action`, download: `./creds/download`, downloadAll: `./creds/download-all`, detail: `./creds/detail`, fetchEmail: `./creds/fetch-email`, refreshAllEmails: `./creds/refresh-all-emails`, deduplicate: `./creds/deduplicate-by-email`, verifyProject: `./creds/verify-project`, quota: `./creds/quota` }; return endpoints[action] || ''; }, // 获取mode参数 getModeParam: () => modeParam, // DOM元素ID前缀 getElementId: (suffix) => { // 普通凭证的ID首字母小写,如 credsLoading // Antigravity的ID是 antigravity + 首字母大写,如 antigravityCredsLoading if (type === 'antigravity') { return 'antigravity' + suffix.charAt(0).toUpperCase() + suffix.slice(1); } return suffix.charAt(0).toLowerCase() + suffix.slice(1); }, // 刷新凭证列表 async refresh() { const loading = document.getElementById(this.getElementId('CredsLoading')); const list = document.getElementById(this.getElementId('CredsList')); try { loading.style.display = 'block'; list.innerHTML = ''; const offset = (this.currentPage - 1) * this.pageSize; const errorCodeFilter = this.currentErrorCodeFilter || 'all'; const cooldownFilter = this.currentCooldownFilter || 'all'; const response = await fetch( `${this.getEndpoint('status')}?offset=${offset}&limit=${this.pageSize}&status_filter=${this.currentStatusFilter}&error_code_filter=${errorCodeFilter}&cooldown_filter=${cooldownFilter}&${this.getModeParam()}`, { headers: getAuthHeaders() } ); const data = await response.json(); if (response.ok) { this.data = {}; data.items.forEach(item => { this.data[item.filename] = { filename: item.filename, status: { disabled: item.disabled, error_codes: item.error_codes || [], last_success: item.last_success, }, user_email: item.user_email, model_cooldowns: item.model_cooldowns || {} }; }); this.totalCount = data.total; // 使用后端返回的全局统计数据 if (data.stats) { this.statsData = data.stats; } else { // 兼容旧版本后端 this.calculateStats(); } this.updateStatsDisplay(); this.filteredData = this.data; this.renderList(); this.updatePagination(); let msg = `已加载 ${data.total} 个${type === 'antigravity' ? 'Antigravity' : ''}凭证文件`; if (this.currentStatusFilter !== 'all') { msg += ` (筛选: ${this.currentStatusFilter === 'enabled' ? '仅启用' : '仅禁用'})`; } showStatus(msg, 'success'); } else { showStatus(`加载失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { loading.style.display = 'none'; } }, // 计算统计数据(仅用于兼容旧版本后端) calculateStats() { this.statsData = { total: this.totalCount, normal: 0, disabled: 0 }; Object.values(this.data).forEach(credInfo => { if (credInfo.status.disabled) { this.statsData.disabled++; } else { this.statsData.normal++; } }); }, // 更新统计显示 updateStatsDisplay() { document.getElementById(this.getElementId('StatTotal')).textContent = this.statsData.total; document.getElementById(this.getElementId('StatNormal')).textContent = this.statsData.normal; document.getElementById(this.getElementId('StatDisabled')).textContent = this.statsData.disabled; }, // 渲染凭证列表 renderList() { const list = document.getElementById(this.getElementId('CredsList')); list.innerHTML = ''; const entries = Object.entries(this.filteredData); if (entries.length === 0) { const msg = this.totalCount === 0 ? '暂无凭证文件' : '当前筛选条件下暂无数据'; list.innerHTML = `

${msg}

`; document.getElementById(this.getElementId('PaginationContainer')).style.display = 'none'; return; } entries.forEach(([, credInfo]) => { list.appendChild(createCredCard(credInfo, this)); }); document.getElementById(this.getElementId('PaginationContainer')).style.display = this.getTotalPages() > 1 ? 'flex' : 'none'; this.updateBatchControls(); }, // 获取总页数 getTotalPages() { return Math.ceil(this.totalCount / this.pageSize); }, // 更新分页信息 updatePagination() { const totalPages = this.getTotalPages(); const startItem = (this.currentPage - 1) * this.pageSize + 1; const endItem = Math.min(this.currentPage * this.pageSize, this.totalCount); document.getElementById(this.getElementId('PaginationInfo')).textContent = `第 ${this.currentPage} 页,共 ${totalPages} 页 (显示 ${startItem}-${endItem},共 ${this.totalCount} 项)`; document.getElementById(this.getElementId('PrevPageBtn')).disabled = this.currentPage <= 1; document.getElementById(this.getElementId('NextPageBtn')).disabled = this.currentPage >= totalPages; }, // 切换页面 changePage(direction) { const newPage = this.currentPage + direction; if (newPage >= 1 && newPage <= this.getTotalPages()) { this.currentPage = newPage; this.refresh(); } }, // 改变每页大小 changePageSize() { this.pageSize = parseInt(document.getElementById(this.getElementId('PageSizeSelect')).value); this.currentPage = 1; this.refresh(); }, // 应用状态筛选 applyStatusFilter() { this.currentStatusFilter = document.getElementById(this.getElementId('StatusFilter')).value; const errorCodeFilterEl = document.getElementById(this.getElementId('ErrorCodeFilter')); const cooldownFilterEl = document.getElementById(this.getElementId('CooldownFilter')); this.currentErrorCodeFilter = errorCodeFilterEl ? errorCodeFilterEl.value : 'all'; this.currentCooldownFilter = cooldownFilterEl ? cooldownFilterEl.value : 'all'; this.currentPage = 1; this.refresh(); }, // 更新批量控件 updateBatchControls() { const selectedCount = this.selectedFiles.size; document.getElementById(this.getElementId('SelectedCount')).textContent = `已选择 ${selectedCount} 项`; const batchBtns = ['Enable', 'Disable', 'Delete', 'Verify'].map(action => document.getElementById(this.getElementId(`Batch${action}Btn`)) ); batchBtns.forEach(btn => btn && (btn.disabled = selectedCount === 0)); const selectAllCheckbox = document.getElementById(this.getElementId('SelectAllCheckbox')); if (!selectAllCheckbox) return; const checkboxes = document.querySelectorAll(`.${this.getElementId('file-checkbox')}`); const currentPageSelectedCount = Array.from(checkboxes) .filter(cb => this.selectedFiles.has(cb.getAttribute('data-filename'))).length; if (currentPageSelectedCount === 0) { selectAllCheckbox.indeterminate = false; selectAllCheckbox.checked = false; } else if (currentPageSelectedCount === checkboxes.length) { selectAllCheckbox.indeterminate = false; selectAllCheckbox.checked = true; } else { selectAllCheckbox.indeterminate = true; } checkboxes.forEach(cb => { cb.checked = this.selectedFiles.has(cb.getAttribute('data-filename')); }); }, // 凭证操作 async action(filename, action) { try { const response = await fetch(`${this.getEndpoint('action')}?${this.getModeParam()}`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ filename, action }) }); const data = await response.json(); if (response.ok) { showStatus(data.message || `操作成功: ${action}`, 'success'); await this.refresh(); } else { showStatus(`操作失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } }, // 批量操作 async batchAction(action) { const selectedFiles = Array.from(this.selectedFiles); if (selectedFiles.length === 0) { showStatus('请先选择要操作的文件', 'error'); return; } const actionNames = { enable: '启用', disable: '禁用', delete: '删除' }; const confirmMsg = action === 'delete' ? `确定要删除选中的 ${selectedFiles.length} 个文件吗?\n注意:此操作不可恢复!` : `确定要${actionNames[action]}选中的 ${selectedFiles.length} 个文件吗?`; if (!confirm(confirmMsg)) return; try { showStatus(`正在执行批量${actionNames[action]}操作...`, 'info'); const response = await fetch(`${this.getEndpoint('batchAction')}?${this.getModeParam()}`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ action, filenames: selectedFiles }) }); const data = await response.json(); if (response.ok) { const successCount = data.success_count || data.succeeded; showStatus(`批量操作完成:成功处理 ${successCount}/${selectedFiles.length} 个文件`, 'success'); this.selectedFiles.clear(); this.updateBatchControls(); await this.refresh(); } else { showStatus(`批量操作失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`批量操作网络错误: ${error.message}`, 'error'); } } }; } // ===================================================================== // 文件上传管理器工厂 // ===================================================================== function createUploadManager(type) { const modeParam = type === 'antigravity' ? 'mode=antigravity' : 'mode=geminicli'; const endpoint = `./auth/upload?${modeParam}`; return { type: type, selectedFiles: [], getElementId: (suffix) => { // 普通上传的ID首字母小写,如 fileList // Antigravity的ID是 antigravity + 首字母大写,如 antigravityFileList if (type === 'antigravity') { return 'antigravity' + suffix.charAt(0).toUpperCase() + suffix.slice(1); } return suffix.charAt(0).toLowerCase() + suffix.slice(1); }, handleFileSelect(event) { this.addFiles(Array.from(event.target.files)); }, addFiles(files) { files.forEach(file => { const isValid = file.type === 'application/json' || file.name.endsWith('.json') || file.type === 'application/zip' || file.name.endsWith('.zip'); if (isValid) { if (!this.selectedFiles.find(f => f.name === file.name && f.size === file.size)) { this.selectedFiles.push(file); } } else { showStatus(`文件 ${file.name} 格式不支持,只支持JSON和ZIP文件`, 'error'); } }); this.updateFileList(); }, updateFileList() { const list = document.getElementById(this.getElementId('FileList')); const section = document.getElementById(this.getElementId('FileListSection')); if (!list || !section) { console.warn('File list elements not found:', this.getElementId('FileList')); return; } if (this.selectedFiles.length === 0) { section.classList.add('hidden'); return; } section.classList.remove('hidden'); list.innerHTML = ''; this.selectedFiles.forEach((file, index) => { const isZip = file.name.endsWith('.zip'); const fileIcon = isZip ? '📦' : '📄'; const fileType = isZip ? ' (ZIP压缩包)' : ' (JSON文件)'; const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = `
${fileIcon} ${file.name} (${formatFileSize(file.size)}${fileType})
`; list.appendChild(fileItem); }); }, removeFile(index) { this.selectedFiles.splice(index, 1); this.updateFileList(); }, clearFiles() { this.selectedFiles = []; this.updateFileList(); }, async upload() { if (this.selectedFiles.length === 0) { showStatus('请选择要上传的文件', 'error'); return; } const progressSection = document.getElementById(this.getElementId('UploadProgressSection')); const progressFill = document.getElementById(this.getElementId('ProgressFill')); const progressText = document.getElementById(this.getElementId('ProgressText')); progressSection.classList.remove('hidden'); const formData = new FormData(); this.selectedFiles.forEach(file => formData.append('files', file)); if (this.selectedFiles.some(f => f.name.endsWith('.zip'))) { showStatus('正在上传并解压ZIP文件...', 'info'); } try { const xhr = new XMLHttpRequest(); xhr.timeout = 300000; // 5分钟 xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percent = (event.loaded / event.total) * 100; progressFill.style.width = percent + '%'; progressText.textContent = Math.round(percent) + '%'; } }; xhr.onload = () => { if (xhr.status === 200) { try { const data = JSON.parse(xhr.responseText); showStatus(`成功上传 ${data.uploaded_count} 个${type === 'antigravity' ? 'Antigravity' : ''}文件`, 'success'); this.clearFiles(); progressSection.classList.add('hidden'); } catch (e) { showStatus('上传失败: 服务器响应格式错误', 'error'); } } else { try { const error = JSON.parse(xhr.responseText); showStatus(`上传失败: ${error.detail || error.error || '未知错误'}`, 'error'); } catch (e) { showStatus(`上传失败: HTTP ${xhr.status}`, 'error'); } } }; xhr.onerror = () => { showStatus(`上传失败:连接中断 - 可能原因:文件过多(${this.selectedFiles.length}个)或网络不稳定。建议分批上传。`, 'error'); progressSection.classList.add('hidden'); }; xhr.ontimeout = () => { showStatus('上传失败:请求超时 - 文件处理时间过长,请减少文件数量或检查网络连接', 'error'); progressSection.classList.add('hidden'); }; xhr.open('POST', endpoint); xhr.setRequestHeader('Authorization', `Bearer ${AppState.authToken}`); xhr.send(formData); } catch (error) { showStatus(`上传失败: ${error.message}`, 'error'); } } }; } // ===================================================================== // 工具函数 // ===================================================================== function showStatus(message, type = 'info') { const statusSection = document.getElementById('statusSection'); if (statusSection) { // 清除之前的定时器 if (window._statusTimeout) { clearTimeout(window._statusTimeout); } // 创建新的 toast statusSection.innerHTML = `
${message}
`; const statusDiv = statusSection.querySelector('.status'); // 强制重绘以触发动画 statusDiv.offsetHeight; statusDiv.classList.add('show'); // 3秒后淡出并移除 window._statusTimeout = setTimeout(() => { statusDiv.classList.add('fade-out'); setTimeout(() => { statusSection.innerHTML = ''; }, 300); // 等待淡出动画完成 }, 3000); } else { alert(message); } } function getAuthHeaders() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${AppState.authToken}` }; } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB'; return Math.round(bytes / (1024 * 1024)) + ' MB'; } function formatCooldownTime(remainingSeconds) { const hours = Math.floor(remainingSeconds / 3600); const minutes = Math.floor((remainingSeconds % 3600) / 60); const seconds = remainingSeconds % 60; if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; } // ===================================================================== // 凭证卡片创建(通用) // ===================================================================== function createCredCard(credInfo, manager) { const div = document.createElement('div'); const { status, filename } = credInfo; const managerType = manager.type; // 卡片样式 div.className = status.disabled ? 'cred-card disabled' : 'cred-card'; // 状态徽章 let statusBadges = ''; statusBadges += status.disabled ? '已禁用' : '已启用'; if (status.error_codes && status.error_codes.length > 0) { statusBadges += `错误码: ${status.error_codes.join(', ')}`; const autoBan = status.error_codes.filter(c => c === 400 || c === 403); if (autoBan.length > 0 && status.disabled) { statusBadges += 'AUTO_BAN'; } } else { statusBadges += '无错误'; } // 模型级冷却状态 if (credInfo.model_cooldowns && Object.keys(credInfo.model_cooldowns).length > 0) { const currentTime = Date.now() / 1000; const activeCooldowns = Object.entries(credInfo.model_cooldowns) .filter(([, until]) => until > currentTime) .map(([model, until]) => { const remaining = Math.max(0, Math.floor(until - currentTime)); const shortModel = model.replace('gemini-', '').replace('-exp', '') .replace('2.0-', '2-').replace('1.5-', '1.5-'); return { model: shortModel, time: formatCooldownTime(remaining).replace(/s$/, '').replace(/ /g, ''), fullModel: model }; }); if (activeCooldowns.length > 0) { activeCooldowns.slice(0, 2).forEach(item => { statusBadges += `🔧 ${item.model}: ${item.time}`; }); if (activeCooldowns.length > 2) { const remaining = activeCooldowns.length - 2; const remainingModels = activeCooldowns.slice(2).map(i => `${i.fullModel}: ${i.time}`).join('\n'); statusBadges += `+${remaining}`; } } } // 路径ID const pathId = (managerType === 'antigravity' ? 'ag_' : '') + btoa(encodeURIComponent(filename)).replace(/[+/=]/g, '_'); // 操作按钮 const actionButtons = ` ${status.disabled ? `` : `` } ${managerType === 'antigravity' ? `` : ''} `; // 邮箱信息 const emailInfo = credInfo.user_email ? `
${credInfo.user_email}
` : '
未获取邮箱
'; const checkboxClass = manager.getElementId('file-checkbox'); div.innerHTML = `
${filename}
${emailInfo}
${statusBadges}
${actionButtons}
点击"查看内容"按钮加载文件详情...
${managerType === 'antigravity' ? ` ` : ''} `; // 添加事件监听 div.querySelectorAll('[data-filename][data-action]').forEach(button => { button.addEventListener('click', function () { const fn = this.getAttribute('data-filename'); const action = this.getAttribute('data-action'); if (action === 'delete') { if (confirm(`确定要删除${managerType === 'antigravity' ? ' Antigravity ' : ''}凭证文件吗?\n${fn}`)) { manager.action(fn, action); } } else { manager.action(fn, action); } }); }); return div; } // ===================================================================== // 凭证详情切换 // ===================================================================== async function toggleCredDetails(pathId) { await toggleCredDetailsCommon(pathId, AppState.creds); } async function toggleAntigravityCredDetails(pathId) { await toggleCredDetailsCommon(pathId, AppState.antigravityCreds); } async function toggleCredDetailsCommon(pathId, manager) { const details = document.getElementById('details-' + pathId); if (!details) return; const isShowing = details.classList.toggle('show'); if (isShowing) { const contentDiv = details.querySelector('.cred-content'); const filename = contentDiv.getAttribute('data-filename'); const loaded = contentDiv.getAttribute('data-loaded'); if (loaded === 'false' && filename) { contentDiv.textContent = '正在加载文件内容...'; try { const modeParam = manager.type === 'antigravity' ? 'mode=antigravity' : 'mode=geminicli'; const endpoint = `./creds/detail/${encodeURIComponent(filename)}?${modeParam}`; const response = await fetch(endpoint, { headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.content) { contentDiv.textContent = JSON.stringify(data.content, null, 2); contentDiv.setAttribute('data-loaded', 'true'); } else { contentDiv.textContent = '无法加载文件内容: ' + (data.error || data.detail || '未知错误'); } } catch (error) { contentDiv.textContent = '加载文件内容失败: ' + error.message; } } } } // ===================================================================== // 登录相关函数 // ===================================================================== async function login() { const password = document.getElementById('loginPassword').value; if (!password) { showStatus('请输入密码', 'error'); return; } try { const response = await fetch('./auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); const data = await response.json(); if (response.ok) { AppState.authToken = data.token; localStorage.setItem('gcli2api_auth_token', AppState.authToken); document.getElementById('loginSection').classList.add('hidden'); document.getElementById('mainSection').classList.remove('hidden'); showStatus('登录成功', 'success'); // 显示面板后初始化滑块 requestAnimationFrame(() => initTabSlider()); } else { showStatus(`登录失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } async function autoLogin() { const savedToken = localStorage.getItem('gcli2api_auth_token'); if (!savedToken) return false; AppState.authToken = savedToken; try { const response = await fetch('./config/get', { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${AppState.authToken}` } }); if (response.ok) { document.getElementById('loginSection').classList.add('hidden'); document.getElementById('mainSection').classList.remove('hidden'); showStatus('自动登录成功', 'success'); // 显示面板后初始化滑块 requestAnimationFrame(() => initTabSlider()); return true; } else if (response.status === 401) { localStorage.removeItem('gcli2api_auth_token'); AppState.authToken = ''; return false; } return false; } catch (error) { return false; } } function logout() { localStorage.removeItem('gcli2api_auth_token'); AppState.authToken = ''; document.getElementById('loginSection').classList.remove('hidden'); document.getElementById('mainSection').classList.add('hidden'); showStatus('已退出登录', 'info'); const passwordInput = document.getElementById('loginPassword'); if (passwordInput) passwordInput.value = ''; } function handlePasswordEnter(event) { if (event.key === 'Enter') login(); } // ===================================================================== // 标签页切换 // ===================================================================== // 更新滑块位置 function updateTabSlider(targetTab, animate = true) { const slider = document.querySelector('.tab-slider'); const tabs = document.querySelector('.tabs'); if (!slider || !tabs || !targetTab) return; // 获取按钮位置和容器宽度 const tabLeft = targetTab.offsetLeft; const tabWidth = targetTab.offsetWidth; const tabsWidth = tabs.scrollWidth; // 使用 left 和 right 同时控制,确保动画同步 const rightValue = tabsWidth - tabLeft - tabWidth; if (animate) { slider.style.left = `${tabLeft}px`; slider.style.right = `${rightValue}px`; } else { // 首次加载时不使用动画 slider.style.transition = 'none'; slider.style.left = `${tabLeft}px`; slider.style.right = `${rightValue}px`; // 强制重绘后恢复过渡 slider.offsetHeight; slider.style.transition = ''; } } // 初始化滑块位置 function initTabSlider() { const activeTab = document.querySelector('.tab.active'); if (activeTab) { updateTabSlider(activeTab, false); } } // 页面加载和窗口大小变化时初始化滑块 document.addEventListener('DOMContentLoaded', initTabSlider); window.addEventListener('resize', () => { const activeTab = document.querySelector('.tab.active'); if (activeTab) updateTabSlider(activeTab, false); }); function switchTab(tabName) { // 获取当前活动的内容区域 const currentContent = document.querySelector('.tab-content.active'); const targetContent = document.getElementById(tabName + 'Tab'); // 如果点击的是当前标签页,不做任何操作 if (currentContent === targetContent) return; // 找到目标标签按钮 const targetTab = event && event.target ? event.target : document.querySelector(`.tab[onclick*="'${tabName}'"]`); // 移除所有标签页的active状态 document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); // 添加当前点击标签的active状态 if (targetTab) { targetTab.classList.add('active'); // 更新滑块位置(带动画) updateTabSlider(targetTab, true); } // 淡出当前内容 if (currentContent) { // 设置淡出过渡 currentContent.style.transition = 'opacity 0.18s ease-out, transform 0.18s ease-out'; currentContent.style.opacity = '0'; currentContent.style.transform = 'translateX(-12px)'; setTimeout(() => { currentContent.classList.remove('active'); currentContent.style.transition = ''; currentContent.style.opacity = ''; currentContent.style.transform = ''; // 淡入新内容 if (targetContent) { // 先设置初始状态(在添加 active 类之前) targetContent.style.opacity = '0'; targetContent.style.transform = 'translateX(12px)'; targetContent.style.transition = 'none'; // 暂时禁用过渡 // 添加 active 类使元素可见 targetContent.classList.add('active'); // 使用双重 requestAnimationFrame 确保浏览器完成重绘 requestAnimationFrame(() => { requestAnimationFrame(() => { // 启用过渡并应用最终状态 targetContent.style.transition = 'opacity 0.25s ease-out, transform 0.25s ease-out'; targetContent.style.opacity = '1'; targetContent.style.transform = 'translateX(0)'; // 清理内联样式并执行数据加载 setTimeout(() => { targetContent.style.transition = ''; targetContent.style.opacity = ''; targetContent.style.transform = ''; // 动画完成后触发数据加载 triggerTabDataLoad(tabName); }, 260); }); }); } }, 180); } else { // 如果没有当前内容(首次加载),直接显示目标内容 if (targetContent) { targetContent.classList.add('active'); // 直接触发数据加载 triggerTabDataLoad(tabName); } } } // 标签页数据加载(从动画中分离出来) function triggerTabDataLoad(tabName) { if (tabName === 'manage') AppState.creds.refresh(); if (tabName === 'antigravity-manage') AppState.antigravityCreds.refresh(); if (tabName === 'config') loadConfig(); if (tabName === 'logs') connectWebSocket(); } // ===================================================================== // OAuth认证相关函数 // ===================================================================== async function startAuth() { const projectId = document.getElementById('projectId').value.trim(); AppState.currentProjectId = projectId || null; const btn = document.getElementById('getAuthBtn'); btn.disabled = true; btn.textContent = '正在获取认证链接...'; try { const requestBody = projectId ? { project_id: projectId } : {}; showStatus(projectId ? '使用指定的项目ID生成认证链接...' : '将尝试自动检测项目ID,正在生成认证链接...', 'info'); const response = await fetch('./auth/start', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(requestBody) }); const data = await response.json(); if (response.ok) { document.getElementById('authUrl').href = data.auth_url; document.getElementById('authUrl').textContent = data.auth_url; document.getElementById('authUrlSection').classList.remove('hidden'); const msg = data.auto_project_detection ? '认证链接已生成(将在认证完成后自动检测项目ID),请点击链接完成授权' : `认证链接已生成(项目ID: ${data.detected_project_id}),请点击链接完成授权`; showStatus(msg, 'info'); AppState.authInProgress = true; } else { showStatus(`错误: ${data.error || '获取认证链接失败'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = '获取认证链接'; } } async function getCredentials() { if (!AppState.authInProgress) { showStatus('请先获取认证链接并完成授权', 'error'); return; } const btn = document.getElementById('getCredsBtn'); btn.disabled = true; btn.textContent = '等待OAuth回调中...'; try { showStatus('正在等待OAuth回调,这可能需要一些时间...', 'info'); const requestBody = AppState.currentProjectId ? { project_id: AppState.currentProjectId } : {}; const response = await fetch('./auth/callback', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(requestBody) }); const data = await response.json(); if (response.ok) { document.getElementById('credentialsContent').textContent = JSON.stringify(data.credentials, null, 2); const msg = data.auto_detected_project ? `✅ 认证成功!项目ID已自动检测为: ${data.credentials.project_id},文件已保存到: ${data.file_path}` : `✅ 认证成功!文件已保存到: ${data.file_path}`; showStatus(msg, 'success'); document.getElementById('credentialsSection').classList.remove('hidden'); AppState.authInProgress = false; } else if (data.requires_project_selection && data.available_projects) { let projectOptions = "请选择一个项目:\n\n"; data.available_projects.forEach((project, index) => { projectOptions += `${index + 1}. ${project.name} (${project.project_id})\n`; }); projectOptions += `\n请输入序号 (1-${data.available_projects.length}):`; const selection = prompt(projectOptions); const projectIndex = parseInt(selection) - 1; if (projectIndex >= 0 && projectIndex < data.available_projects.length) { AppState.currentProjectId = data.available_projects[projectIndex].project_id; btn.textContent = '重新尝试获取认证文件'; showStatus(`使用选择的项目重新尝试...`, 'info'); setTimeout(() => getCredentials(), 1000); return; } else { showStatus('无效的选择,请重新开始认证', 'error'); } } else if (data.requires_manual_project_id) { const userProjectId = prompt('无法自动检测项目ID,请手动输入您的Google Cloud项目ID:'); if (userProjectId && userProjectId.trim()) { AppState.currentProjectId = userProjectId.trim(); btn.textContent = '重新尝试获取认证文件'; showStatus('使用手动输入的项目ID重新尝试...', 'info'); setTimeout(() => getCredentials(), 1000); return; } else { showStatus('需要项目ID才能完成认证,请重新开始并输入正确的项目ID', 'error'); } } else { showStatus(`❌ 错误: ${data.error || '获取认证文件失败'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = '获取认证文件'; } } // ===================================================================== // Antigravity 认证相关函数 // ===================================================================== async function startAntigravityAuth() { const btn = document.getElementById('getAntigravityAuthBtn'); btn.disabled = true; btn.textContent = '生成认证链接中...'; try { showStatus('正在生成 Antigravity 认证链接...', 'info'); const response = await fetch('./auth/start', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ mode: 'antigravity' }) }); const data = await response.json(); if (response.ok) { AppState.antigravityAuthState = data.state; AppState.antigravityAuthInProgress = true; const authUrlLink = document.getElementById('antigravityAuthUrl'); authUrlLink.href = data.auth_url; authUrlLink.textContent = data.auth_url; document.getElementById('antigravityAuthUrlSection').classList.remove('hidden'); showStatus('✅ Antigravity 认证链接已生成!请点击链接完成授权', 'success'); } else { showStatus(`❌ 错误: ${data.error || '生成认证链接失败'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = '获取 Antigravity 认证链接'; } } async function getAntigravityCredentials() { if (!AppState.antigravityAuthInProgress) { showStatus('请先获取 Antigravity 认证链接并完成授权', 'error'); return; } const btn = document.getElementById('getAntigravityCredsBtn'); btn.disabled = true; btn.textContent = '等待OAuth回调中...'; try { showStatus('正在等待 Antigravity OAuth回调...', 'info'); const response = await fetch('./auth/callback', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ mode: 'antigravity' }) }); const data = await response.json(); if (response.ok) { document.getElementById('antigravityCredsContent').textContent = JSON.stringify(data.credentials, null, 2); document.getElementById('antigravityCredsSection').classList.remove('hidden'); AppState.antigravityAuthInProgress = false; showStatus(`✅ Antigravity 认证成功!文件已保存到: ${data.file_path}`, 'success'); } else { showStatus(`❌ 错误: ${data.error || '获取认证文件失败'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = '获取 Antigravity 凭证'; } } function downloadAntigravityCredentials() { const content = document.getElementById('antigravityCredsContent').textContent; const blob = new Blob([content], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `antigravity-credential-${Date.now()}.json`; a.click(); window.URL.revokeObjectURL(url); } // ===================================================================== // 回调URL处理 // ===================================================================== function toggleProjectIdSection() { const section = document.getElementById('projectIdSection'); const icon = document.getElementById('projectIdToggleIcon'); if (section.style.display === 'none') { section.style.display = 'block'; icon.style.transform = 'rotate(90deg)'; icon.textContent = '▼'; } else { section.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; icon.textContent = '▶'; } } function toggleCallbackUrlSection() { const section = document.getElementById('callbackUrlSection'); const icon = document.getElementById('callbackUrlToggleIcon'); if (section.style.display === 'none') { section.style.display = 'block'; icon.style.transform = 'rotate(180deg)'; icon.textContent = '▲'; } else { section.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; icon.textContent = '▼'; } } function toggleAntigravityCallbackUrlSection() { const section = document.getElementById('antigravityCallbackUrlSection'); const icon = document.getElementById('antigravityCallbackUrlToggleIcon'); if (section.style.display === 'none') { section.style.display = 'block'; icon.style.transform = 'rotate(180deg)'; icon.textContent = '▲'; } else { section.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; icon.textContent = '▼'; } } async function processCallbackUrl() { const callbackUrl = document.getElementById('callbackUrlInput').value.trim(); if (!callbackUrl) { showStatus('请输入回调URL', 'error'); return; } if (!callbackUrl.startsWith('http://') && !callbackUrl.startsWith('https://')) { showStatus('请输入有效的URL(以http://或https://开头)', 'error'); return; } if (!callbackUrl.includes('code=') || !callbackUrl.includes('state=')) { showStatus('❌ 这不是有效的回调URL!请确保:\n1. 已完成Google OAuth授权\n2. 复制的是浏览器地址栏的完整URL\n3. URL包含code和state参数', 'error'); return; } showStatus('正在从回调URL获取凭证...', 'info'); try { const projectId = document.getElementById('projectId')?.value.trim() || null; const response = await fetch('./auth/callback-url', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ callback_url: callbackUrl, project_id: projectId }) }); const result = await response.json(); if (result.credentials) { showStatus(result.message || '从回调URL获取凭证成功!', 'success'); document.getElementById('credentialsContent').innerHTML = '
' + JSON.stringify(result.credentials, null, 2) + '
'; document.getElementById('credentialsSection').classList.remove('hidden'); } else if (result.requires_manual_project_id) { showStatus('需要手动指定项目ID,请在高级选项中填入Google Cloud项目ID后重试', 'error'); } else if (result.requires_project_selection) { let msg = '
可用项目:
'; result.available_projects.forEach(p => { msg += `• ${p.name} (ID: ${p.project_id})
`; }); showStatus('检测到多个项目,请在高级选项中指定项目ID:' + msg, 'error'); } else { showStatus(result.error || '从回调URL获取凭证失败', 'error'); } document.getElementById('callbackUrlInput').value = ''; } catch (error) { showStatus(`从回调URL获取凭证失败: ${error.message}`, 'error'); } } async function processAntigravityCallbackUrl() { const callbackUrl = document.getElementById('antigravityCallbackUrlInput').value.trim(); if (!callbackUrl) { showStatus('请输入回调URL', 'error'); return; } if (!callbackUrl.startsWith('http://') && !callbackUrl.startsWith('https://')) { showStatus('请输入有效的URL(以http://或https://开头)', 'error'); return; } if (!callbackUrl.includes('code=') || !callbackUrl.includes('state=')) { showStatus('❌ 这不是有效的回调URL!请确保包含code和state参数', 'error'); return; } showStatus('正在从回调URL获取 Antigravity 凭证...', 'info'); try { const response = await fetch('./auth/callback-url', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ callback_url: callbackUrl, mode: 'antigravity' }) }); const result = await response.json(); if (result.credentials) { showStatus(result.message || '从回调URL获取 Antigravity 凭证成功!', 'success'); document.getElementById('antigravityCredsContent').textContent = JSON.stringify(result.credentials, null, 2); document.getElementById('antigravityCredsSection').classList.remove('hidden'); } else { showStatus(result.error || '从回调URL获取 Antigravity 凭证失败', 'error'); } document.getElementById('antigravityCallbackUrlInput').value = ''; } catch (error) { showStatus(`从回调URL获取 Antigravity 凭证失败: ${error.message}`, 'error'); } } // ===================================================================== // 全局兼容函数(供HTML调用) // ===================================================================== // 普通凭证管理 function refreshCredsStatus() { AppState.creds.refresh(); } function applyStatusFilter() { AppState.creds.applyStatusFilter(); } function changePage(direction) { AppState.creds.changePage(direction); } function changePageSize() { AppState.creds.changePageSize(); } function toggleFileSelection(filename) { if (AppState.creds.selectedFiles.has(filename)) { AppState.creds.selectedFiles.delete(filename); } else { AppState.creds.selectedFiles.add(filename); } AppState.creds.updateBatchControls(); } function toggleSelectAll() { const checkbox = document.getElementById('selectAllCheckbox'); const checkboxes = document.querySelectorAll('.file-checkbox'); if (checkbox.checked) { checkboxes.forEach(cb => AppState.creds.selectedFiles.add(cb.getAttribute('data-filename'))); } else { AppState.creds.selectedFiles.clear(); } checkboxes.forEach(cb => cb.checked = checkbox.checked); AppState.creds.updateBatchControls(); } function batchAction(action) { AppState.creds.batchAction(action); } function downloadCred(filename) { fetch(`./creds/download/${filename}`, { headers: { 'Authorization': `Bearer ${AppState.authToken}` } }) .then(r => r.ok ? r.blob() : Promise.reject()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); showStatus(`已下载文件: ${filename}`, 'success'); }) .catch(() => showStatus(`下载失败: ${filename}`, 'error')); } async function downloadAllCreds() { try { const response = await fetch('./creds/download-all', { headers: { 'Authorization': `Bearer ${AppState.authToken}` } }); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'credentials.zip'; a.click(); window.URL.revokeObjectURL(url); showStatus('已下载所有凭证文件', 'success'); } } catch (error) { showStatus(`打包下载失败: ${error.message}`, 'error'); } } // Antigravity凭证管理 function refreshAntigravityCredsList() { AppState.antigravityCreds.refresh(); } function applyAntigravityStatusFilter() { AppState.antigravityCreds.applyStatusFilter(); } function changeAntigravityPage(direction) { AppState.antigravityCreds.changePage(direction); } function changeAntigravityPageSize() { AppState.antigravityCreds.changePageSize(); } function toggleAntigravityFileSelection(filename) { if (AppState.antigravityCreds.selectedFiles.has(filename)) { AppState.antigravityCreds.selectedFiles.delete(filename); } else { AppState.antigravityCreds.selectedFiles.add(filename); } AppState.antigravityCreds.updateBatchControls(); } function toggleSelectAllAntigravity() { const checkbox = document.getElementById('selectAllAntigravityCheckbox'); const checkboxes = document.querySelectorAll('.antigravityFile-checkbox'); if (checkbox.checked) { checkboxes.forEach(cb => AppState.antigravityCreds.selectedFiles.add(cb.getAttribute('data-filename'))); } else { AppState.antigravityCreds.selectedFiles.clear(); } checkboxes.forEach(cb => cb.checked = checkbox.checked); AppState.antigravityCreds.updateBatchControls(); } function batchAntigravityAction(action) { AppState.antigravityCreds.batchAction(action); } function downloadAntigravityCred(filename) { fetch(`./creds/download/${filename}?mode=antigravity`, { headers: getAuthHeaders() }) .then(r => r.ok ? r.blob() : Promise.reject()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); showStatus(`✅ 已下载: ${filename}`, 'success'); }) .catch(() => showStatus(`下载失败: ${filename}`, 'error')); } function deleteAntigravityCred(filename) { if (confirm(`确定要删除 ${filename} 吗?`)) { AppState.antigravityCreds.action(filename, 'delete'); } } async function downloadAllAntigravityCreds() { try { const response = await fetch('./creds/download-all?mode=antigravity', { headers: getAuthHeaders() }); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `antigravity_credentials_${Date.now()}.zip`; a.click(); window.URL.revokeObjectURL(url); showStatus('✅ 所有Antigravity凭证已打包下载', 'success'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } // 文件上传 function handleFileSelect(event) { AppState.uploadFiles.handleFileSelect(event); } function removeFile(index) { AppState.uploadFiles.removeFile(index); } function clearFiles() { AppState.uploadFiles.clearFiles(); } function uploadFiles() { AppState.uploadFiles.upload(); } function handleAntigravityFileSelect(event) { AppState.antigravityUploadFiles.handleFileSelect(event); } function handleAntigravityFileDrop(event) { event.preventDefault(); event.currentTarget.style.borderColor = '#007bff'; event.currentTarget.style.backgroundColor = '#f8f9fa'; AppState.antigravityUploadFiles.addFiles(Array.from(event.dataTransfer.files)); } function removeAntigravityFile(index) { AppState.antigravityUploadFiles.removeFile(index); } function clearAntigravityFiles() { AppState.antigravityUploadFiles.clearFiles(); } function uploadAntigravityFiles() { AppState.antigravityUploadFiles.upload(); } // 邮箱相关 // 辅助函数:根据文件名更新卡片中的邮箱显示 function updateEmailDisplay(filename, email, managerType = 'normal') { // 查找对应的凭证卡片 const containerId = managerType === 'antigravity' ? 'antigravityCredsList' : 'credsList'; const container = document.getElementById(containerId); if (!container) return false; // 通过 data-filename 找到对应的复选框,再找到其父卡片 const checkbox = container.querySelector(`input[data-filename="${filename}"]`); if (!checkbox) return false; // 找到对应的 cred-card 元素 const card = checkbox.closest('.cred-card'); if (!card) return false; // 找到邮箱显示元素 const emailDiv = card.querySelector('.cred-email'); if (emailDiv) { emailDiv.textContent = email; emailDiv.style.color = '#666'; emailDiv.style.fontStyle = 'normal'; return true; } return false; } async function fetchUserEmail(filename) { try { showStatus('正在获取用户邮箱...', 'info'); const response = await fetch(`./creds/fetch-email/${encodeURIComponent(filename)}`, { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.user_email) { showStatus(`成功获取邮箱: ${data.user_email}`, 'success'); // 直接更新卡片中的邮箱显示,不刷新整个列表 updateEmailDisplay(filename, data.user_email, 'normal'); } else { showStatus(data.message || '无法获取用户邮箱', 'error'); } } catch (error) { showStatus(`获取邮箱失败: ${error.message}`, 'error'); } } async function fetchAntigravityUserEmail(filename) { try { showStatus('正在获取用户邮箱...', 'info'); const response = await fetch(`./creds/fetch-email/${encodeURIComponent(filename)}?mode=antigravity`, { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.user_email) { showStatus(`成功获取邮箱: ${data.user_email}`, 'success'); // 直接更新卡片中的邮箱显示,不刷新整个列表 updateEmailDisplay(filename, data.user_email, 'antigravity'); } else { showStatus(data.message || '无法获取用户邮箱', 'error'); } } catch (error) { showStatus(`获取邮箱失败: ${error.message}`, 'error'); } } async function verifyProjectId(filename) { try { // 显示加载状态 showStatus('🔍 正在检验Project ID,请稍候...', 'info'); const response = await fetch(`./creds/verify-project/${encodeURIComponent(filename)}`, { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.success) { // 成功时显示绿色成功消息和Project ID const successMsg = `✅ 检验成功!\n文件: ${filename}\nProject ID: ${data.project_id}\n\n${data.message}`; showStatus(successMsg.replace(/\n/g, '
'), 'success'); // 弹出成功提示 alert(`✅ 检验成功!\n\n文件: ${filename}\nProject ID: ${data.project_id}\n\n${data.message}`); await AppState.creds.refresh(); } else { // 失败时显示红色错误消息 const errorMsg = data.message || '检验失败'; showStatus(`❌ ${errorMsg}`, 'error'); alert(`❌ 检验失败\n\n${errorMsg}`); } } catch (error) { const errorMsg = `检验失败: ${error.message}`; showStatus(`❌ ${errorMsg}`, 'error'); alert(`❌ ${errorMsg}`); } } async function verifyAntigravityProjectId(filename) { try { // 显示加载状态 showStatus('🔍 正在检验Antigravity Project ID,请稍候...', 'info'); const response = await fetch(`./creds/verify-project/${encodeURIComponent(filename)}?mode=antigravity`, { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.success) { // 成功时显示绿色成功消息和Project ID const successMsg = `✅ 检验成功!\n文件: ${filename}\nProject ID: ${data.project_id}\n\n${data.message}`; showStatus(successMsg.replace(/\n/g, '
'), 'success'); // 弹出成功提示 alert(`✅ Antigravity检验成功!\n\n文件: ${filename}\nProject ID: ${data.project_id}\n\n${data.message}`); await AppState.antigravityCreds.refresh(); } else { // 失败时显示红色错误消息 const errorMsg = data.message || '检验失败'; showStatus(`❌ ${errorMsg}`, 'error'); alert(`❌ 检验失败\n\n${errorMsg}`); } } catch (error) { const errorMsg = `检验失败: ${error.message}`; showStatus(`❌ ${errorMsg}`, 'error'); alert(`❌ ${errorMsg}`); } } async function toggleAntigravityQuotaDetails(pathId) { const quotaDetails = document.getElementById('quota-' + pathId); if (!quotaDetails) return; // 切换显示状态 const isShowing = quotaDetails.style.display === 'block'; if (isShowing) { // 收起 quotaDetails.style.display = 'none'; } else { // 展开 quotaDetails.style.display = 'block'; const contentDiv = quotaDetails.querySelector('.cred-quota-content'); const filename = contentDiv.getAttribute('data-filename'); const loaded = contentDiv.getAttribute('data-loaded'); // 如果还没加载过,则加载数据 if (loaded === 'false' && filename) { contentDiv.innerHTML = '
📊 正在加载额度信息...
'; try { const response = await fetch(`./creds/quota/${encodeURIComponent(filename)}?mode=antigravity`, { method: 'GET', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.success) { // 成功时渲染美化的额度信息 const models = data.models || {}; if (Object.keys(models).length === 0) { contentDiv.innerHTML = `
📊
暂无额度信息
`; } else { let quotaHTML = `

📊 额度信息详情

文件: ${filename}
`; for (const [modelName, quotaData] of Object.entries(models)) { // 后端返回的是剩余比例 (0-1),不是绝对数量 const remainingFraction = quotaData.remaining || 0; const resetTime = quotaData.resetTime || 'N/A'; // 计算已使用百分比(1 - 剩余比例) const usedPercentage = Math.round((1 - remainingFraction) * 100); const remainingPercentage = Math.round(remainingFraction * 100); // 根据使用情况选择颜色 let percentageColor = '#28a745'; // 绿色:使用少 if (usedPercentage >= 90) percentageColor = '#dc3545'; // 红色:使用多 else if (usedPercentage >= 70) percentageColor = '#ffc107'; // 黄色:使用较多 else if (usedPercentage >= 50) percentageColor = '#17a2b8'; // 蓝色:使用中等 quotaHTML += `
${modelName}
${remainingPercentage}%
${resetTime !== 'N/A' ? '🔄 ' + resetTime : ''}
`; } quotaHTML += '
'; contentDiv.innerHTML = quotaHTML; } contentDiv.setAttribute('data-loaded', 'true'); showStatus('✅ 成功加载额度信息', 'success'); } else { // 失败时显示错误 const errorMsg = data.error || '获取额度信息失败'; contentDiv.innerHTML = `
获取额度信息失败
${errorMsg}
`; showStatus(`❌ ${errorMsg}`, 'error'); } } catch (error) { contentDiv.innerHTML = `
网络错误
${error.message}
`; showStatus(`❌ 获取额度信息失败: ${error.message}`, 'error'); } } } } async function batchVerifyProjectIds() { const selectedFiles = Array.from(AppState.creds.selectedFiles); if (selectedFiles.length === 0) { showStatus('❌ 请先选择要检验的凭证', 'error'); alert('请先选择要检验的凭证'); return; } if (!confirm(`确定要批量检验 ${selectedFiles.length} 个凭证的Project ID吗?\n\n将并行检验以加快速度。`)) { return; } showStatus(`🔍 正在并行检验 ${selectedFiles.length} 个凭证,请稍候...`, 'info'); // 并行执行所有检验请求 const promises = selectedFiles.map(async (filename) => { try { const response = await fetch(`./creds/verify-project/${encodeURIComponent(filename)}`, { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.success) { return { success: true, filename, projectId: data.project_id, message: data.message }; } else { return { success: false, filename, error: data.message || '失败' }; } } catch (error) { return { success: false, filename, error: error.message }; } }); // 等待所有请求完成 const results = await Promise.all(promises); // 统计结果 let successCount = 0; let failCount = 0; const resultMessages = []; results.forEach(result => { if (result.success) { successCount++; resultMessages.push(`✅ ${result.filename}: ${result.projectId}`); } else { failCount++; resultMessages.push(`❌ ${result.filename}: ${result.error}`); } }); await AppState.creds.refresh(); const summary = `批量检验完成!\n\n成功: ${successCount} 个\n失败: ${failCount} 个\n总计: ${selectedFiles.length} 个\n\n详细结果:\n${resultMessages.join('\n')}`; if (failCount === 0) { showStatus(`✅ 全部检验成功!成功检验 ${successCount}/${selectedFiles.length} 个凭证`, 'success'); } else if (successCount === 0) { showStatus(`❌ 全部检验失败!失败 ${failCount}/${selectedFiles.length} 个凭证`, 'error'); } else { showStatus(`⚠️ 批量检验完成:成功 ${successCount}/${selectedFiles.length} 个,失败 ${failCount} 个`, 'info'); } console.log(summary); alert(summary); } async function batchVerifyAntigravityProjectIds() { const selectedFiles = Array.from(AppState.antigravityCreds.selectedFiles); if (selectedFiles.length === 0) { showStatus('❌ 请先选择要检验的Antigravity凭证', 'error'); alert('请先选择要检验的Antigravity凭证'); return; } if (!confirm(`确定要批量检验 ${selectedFiles.length} 个Antigravity凭证的Project ID吗?\n\n将并行检验以加快速度。`)) { return; } showStatus(`🔍 正在并行检验 ${selectedFiles.length} 个Antigravity凭证,请稍候...`, 'info'); // 并行执行所有检验请求 const promises = selectedFiles.map(async (filename) => { try { const response = await fetch(`./creds/verify-project/${encodeURIComponent(filename)}?mode=antigravity`, { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok && data.success) { return { success: true, filename, projectId: data.project_id, message: data.message }; } else { return { success: false, filename, error: data.message || '失败' }; } } catch (error) { return { success: false, filename, error: error.message }; } }); // 等待所有请求完成 const results = await Promise.all(promises); // 统计结果 let successCount = 0; let failCount = 0; const resultMessages = []; results.forEach(result => { if (result.success) { successCount++; resultMessages.push(`✅ ${result.filename}: ${result.projectId}`); } else { failCount++; resultMessages.push(`❌ ${result.filename}: ${result.error}`); } }); await AppState.antigravityCreds.refresh(); const summary = `Antigravity批量检验完成!\n\n成功: ${successCount} 个\n失败: ${failCount} 个\n总计: ${selectedFiles.length} 个\n\n详细结果:\n${resultMessages.join('\n')}`; if (failCount === 0) { showStatus(`✅ 全部检验成功!成功检验 ${successCount}/${selectedFiles.length} 个Antigravity凭证`, 'success'); } else if (successCount === 0) { showStatus(`❌ 全部检验失败!失败 ${failCount}/${selectedFiles.length} 个Antigravity凭证`, 'error'); } else { showStatus(`⚠️ 批量检验完成:成功 ${successCount}/${selectedFiles.length} 个,失败 ${failCount} 个`, 'info'); } console.log(summary); alert(summary); } async function refreshAllEmails() { if (!confirm('确定要刷新所有凭证的用户邮箱吗?这可能需要一些时间。')) return; try { showStatus('正在刷新所有用户邮箱...', 'info'); const response = await fetch('./creds/refresh-all-emails', { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { showStatus(`邮箱刷新完成:成功获取 ${data.success_count}/${data.total_count} 个邮箱地址`, 'success'); await AppState.creds.refresh(); } else { showStatus(data.message || '邮箱刷新失败', 'error'); } } catch (error) { showStatus(`邮箱刷新网络错误: ${error.message}`, 'error'); } } async function refreshAllAntigravityEmails() { if (!confirm('确定要刷新所有Antigravity凭证的用户邮箱吗?这可能需要一些时间。')) return; try { showStatus('正在刷新所有用户邮箱...', 'info'); const response = await fetch('./creds/refresh-all-emails?mode=antigravity', { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { showStatus(`邮箱刷新完成:成功获取 ${data.success_count}/${data.total_count} 个邮箱地址`, 'success'); await AppState.antigravityCreds.refresh(); } else { showStatus(data.message || '邮箱刷新失败', 'error'); } } catch (error) { showStatus(`邮箱刷新网络错误: ${error.message}`, 'error'); } } async function deduplicateByEmail() { if (!confirm('确定要对凭证进行凭证一键去重吗?\n\n相同邮箱的凭证只保留一个,其他将被删除。\n此操作不可撤销!')) return; try { showStatus('正在进行凭证一键去重...', 'info'); const response = await fetch('./creds/deduplicate-by-email', { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { const msg = `去重完成:删除 ${data.deleted_count} 个重复凭证,保留 ${data.kept_count} 个凭证(${data.unique_emails_count} 个唯一邮箱)`; showStatus(msg, 'success'); await AppState.creds.refresh(); // 显示详细信息 if (data.duplicate_groups && data.duplicate_groups.length > 0) { let details = '去重详情:\n\n'; data.duplicate_groups.forEach(group => { details += `邮箱: ${group.email}\n保留: ${group.kept_file}\n删除: ${group.deleted_files.join(', ')}\n\n`; }); console.log(details); } } else { showStatus(data.message || '去重失败', 'error'); } } catch (error) { showStatus(`去重网络错误: ${error.message}`, 'error'); } } async function deduplicateAntigravityByEmail() { if (!confirm('确定要对Antigravity凭证进行凭证一键去重吗?\n\n相同邮箱的凭证只保留一个,其他将被删除。\n此操作不可撤销!')) return; try { showStatus('正在进行凭证一键去重...', 'info'); const response = await fetch('./creds/deduplicate-by-email?mode=antigravity', { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { const msg = `去重完成:删除 ${data.deleted_count} 个重复凭证,保留 ${data.kept_count} 个凭证(${data.unique_emails_count} 个唯一邮箱)`; showStatus(msg, 'success'); await AppState.antigravityCreds.refresh(); // 显示详细信息 if (data.duplicate_groups && data.duplicate_groups.length > 0) { let details = '去重详情:\n\n'; data.duplicate_groups.forEach(group => { details += `邮箱: ${group.email}\n保留: ${group.kept_file}\n删除: ${group.deleted_files.join(', ')}\n\n`; }); console.log(details); } } else { showStatus(data.message || '去重失败', 'error'); } } catch (error) { showStatus(`去重网络错误: ${error.message}`, 'error'); } } // ===================================================================== // WebSocket日志相关 // ===================================================================== function connectWebSocket() { if (AppState.logWebSocket && AppState.logWebSocket.readyState === WebSocket.OPEN) { showStatus('WebSocket已经连接', 'info'); return; } try { const wsPath = new URL('./auth/logs/stream', window.location.href).href; const wsUrl = wsPath.replace(/^http/, 'ws'); document.getElementById('connectionStatusText').textContent = '连接中...'; document.getElementById('logConnectionStatus').className = 'status info'; AppState.logWebSocket = new WebSocket(wsUrl); AppState.logWebSocket.onopen = () => { document.getElementById('connectionStatusText').textContent = '已连接'; document.getElementById('logConnectionStatus').className = 'status success'; showStatus('日志流连接成功', 'success'); clearLogsDisplay(); }; AppState.logWebSocket.onmessage = (event) => { const logLine = event.data; if (logLine.trim()) { AppState.allLogs.push(logLine); if (AppState.allLogs.length > 1000) { AppState.allLogs = AppState.allLogs.slice(-1000); } filterLogs(); if (document.getElementById('autoScroll').checked) { const logContainer = document.getElementById('logContainer'); logContainer.scrollTop = logContainer.scrollHeight; } } }; AppState.logWebSocket.onclose = () => { document.getElementById('connectionStatusText').textContent = '连接断开'; document.getElementById('logConnectionStatus').className = 'status error'; showStatus('日志流连接断开', 'info'); }; AppState.logWebSocket.onerror = (error) => { document.getElementById('connectionStatusText').textContent = '连接错误'; document.getElementById('logConnectionStatus').className = 'status error'; showStatus('日志流连接错误: ' + error, 'error'); }; } catch (error) { showStatus('创建WebSocket连接失败: ' + error.message, 'error'); document.getElementById('connectionStatusText').textContent = '连接失败'; document.getElementById('logConnectionStatus').className = 'status error'; } } function disconnectWebSocket() { if (AppState.logWebSocket) { AppState.logWebSocket.close(); AppState.logWebSocket = null; document.getElementById('connectionStatusText').textContent = '未连接'; document.getElementById('logConnectionStatus').className = 'status info'; showStatus('日志流连接已断开', 'info'); } } function clearLogsDisplay() { AppState.allLogs = []; AppState.filteredLogs = []; document.getElementById('logContent').textContent = '日志已清空,等待新日志...'; } async function downloadLogs() { try { const response = await fetch('./auth/logs/download', { headers: getAuthHeaders() }); if (response.ok) { const contentDisposition = response.headers.get('Content-Disposition'); let filename = 'gcli2api_logs.txt'; if (contentDisposition) { const match = contentDisposition.match(/filename=(.+)/); if (match) filename = match[1]; } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); showStatus(`日志文件下载成功: ${filename}`, 'success'); } else { const data = await response.json(); showStatus(`下载日志失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`下载日志时网络错误: ${error.message}`, 'error'); } } async function clearLogs() { try { const response = await fetch('./auth/logs/clear', { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { clearLogsDisplay(); showStatus(data.message, 'success'); } else { showStatus(`清空日志失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { clearLogsDisplay(); showStatus(`清空日志时网络错误: ${error.message}`, 'error'); } } function filterLogs() { const filter = document.getElementById('logLevelFilter').value; AppState.currentLogFilter = filter; if (filter === 'all') { AppState.filteredLogs = [...AppState.allLogs]; } else { AppState.filteredLogs = AppState.allLogs.filter(log => log.toUpperCase().includes(filter)); } displayLogs(); } function displayLogs() { const logContent = document.getElementById('logContent'); if (AppState.filteredLogs.length === 0) { logContent.textContent = AppState.currentLogFilter === 'all' ? '暂无日志...' : `暂无${AppState.currentLogFilter}级别的日志...`; } else { logContent.textContent = AppState.filteredLogs.join('\n'); } } // ===================================================================== // 环境变量凭证管理 // ===================================================================== async function checkEnvCredsStatus() { const loading = document.getElementById('envStatusLoading'); const content = document.getElementById('envStatusContent'); try { loading.style.display = 'block'; content.classList.add('hidden'); const response = await fetch('./auth/env-creds-status', { headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { const envVarsList = document.getElementById('envVarsList'); envVarsList.textContent = Object.keys(data.available_env_vars).length > 0 ? Object.keys(data.available_env_vars).join(', ') : '未找到GCLI_CREDS_*环境变量'; const autoLoadStatus = document.getElementById('autoLoadStatus'); autoLoadStatus.textContent = data.auto_load_enabled ? '✅ 已启用' : '❌ 未启用'; autoLoadStatus.style.color = data.auto_load_enabled ? '#28a745' : '#dc3545'; document.getElementById('envFilesCount').textContent = `${data.existing_env_files_count} 个文件`; const envFilesList = document.getElementById('envFilesList'); envFilesList.textContent = data.existing_env_files.length > 0 ? data.existing_env_files.join(', ') : '无'; content.classList.remove('hidden'); showStatus('环境变量状态检查完成', 'success'); } else { showStatus(`获取环境变量状态失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { loading.style.display = 'none'; } } async function loadEnvCredentials() { try { showStatus('正在从环境变量导入凭证...', 'info'); const response = await fetch('./auth/load-env-creds', { method: 'POST', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { if (data.loaded_count > 0) { showStatus(`✅ 成功导入 ${data.loaded_count}/${data.total_count} 个凭证文件`, 'success'); setTimeout(() => checkEnvCredsStatus(), 1000); } else { showStatus(`⚠️ ${data.message}`, 'info'); } } else { showStatus(`导入失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } async function clearEnvCredentials() { if (!confirm('确定要清除所有从环境变量导入的凭证文件吗?\n这将删除所有文件名以 "env-" 开头的认证文件。')) { return; } try { showStatus('正在清除环境变量凭证文件...', 'info'); const response = await fetch('./auth/env-creds', { method: 'DELETE', headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { showStatus(`✅ 成功删除 ${data.deleted_count} 个环境变量凭证文件`, 'success'); setTimeout(() => checkEnvCredsStatus(), 1000); } else { showStatus(`清除失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } // ===================================================================== // 配置管理 // ===================================================================== async function loadConfig() { const loading = document.getElementById('configLoading'); const form = document.getElementById('configForm'); try { loading.style.display = 'block'; form.classList.add('hidden'); const response = await fetch('./config/get', { headers: getAuthHeaders() }); const data = await response.json(); if (response.ok) { AppState.currentConfig = data.config; AppState.envLockedFields = new Set(data.env_locked || []); populateConfigForm(); form.classList.remove('hidden'); showStatus('配置加载成功', 'success'); } else { showStatus(`加载配置失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { loading.style.display = 'none'; } } function populateConfigForm() { const c = AppState.currentConfig; setConfigField('host', c.host || '0.0.0.0'); setConfigField('port', c.port || 7861); setConfigField('configApiPassword', c.api_password || ''); setConfigField('configPanelPassword', c.panel_password || ''); setConfigField('configPassword', c.password || 'pwd'); setConfigField('credentialsDir', c.credentials_dir || ''); setConfigField('proxy', c.proxy || ''); setConfigField('codeAssistEndpoint', c.code_assist_endpoint || ''); setConfigField('oauthProxyUrl', c.oauth_proxy_url || ''); setConfigField('googleapisProxyUrl', c.googleapis_proxy_url || ''); setConfigField('resourceManagerApiUrl', c.resource_manager_api_url || ''); setConfigField('serviceUsageApiUrl', c.service_usage_api_url || ''); setConfigField('antigravityApiUrl', c.antigravity_api_url || ''); document.getElementById('autoBanEnabled').checked = Boolean(c.auto_ban_enabled); setConfigField('autoBanErrorCodes', (c.auto_ban_error_codes || []).join(',')); setConfigField('callsPerRotation', c.calls_per_rotation || 10); document.getElementById('retry429Enabled').checked = Boolean(c.retry_429_enabled); setConfigField('retry429MaxRetries', c.retry_429_max_retries || 20); setConfigField('retry429Interval', c.retry_429_interval || 0.1); document.getElementById('compatibilityModeEnabled').checked = Boolean(c.compatibility_mode_enabled); document.getElementById('returnThoughtsToFrontend').checked = Boolean(c.return_thoughts_to_frontend !== false); document.getElementById('antigravityStream2nostream').checked = Boolean(c.antigravity_stream2nostream !== false); setConfigField('antiTruncationMaxAttempts', c.anti_truncation_max_attempts || 3); } function setConfigField(fieldId, value) { const field = document.getElementById(fieldId); if (field) { field.value = value; const configKey = fieldId.replace(/([A-Z])/g, '_$1').toLowerCase(); if (AppState.envLockedFields.has(configKey)) { field.disabled = true; field.classList.add('env-locked'); } else { field.disabled = false; field.classList.remove('env-locked'); } } } async function saveConfig() { try { const getValue = (id, def = '') => document.getElementById(id)?.value.trim() || def; const getInt = (id, def = 0) => parseInt(document.getElementById(id)?.value) || def; const getFloat = (id, def = 0.0) => parseFloat(document.getElementById(id)?.value) || def; const getChecked = (id, def = false) => document.getElementById(id)?.checked || def; const config = { host: getValue('host', '0.0.0.0'), port: getInt('port', 7861), api_password: getValue('configApiPassword'), panel_password: getValue('configPanelPassword'), password: getValue('configPassword', 'pwd'), code_assist_endpoint: getValue('codeAssistEndpoint'), credentials_dir: getValue('credentialsDir'), proxy: getValue('proxy'), oauth_proxy_url: getValue('oauthProxyUrl'), googleapis_proxy_url: getValue('googleapisProxyUrl'), resource_manager_api_url: getValue('resourceManagerApiUrl'), service_usage_api_url: getValue('serviceUsageApiUrl'), antigravity_api_url: getValue('antigravityApiUrl'), auto_ban_enabled: getChecked('autoBanEnabled'), auto_ban_error_codes: getValue('autoBanErrorCodes').split(',') .map(c => parseInt(c.trim())).filter(c => !isNaN(c)), calls_per_rotation: getInt('callsPerRotation', 10), retry_429_enabled: getChecked('retry429Enabled'), retry_429_max_retries: getInt('retry429MaxRetries', 20), retry_429_interval: getFloat('retry429Interval', 0.1), compatibility_mode_enabled: getChecked('compatibilityModeEnabled'), return_thoughts_to_frontend: getChecked('returnThoughtsToFrontend'), antigravity_stream2nostream: getChecked('antigravityStream2nostream'), anti_truncation_max_attempts: getInt('antiTruncationMaxAttempts', 3) }; const response = await fetch('./config/save', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ config }) }); const data = await response.json(); if (response.ok) { let message = '配置保存成功'; if (data.hot_updated && data.hot_updated.length > 0) { message += `,以下配置已立即生效: ${data.hot_updated.join(', ')}`; } if (data.restart_required && data.restart_required.length > 0) { message += `\n⚠️ 重启提醒: ${data.restart_notice}`; showStatus(message, 'info'); } else { showStatus(message, 'success'); } setTimeout(() => loadConfig(), 1000); } else { showStatus(`保存配置失败: ${data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } // 镜像网址配置 const mirrorUrls = { codeAssistEndpoint: 'https://gcli-api.sukaka.top/cloudcode-pa', oauthProxyUrl: 'https://gcli-api.sukaka.top/oauth2', googleapisProxyUrl: 'https://gcli-api.sukaka.top/googleapis', resourceManagerApiUrl: 'https://gcli-api.sukaka.top/cloudresourcemanager', serviceUsageApiUrl: 'https://gcli-api.sukaka.top/serviceusage', antigravityApiUrl: 'https://gcli-api.sukaka.top/daily-cloudcode-pa' }; const officialUrls = { codeAssistEndpoint: 'https://cloudcode-pa.googleapis.com', oauthProxyUrl: 'https://oauth2.googleapis.com', googleapisProxyUrl: 'https://www.googleapis.com', resourceManagerApiUrl: 'https://cloudresourcemanager.googleapis.com', serviceUsageApiUrl: 'https://serviceusage.googleapis.com', antigravityApiUrl: 'https://daily-cloudcode-pa.sandbox.googleapis.com' }; function useMirrorUrls() { if (confirm('确定要将所有端点配置为镜像网址吗?')) { for (const [fieldId, url] of Object.entries(mirrorUrls)) { const field = document.getElementById(fieldId); if (field && !field.disabled) field.value = url; } showStatus('✅ 已切换到镜像网址配置,记得点击"保存配置"按钮保存设置', 'success'); } } function restoreOfficialUrls() { if (confirm('确定要将所有端点配置为官方地址吗?')) { for (const [fieldId, url] of Object.entries(officialUrls)) { const field = document.getElementById(fieldId); if (field && !field.disabled) field.value = url; } showStatus('✅ 已切换到官方端点配置,记得点击"保存配置"按钮保存设置', 'success'); } } // ===================================================================== // 使用统计 // ===================================================================== async function refreshUsageStats() { const loading = document.getElementById('usageLoading'); const list = document.getElementById('usageList'); try { loading.style.display = 'block'; list.innerHTML = ''; const [statsResponse, aggregatedResponse] = await Promise.all([ fetch('./usage/stats', { headers: getAuthHeaders() }), fetch('./usage/aggregated', { headers: getAuthHeaders() }) ]); if (statsResponse.status === 401 || aggregatedResponse.status === 401) { showStatus('认证失败,请重新登录', 'error'); setTimeout(() => location.reload(), 1500); return; } const statsData = await statsResponse.json(); const aggregatedData = await aggregatedResponse.json(); if (statsResponse.ok && aggregatedResponse.ok) { AppState.usageStatsData = statsData.success ? statsData.data : statsData; const aggData = aggregatedData.success ? aggregatedData.data : aggregatedData; document.getElementById('totalApiCalls').textContent = aggData.total_calls_24h || 0; document.getElementById('totalFiles').textContent = aggData.total_files || 0; document.getElementById('avgCallsPerFile').textContent = (aggData.avg_calls_per_file || 0).toFixed(1); renderUsageList(); showStatus(`已加载 ${aggData.total_files || Object.keys(AppState.usageStatsData).length} 个文件的使用统计`, 'success'); } else { const errorMsg = statsData.detail || aggregatedData.detail || '加载使用统计失败'; showStatus(`错误: ${errorMsg}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } finally { loading.style.display = 'none'; } } function renderUsageList() { const list = document.getElementById('usageList'); list.innerHTML = ''; if (Object.keys(AppState.usageStatsData).length === 0) { list.innerHTML = '

暂无使用统计数据

'; return; } for (const [filename, stats] of Object.entries(AppState.usageStatsData)) { const card = document.createElement('div'); card.className = 'usage-card'; const calls24h = stats.calls_24h || 0; card.innerHTML = `
${filename}
24小时内调用次数 ${calls24h}
`; list.appendChild(card); } } async function resetSingleUsageStats(filename) { if (!confirm(`确定要重置 ${filename} 的使用统计吗?`)) return; try { const response = await fetch('./usage/reset', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ filename }) }); const data = await response.json(); if (response.ok && data.success) { showStatus(data.message, 'success'); await refreshUsageStats(); } else { showStatus(`重置失败: ${data.message || data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } async function resetAllUsageStats() { if (!confirm('确定要重置所有文件的使用统计吗?此操作不可恢复!')) return; try { const response = await fetch('./usage/reset', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({}) }); const data = await response.json(); if (response.ok && data.success) { showStatus(data.message, 'success'); await refreshUsageStats(); } else { showStatus(`重置失败: ${data.message || data.detail || data.error || '未知错误'}`, 'error'); } } catch (error) { showStatus(`网络错误: ${error.message}`, 'error'); } } // ===================================================================== // 冷却倒计时自动更新 // ===================================================================== function startCooldownTimer() { if (AppState.cooldownTimerInterval) { clearInterval(AppState.cooldownTimerInterval); } AppState.cooldownTimerInterval = setInterval(() => { updateCooldownDisplays(); }, 1000); } function stopCooldownTimer() { if (AppState.cooldownTimerInterval) { clearInterval(AppState.cooldownTimerInterval); AppState.cooldownTimerInterval = null; } } function updateCooldownDisplays() { let needsRefresh = false; // 检查模型级冷却是否过期 for (const credInfo of Object.values(AppState.creds.data)) { if (credInfo.model_cooldowns && Object.keys(credInfo.model_cooldowns).length > 0) { const currentTime = Date.now() / 1000; const hasExpiredCooldowns = Object.entries(credInfo.model_cooldowns).some(([, until]) => until <= currentTime); if (hasExpiredCooldowns) { needsRefresh = true; break; } } } if (needsRefresh) { AppState.creds.renderList(); return; } // 更新模型级冷却的显示 document.querySelectorAll('.cooldown-badge').forEach(badge => { const card = badge.closest('.cred-card'); const filenameEl = card?.querySelector('.cred-filename'); if (!filenameEl) return; const filename = filenameEl.textContent; const credInfo = Object.values(AppState.creds.data).find(c => c.filename === filename); if (credInfo && credInfo.model_cooldowns) { const currentTime = Date.now() / 1000; const titleMatch = badge.getAttribute('title')?.match(/模型: (.+)/); if (titleMatch) { const model = titleMatch[1]; const cooldownUntil = credInfo.model_cooldowns[model]; if (cooldownUntil) { const remaining = Math.max(0, Math.floor(cooldownUntil - currentTime)); if (remaining > 0) { const shortModel = model.replace('gemini-', '').replace('-exp', '') .replace('2.0-', '2-').replace('1.5-', '1.5-'); const timeDisplay = formatCooldownTime(remaining).replace(/s$/, '').replace(/ /g, ''); badge.innerHTML = `🔧 ${shortModel}: ${timeDisplay}`; } } } } }); } // ===================================================================== // 版本信息管理 // ===================================================================== // 获取并显示版本信息(不检查更新) async function fetchAndDisplayVersion() { try { const response = await fetch('./version/info'); const data = await response.json(); const versionText = document.getElementById('versionText'); if (data.success) { // 只显示版本号 versionText.textContent = `v${data.version}`; versionText.title = `完整版本: ${data.full_hash}\n提交信息: ${data.message}\n提交时间: ${data.date}`; versionText.style.cursor = 'help'; } else { versionText.textContent = '未知版本'; versionText.title = data.error || '无法获取版本信息'; } } catch (error) { console.error('获取版本信息失败:', error); const versionText = document.getElementById('versionText'); if (versionText) { versionText.textContent = '版本信息获取失败'; } } } // 检查更新 async function checkForUpdates() { const checkBtn = document.getElementById('checkUpdateBtn'); if (!checkBtn) return; const originalText = checkBtn.textContent; try { // 显示检查中状态 checkBtn.textContent = '检查中...'; checkBtn.disabled = true; // 调用API检查更新 const response = await fetch('./version/info?check_update=true'); const data = await response.json(); if (data.success) { if (data.check_update === false) { // 检查更新失败 showStatus(`检查更新失败: ${data.update_error || '未知错误'}`, 'error'); } else if (data.has_update === true) { // 有更新 const updateMsg = `发现新版本!\n当前: v${data.version}\n最新: v${data.latest_version}\n\n更新内容: ${data.latest_message || '无'}`; showStatus(updateMsg.replace(/\n/g, ' '), 'warning'); // 更新按钮样式 checkBtn.style.backgroundColor = '#ffc107'; checkBtn.textContent = '有新版本'; setTimeout(() => { checkBtn.style.backgroundColor = '#17a2b8'; checkBtn.textContent = originalText; }, 5000); } else if (data.has_update === false) { // 已是最新 showStatus('已是最新版本!', 'success'); checkBtn.style.backgroundColor = '#28a745'; checkBtn.textContent = '已是最新'; setTimeout(() => { checkBtn.style.backgroundColor = '#17a2b8'; checkBtn.textContent = originalText; }, 3000); } else { // 无法确定 showStatus('无法确定是否有更新', 'info'); } } else { showStatus(`检查更新失败: ${data.error}`, 'error'); } } catch (error) { console.error('检查更新失败:', error); showStatus(`检查更新失败: ${error.message}`, 'error'); } finally { checkBtn.disabled = false; if (checkBtn.textContent === '检查中...') { checkBtn.textContent = originalText; } } } // ===================================================================== // 页面初始化 // ===================================================================== window.onload = async function () { const autoLoginSuccess = await autoLogin(); if (!autoLoginSuccess) { showStatus('请输入密码登录', 'info'); } else { // 登录成功后获取版本信息 await fetchAndDisplayVersion(); } startCooldownTimer(); const antigravityAuthBtn = document.getElementById('getAntigravityAuthBtn'); if (antigravityAuthBtn) { antigravityAuthBtn.addEventListener('click', startAntigravityAuth); } }; // 拖拽功能 - 初始化 document.addEventListener('DOMContentLoaded', function () { const uploadArea = document.getElementById('uploadArea'); if (uploadArea) { uploadArea.addEventListener('dragover', (event) => { event.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', (event) => { event.preventDefault(); uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (event) => { event.preventDefault(); uploadArea.classList.remove('dragover'); AppState.uploadFiles.addFiles(Array.from(event.dataTransfer.files)); }); } });