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 = `
+
+ ${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 = `
+
+
+
+ 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));
+ });
+ }
+});