|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const AppState = { |
|
|
|
|
|
authToken: '', |
|
|
authInProgress: false, |
|
|
currentProjectId: '', |
|
|
|
|
|
|
|
|
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 }, |
|
|
|
|
|
|
|
|
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] || ''; |
|
|
}, |
|
|
|
|
|
|
|
|
getModeParam: () => modeParam, |
|
|
|
|
|
|
|
|
getElementId: (suffix) => { |
|
|
|
|
|
|
|
|
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 = `<p style="text-align: center; color: #666;">${msg}</p>`; |
|
|
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) => { |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<div> |
|
|
<span class="file-name">${fileIcon} ${file.name}</span> |
|
|
<span class="file-size">(${formatFileSize(file.size)}${fileType})</span> |
|
|
</div> |
|
|
<button class="remove-btn" onclick="${type === 'antigravity' ? 'removeAntigravityFile' : 'removeFile'}(${index})">删除</button> |
|
|
`; |
|
|
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; |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
statusSection.innerHTML = `<div class="status ${type}">${message}</div>`; |
|
|
const statusDiv = statusSection.querySelector('.status'); |
|
|
|
|
|
|
|
|
statusDiv.offsetHeight; |
|
|
statusDiv.classList.add('show'); |
|
|
|
|
|
|
|
|
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 |
|
|
? '<span class="status-badge disabled">已禁用</span>' |
|
|
: '<span class="status-badge enabled">已启用</span>'; |
|
|
|
|
|
if (status.error_codes && status.error_codes.length > 0) { |
|
|
statusBadges += `<span class="error-codes">错误码: ${status.error_codes.join(', ')}</span>`; |
|
|
const autoBan = status.error_codes.filter(c => c === 400 || c === 403); |
|
|
if (autoBan.length > 0 && status.disabled) { |
|
|
statusBadges += '<span class="status-badge" style="background-color: #e74c3c; color: white;">AUTO_BAN</span>'; |
|
|
} |
|
|
} else { |
|
|
statusBadges += '<span class="status-badge" style="background-color: #28a745; color: white;">无错误</span>'; |
|
|
} |
|
|
|
|
|
|
|
|
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 += `<span class="cooldown-badge" style="background-color: #17a2b8;" title="模型: ${item.fullModel}">🔧 ${item.model}: ${item.time}</span>`; |
|
|
}); |
|
|
if (activeCooldowns.length > 2) { |
|
|
const remaining = activeCooldowns.length - 2; |
|
|
const remainingModels = activeCooldowns.slice(2).map(i => `${i.fullModel}: ${i.time}`).join('\n'); |
|
|
statusBadges += `<span class="cooldown-badge" style="background-color: #17a2b8;" title="其他模型:\n${remainingModels}">+${remaining}</span>`; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const pathId = (managerType === 'antigravity' ? 'ag_' : '') + btoa(encodeURIComponent(filename)).replace(/[+/=]/g, '_'); |
|
|
|
|
|
|
|
|
const actionButtons = ` |
|
|
${status.disabled |
|
|
? `<button class="cred-btn enable" data-filename="${filename}" data-action="enable">启用</button>` |
|
|
: `<button class="cred-btn disable" data-filename="${filename}" data-action="disable">禁用</button>` |
|
|
} |
|
|
<button class="cred-btn view" onclick="toggle${managerType === 'antigravity' ? 'Antigravity' : ''}CredDetails('${pathId}')">查看内容</button> |
|
|
<button class="cred-btn download" onclick="download${managerType === 'antigravity' ? 'Antigravity' : ''}Cred('${filename}')">下载</button> |
|
|
<button class="cred-btn email" onclick="fetch${managerType === 'antigravity' ? 'Antigravity' : ''}UserEmail('${filename}')">查看账号邮箱</button> |
|
|
${managerType === 'antigravity' ? `<button class="cred-btn" style="background-color: #17a2b8;" onclick="toggleAntigravityQuotaDetails('${pathId}')" title="查看该凭证的额度信息">查看额度</button>` : ''} |
|
|
<button class="cred-btn" style="background-color: #ff9800;" onclick="verify${managerType === 'antigravity' ? 'Antigravity' : ''}ProjectId('${filename}')" title="重新获取Project ID,可恢复403错误">检验</button> |
|
|
<button class="cred-btn delete" data-filename="${filename}" data-action="delete">删除</button> |
|
|
`; |
|
|
|
|
|
|
|
|
const emailInfo = credInfo.user_email |
|
|
? `<div class="cred-email" style="font-size: 12px; color: #666; margin-top: 2px;">${credInfo.user_email}</div>` |
|
|
: '<div class="cred-email" style="font-size: 12px; color: #999; margin-top: 2px; font-style: italic;">未获取邮箱</div>'; |
|
|
|
|
|
const checkboxClass = manager.getElementId('file-checkbox'); |
|
|
|
|
|
div.innerHTML = ` |
|
|
<div class="cred-header"> |
|
|
<div style="display: flex; align-items: center; gap: 10px;"> |
|
|
<input type="checkbox" class="${checkboxClass}" data-filename="${filename}" onchange="toggle${managerType === 'antigravity' ? 'Antigravity' : ''}FileSelection('${filename}')"> |
|
|
<div> |
|
|
<div class="cred-filename">${filename}</div> |
|
|
${emailInfo} |
|
|
</div> |
|
|
</div> |
|
|
<div class="cred-status">${statusBadges}</div> |
|
|
</div> |
|
|
<div class="cred-actions">${actionButtons}</div> |
|
|
<div class="cred-details" id="details-${pathId}"> |
|
|
<div class="cred-content" data-filename="${filename}" data-loaded="false">点击"查看内容"按钮加载文件详情...</div> |
|
|
</div> |
|
|
${managerType === 'antigravity' ? ` |
|
|
<div class="cred-quota-details" id="quota-${pathId}" style="display: none;"> |
|
|
<div class="cred-quota-content" data-filename="${filename}" data-loaded="false"> |
|
|
点击"查看额度"按钮加载额度信息... |
|
|
</div> |
|
|
</div> |
|
|
` : ''} |
|
|
`; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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}'"]`); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('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) { |
|
|
|
|
|
targetContent.style.opacity = '0'; |
|
|
targetContent.style.transform = 'translateX(12px)'; |
|
|
targetContent.style.transition = 'none'; |
|
|
|
|
|
|
|
|
targetContent.classList.add('active'); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = '获取认证文件'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = '<pre>' + JSON.stringify(result.credentials, null, 2) + '</pre>'; |
|
|
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 = '<br><strong>可用项目:</strong><br>'; |
|
|
result.available_projects.forEach(p => { |
|
|
msg += `• ${p.name} (ID: ${p.project_id})<br>`; |
|
|
}); |
|
|
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'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const checkbox = container.querySelector(`input[data-filename="${filename}"]`); |
|
|
if (!checkbox) return false; |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
const successMsg = `✅ 检验成功!\n文件: ${filename}\nProject ID: ${data.project_id}\n\n${data.message}`; |
|
|
showStatus(successMsg.replace(/\n/g, '<br>'), '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) { |
|
|
|
|
|
const successMsg = `✅ 检验成功!\n文件: ${filename}\nProject ID: ${data.project_id}\n\n${data.message}`; |
|
|
showStatus(successMsg.replace(/\n/g, '<br>'), '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 = '<div style="text-align: center; padding: 20px; color: #666;">📊 正在加载额度信息...</div>'; |
|
|
|
|
|
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 = ` |
|
|
<div style="text-align: center; padding: 20px; color: #999;"> |
|
|
<div style="font-size: 48px; margin-bottom: 10px;">📊</div> |
|
|
<div>暂无额度信息</div> |
|
|
</div> |
|
|
`; |
|
|
} else { |
|
|
let quotaHTML = ` |
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px; border-radius: 8px 8px 0 0; margin: -10px -10px 15px -10px;"> |
|
|
<h4 style="margin: 0; font-size: 16px; display: flex; align-items: center; gap: 8px;"> |
|
|
<span style="font-size: 20px;">📊</span> |
|
|
<span>额度信息详情</span> |
|
|
</h4> |
|
|
<div style="font-size: 12px; opacity: 0.9; margin-top: 5px;">文件: ${filename}</div> |
|
|
</div> |
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px;"> |
|
|
`; |
|
|
|
|
|
for (const [modelName, quotaData] of Object.entries(models)) { |
|
|
|
|
|
const remainingFraction = quotaData.remaining || 0; |
|
|
const resetTime = quotaData.resetTime || 'N/A'; |
|
|
|
|
|
|
|
|
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 += ` |
|
|
<div style="background: white; border-left: 4px solid ${percentageColor}; border-radius: 4px; padding: 8px 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> |
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;"> |
|
|
<div style="font-weight: bold; color: #333; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; margin-right: 8px;" title="${modelName} - 剩余${remainingPercentage}% - ${resetTime}"> |
|
|
${modelName} |
|
|
</div> |
|
|
<div style="font-size: 13px; font-weight: bold; color: ${percentageColor}; white-space: nowrap;"> |
|
|
${remainingPercentage}% |
|
|
</div> |
|
|
</div> |
|
|
<div style="width: 100%; height: 8px; background-color: #e9ecef; border-radius: 4px; overflow: hidden; margin-bottom: 4px;"> |
|
|
<div style="width: ${usedPercentage}%; height: 100%; background-color: ${percentageColor}; transition: width 0.3s ease;"></div> |
|
|
</div> |
|
|
<div style="font-size: 10px; color: #666; text-align: right;"> |
|
|
${resetTime !== 'N/A' ? '🔄 ' + resetTime : ''} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
quotaHTML += '</div>'; |
|
|
contentDiv.innerHTML = quotaHTML; |
|
|
} |
|
|
|
|
|
contentDiv.setAttribute('data-loaded', 'true'); |
|
|
showStatus('✅ 成功加载额度信息', 'success'); |
|
|
} else { |
|
|
|
|
|
const errorMsg = data.error || '获取额度信息失败'; |
|
|
contentDiv.innerHTML = ` |
|
|
<div style="text-align: center; padding: 20px; color: #dc3545;"> |
|
|
<div style="font-size: 48px; margin-bottom: 10px;">❌</div> |
|
|
<div style="font-weight: bold; margin-bottom: 5px;">获取额度信息失败</div> |
|
|
<div style="font-size: 13px; color: #666;">${errorMsg}</div> |
|
|
</div> |
|
|
`; |
|
|
showStatus(`❌ ${errorMsg}`, 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
contentDiv.innerHTML = ` |
|
|
<div style="text-align: center; padding: 20px; color: #dc3545;"> |
|
|
<div style="font-size: 48px; margin-bottom: 10px;">❌</div> |
|
|
<div style="font-weight: bold; margin-bottom: 5px;">网络错误</div> |
|
|
<div style="font-size: 13px; color: #666;">${error.message}</div> |
|
|
</div> |
|
|
`; |
|
|
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'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = '<p style="text-align: center; color: #666;">暂无使用统计数据</p>'; |
|
|
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 = ` |
|
|
<div class="usage-header"> |
|
|
<div class="usage-filename">${filename}</div> |
|
|
</div> |
|
|
<div class="usage-info"> |
|
|
<div class="usage-info-item" style="grid-column: 1 / -1;"> |
|
|
<span class="usage-info-label">24小时内调用次数</span> |
|
|
<span class="usage-info-value" style="font-size: 24px; font-weight: bold; color: #007bff;">${calls24h}</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="usage-actions"> |
|
|
<button class="usage-btn reset" onclick="resetSingleUsageStats('${filename}')">重置统计</button> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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)); |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|