// =====================================================================
// 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 = `
`;
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 = `
`;
showStatus(`❌ ${errorMsg}`, 'error');
}
} catch (error) {
contentDiv.innerHTML = `
`;
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 = `
`;
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));
});
}
});