diff --git "a/front/common.js" "b/front/common.js" new file mode 100644--- /dev/null +++ "b/front/common.js" @@ -0,0 +1,2669 @@ +// ===================================================================== +// 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)); + }); + } +});