export const authFilesModule = { // 加载认证文件 async loadAuthFiles(keyStats = null) { try { const data = await this.makeRequest('/auth-files'); if (!keyStats) { keyStats = await this.getKeyStats(); } await this.renderAuthFiles(data.files || [], keyStats); } catch (error) { console.error('加载认证文件失败:', error); } }, // 渲染认证文件列表 async renderAuthFiles(files, keyStats = null) { const container = document.getElementById('auth-files-list'); if (!container) { return; } const allFiles = Array.isArray(files) ? files : []; const visibleFiles = allFiles.filter(file => { if (!file) return false; return this.shouldDisplayDisabledGeminiCli(file) || file.disabled !== true; }); const stats = keyStats || await this.getKeyStats(); this.cachedAuthFiles = visibleFiles.map(file => ({ ...file })); this.authFileStatsCache = stats || { bySource: {}, byAuthIndex: {} }; this.syncAuthFileControls(); if (this.cachedAuthFiles.length === 0) { container.innerHTML = `

${i18n.t('auth_files.empty_title')}

${i18n.t('auth_files.empty_desc')}

`; this.updateFilterButtons(new Set(['all'])); this.bindAuthFileFilterEvents(); this.applyAuthFileFilterState(false); this.authFilesPagination.currentPage = 1; this.authFilesPagination.totalPages = 1; this.updatePaginationControls(0); return; } const existingTypes = new Set(['all']); this.cachedAuthFiles.forEach(file => { if (file.type) { existingTypes.add(file.type); } }); this.updateFilterButtons(existingTypes); this.bindAuthFileFilterEvents(); this.applyAuthFileFilterState(false); this.renderAuthFilesPage(this.authFilesPagination.currentPage); this.bindAuthFileActionEvents(); }, isRuntimeOnlyAuthFile(file) { if (!file) return false; const runtimeValue = file.runtime_only; return runtimeValue === true || runtimeValue === 'true'; }, shouldDisplayDisabledGeminiCli(file) { if (!file) return false; const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : ''; const type = typeof file.type === 'string' ? file.type.toLowerCase() : ''; const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli'; return isGeminiCli && !this.isRuntimeOnlyAuthFile(file); }, resolveAuthFileStats(file, stats = {}) { const statsBySource = (stats && stats.bySource) || stats || {}; const statsByAuthIndex = (stats && stats.byAuthIndex) || {}; const rawFileName = typeof file?.name === 'string' ? file.name : ''; const defaultStats = { success: 0, failure: 0 }; const authIndexKey = this.normalizeAuthIndexValue(file?.auth_index); if (authIndexKey && statsByAuthIndex[authIndexKey]) { return statsByAuthIndex[authIndexKey]; } if (!rawFileName) { return defaultStats; } const fromName = statsBySource[rawFileName]; if (fromName && (fromName.success > 0 || fromName.failure > 0)) { return fromName; } let fileStats = fromName || defaultStats; if (fileStats.success === 0 && fileStats.failure === 0) { const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, ""); if (nameWithoutExt && nameWithoutExt !== rawFileName) { const candidateNames = new Set([nameWithoutExt]); const normalizedName = nameWithoutExt.toLowerCase(); const typePrefix = typeof file?.type === 'string' ? file.type.trim().toLowerCase() : ''; const providerPrefix = typeof file?.provider === 'string' ? file.provider.trim().toLowerCase() : ''; const prefixList = []; if (typePrefix) { prefixList.push(`${typePrefix}-`); } if (providerPrefix && providerPrefix !== typePrefix) { prefixList.push(`${providerPrefix}-`); } prefixList.forEach(prefix => { if (prefix && normalizedName.startsWith(prefix)) { const trimmed = nameWithoutExt.substring(prefix.length); if (trimmed) { candidateNames.add(trimmed); } } }); for (const candidate of candidateNames) { const candidateStats = statsBySource[candidate]; if (candidateStats && (candidateStats.success > 0 || candidateStats.failure > 0)) { fileStats = candidateStats; break; } } } } return fileStats || defaultStats; }, normalizeAuthIndexValue(value) { if (typeof value === 'number' && Number.isFinite(value)) { return value.toString(); } if (typeof value === 'string') { const trimmed = value.trim(); return trimmed ? trimmed : null; } return null; }, buildAuthFileItemHtml(file) { const rawFileName = typeof file?.name === 'string' ? file.name : ''; const safeFileName = this.escapeHtml(rawFileName); const stats = this.authFileStatsCache || {}; const fileStats = this.resolveAuthFileStats(file, stats); const fileType = file.type || 'unknown'; let typeDisplayKey; switch (fileType) { case 'qwen': typeDisplayKey = 'auth_files.type_qwen'; break; case 'gemini': typeDisplayKey = 'auth_files.type_gemini'; break; case 'gemini-cli': typeDisplayKey = 'auth_files.type_gemini-cli'; break; case 'aistudio': typeDisplayKey = 'auth_files.type_aistudio'; break; case 'claude': typeDisplayKey = 'auth_files.type_claude'; break; case 'codex': typeDisplayKey = 'auth_files.type_codex'; break; case 'antigravity': typeDisplayKey = 'auth_files.type_antigravity'; break; case 'iflow': typeDisplayKey = 'auth_files.type_iflow'; break; case 'vertex': typeDisplayKey = 'auth_files.type_vertex'; break; case 'empty': typeDisplayKey = 'auth_files.type_empty'; break; default: typeDisplayKey = 'auth_files.type_unknown'; break; } const typeBadge = `${i18n.t(typeDisplayKey)}`; const isRuntimeOnly = this.isRuntimeOnlyAuthFile(file); const shouldShowMainFlag = this.shouldDisplayDisabledGeminiCli(file); const mainFlagButton = shouldShowMainFlag ? ` ` : ''; const actionsHtml = isRuntimeOnly ? `
虚拟认证文件
` : `
${mainFlagButton}
`; return `
${typeBadge}${safeFileName}
${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')} ${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}
`; }, getFilteredAuthFiles(filterType = this.currentAuthFileFilter) { const files = Array.isArray(this.cachedAuthFiles) ? this.cachedAuthFiles : []; if (!files.length) { return []; } const typeFilter = (filterType || 'all').toLowerCase(); const keyword = (this.authFileSearchQuery || '').trim().toLowerCase(); return files.filter(file => { const name = String(file?.name || '').toLowerCase(); const type = String(file?.type || '').toLowerCase(); const provider = String(file?.provider || '').toLowerCase(); if (typeFilter !== 'all') { if (!type) return false; if (type !== typeFilter) { if (!provider || provider !== typeFilter) { return false; } } } if (!keyword) return true; return name.includes(keyword) || type.includes(keyword) || provider.includes(keyword); }); }, updateAuthFileSearchQuery(value = '') { const normalized = (value || '').trim(); if (this.authFileSearchQuery === normalized) { return; } this.authFileSearchQuery = normalized; this.authFilesPagination.currentPage = 1; this.renderAuthFilesPage(1); }, updateAuthFilesPageSize(value) { const normalized = this.normalizeAuthFilesPageSize(value); if (this.authFilesPagination?.pageSize === normalized) { this.syncAuthFileControls(); return; } this.authFilesPagination.pageSize = normalized; this.authFilesPagination.currentPage = 1; try { localStorage.setItem(this.authFilesPageSizeKey, `${normalized}`); } catch (error) { console.warn('Failed to persist auth files page size:', error); } this.syncAuthFileControls(); this.renderAuthFilesPage(1); }, syncAuthFileControls() { const searchInput = document.getElementById('auth-files-search-input'); if (searchInput && searchInput.value !== this.authFileSearchQuery) { searchInput.value = this.authFileSearchQuery; } const pageSizeInput = document.getElementById('auth-files-page-size-input'); const targetSize = this.authFilesPagination?.pageSize || 9; if (pageSizeInput && parseInt(pageSizeInput.value, 10) !== targetSize) { pageSizeInput.value = targetSize; } }, bindAuthFilesSearchControl() { const searchInput = document.getElementById('auth-files-search-input'); if (!searchInput) return; if (searchInput._authFileSearchListener) { searchInput.removeEventListener('input', searchInput._authFileSearchListener); } const debounced = this.debounce((value) => { this.updateAuthFileSearchQuery(value); }, 250); const listener = (event) => { const value = event?.target?.value ?? ''; debounced(value); }; searchInput._authFileSearchListener = listener; searchInput.addEventListener('input', listener); }, bindAuthFilesPageSizeControl() { const pageSizeInput = document.getElementById('auth-files-page-size-input'); if (!pageSizeInput) return; if (pageSizeInput._authFilePageSizeListener) { pageSizeInput.removeEventListener('change', pageSizeInput._authFilePageSizeListener); } const listener = (event) => { const value = parseInt(event?.target?.value, 10); if (!Number.isFinite(value)) { return; } this.updateAuthFilesPageSize(value); }; pageSizeInput._authFilePageSizeListener = listener; pageSizeInput.addEventListener('change', listener); if (pageSizeInput._authFilePageSizeBlur) { pageSizeInput.removeEventListener('blur', pageSizeInput._authFilePageSizeBlur); } const blurListener = () => { if (!pageSizeInput.value) { this.syncAuthFileControls(); } }; pageSizeInput._authFilePageSizeBlur = blurListener; pageSizeInput.addEventListener('blur', blurListener); }, renderAuthFilesPage(page = null) { const container = document.getElementById('auth-files-list'); if (!container) return; const pageSize = this.authFilesPagination?.pageSize || 9; const filteredFiles = this.getFilteredAuthFiles(); const totalItems = filteredFiles.length; const hasCachedFiles = Array.isArray(this.cachedAuthFiles) && this.cachedAuthFiles.length > 0; const filterApplied = (this.currentAuthFileFilter && this.currentAuthFileFilter !== 'all'); const searchApplied = Boolean((this.authFileSearchQuery || '').trim()); if (totalItems === 0) { const titleKey = hasCachedFiles && (filterApplied || searchApplied) ? 'auth_files.search_empty_title' : 'auth_files.empty_title'; const descKey = hasCachedFiles && (filterApplied || searchApplied) ? 'auth_files.search_empty_desc' : 'auth_files.empty_desc'; container.innerHTML = `

${i18n.t(titleKey)}

${i18n.t(descKey)}

`; this.authFilesPagination.currentPage = 1; this.updatePaginationControls(0); return; } const maxPages = Math.max(1, Math.ceil(totalItems / pageSize)); let currentPage = page == null ? (this.authFilesPagination.currentPage || 1) : page; if (currentPage > maxPages) { currentPage = maxPages; } if (currentPage < 1) { currentPage = 1; } this.authFilesPagination.currentPage = currentPage; this.authFilesPagination.totalPages = maxPages; const startIndex = (currentPage - 1) * pageSize; const pageFiles = filteredFiles.slice(startIndex, startIndex + pageSize); container.innerHTML = pageFiles.map(file => this.buildAuthFileItemHtml(file)).join(''); this.updatePaginationControls(totalItems); this.bindAuthFileActionEvents(); }, bindAuthFilesPaginationEvents() { const container = document.getElementById('auth-files-pagination'); if (!container) return; const oldListener = container._paginationListener; if (oldListener) { container.removeEventListener('click', oldListener); } const listener = (event) => { const button = event.target.closest('button[data-action]'); if (!button || !container.contains(button)) return; event.preventDefault(); const action = button.dataset.action; const currentPage = this.authFilesPagination?.currentPage || 1; if (action === 'prev') { this.renderAuthFilesPage(currentPage - 1); } else if (action === 'next') { this.renderAuthFilesPage(currentPage + 1); } }; container._paginationListener = listener; container.addEventListener('click', listener); }, updatePaginationControls(totalItems = 0) { const paginationContainer = document.getElementById('auth-files-pagination'); const infoEl = document.getElementById('auth-files-pagination-info'); if (!paginationContainer || !infoEl) return; const prevBtn = paginationContainer.querySelector('button[data-action=\"prev\"]'); const nextBtn = paginationContainer.querySelector('button[data-action=\"next\"]'); const pageSize = this.authFilesPagination?.pageSize || 9; const totalPages = this.authFilesPagination?.totalPages || 1; const currentPage = Math.min(this.authFilesPagination?.currentPage || 1, totalPages); const shouldShow = totalItems > pageSize; paginationContainer.style.display = shouldShow ? 'flex' : 'none'; const infoParams = totalItems === 0 ? { current: 0, total: 0, count: 0 } : { current: currentPage, total: totalPages, count: totalItems }; infoEl.textContent = i18n.t('auth_files.pagination_info', infoParams); if (prevBtn) { prevBtn.disabled = currentPage <= 1; } if (nextBtn) { nextBtn.disabled = currentPage >= totalPages; } }, updateFilterButtons(existingTypes) { const filterContainer = document.querySelector('.auth-file-filter'); if (!filterContainer) return; const predefinedTypes = [ { type: 'all', labelKey: 'auth_files.filter_all' }, { type: 'qwen', labelKey: 'auth_files.filter_qwen' }, { type: 'gemini', labelKey: 'auth_files.filter_gemini' }, { type: 'gemini-cli', labelKey: 'auth_files.filter_gemini-cli' }, { type: 'aistudio', labelKey: 'auth_files.filter_aistudio' }, { type: 'claude', labelKey: 'auth_files.filter_claude' }, { type: 'codex', labelKey: 'auth_files.filter_codex' }, { type: 'antigravity', labelKey: 'auth_files.filter_antigravity' }, { type: 'iflow', labelKey: 'auth_files.filter_iflow' }, { type: 'vertex', labelKey: 'auth_files.filter_vertex' }, { type: 'empty', labelKey: 'auth_files.filter_empty' } ]; const existingButtons = filterContainer.querySelectorAll('.filter-btn'); const existingButtonTypes = new Set(); existingButtons.forEach(btn => { existingButtonTypes.add(btn.dataset.type); }); existingButtons.forEach(btn => { const btnType = btn.dataset.type; if (existingTypes.has(btnType)) { btn.style.display = 'inline-block'; const match = predefinedTypes.find(item => item.type === btnType); if (match) { btn.textContent = i18n.t(match.labelKey); btn.setAttribute('data-i18n-text', match.labelKey); } } else { btn.style.display = 'none'; } }); const predefinedTypeSet = new Set(predefinedTypes.map(t => t.type)); existingTypes.forEach(type => { if (type !== 'all' && !predefinedTypeSet.has(type) && !existingButtonTypes.has(type)) { const btn = document.createElement('button'); btn.className = 'filter-btn'; btn.dataset.type = type; const match = predefinedTypes.find(item => item.type === type); if (match) { btn.setAttribute('data-i18n-text', match.labelKey); btn.textContent = i18n.t(match.labelKey); } else { const dynamicKey = `auth_files.filter_${type}`; btn.setAttribute('data-i18n-text', dynamicKey); btn.textContent = this.generateDynamicTypeLabel(type); } const emptyBtn = filterContainer.querySelector('[data-type=\"empty\"]'); if (emptyBtn) { filterContainer.insertBefore(btn, emptyBtn); } else { filterContainer.appendChild(btn); } } }); }, handleFilterClick(clickedBtn, options = {}) { if (!clickedBtn) return; const { skipRender = false } = options; const filterBtns = document.querySelectorAll('.auth-file-filter .filter-btn'); filterBtns.forEach(b => b.classList.remove('active')); clickedBtn.classList.add('active'); const filterType = clickedBtn.dataset.type; this.currentAuthFileFilter = filterType || 'all'; if (!skipRender) { this.authFilesPagination.currentPage = 1; this.renderAuthFilesPage(1); } this.refreshFilterButtonTexts(); this.renderOauthExcludedModels(); }, generateDynamicTypeLabel(type) { if (!type) return ''; const key = `auth_files.type_${type}`; const translated = i18n.t(key); if (translated && translated !== key) { return translated; } if (type.toLowerCase() === 'iflow') return 'iFlow'; return type.charAt(0).toUpperCase() + type.slice(1); }, bindAuthFileFilterEvents() { const filterContainer = document.querySelector('.auth-file-filter'); if (!filterContainer) return; if (filterContainer._filterListener) { filterContainer.removeEventListener('click', filterContainer._filterListener); } const listener = (event) => { const button = event.target.closest('.filter-btn'); if (!button || !filterContainer.contains(button)) return; event.preventDefault(); this.handleFilterClick(button); }; filterContainer._filterListener = listener; filterContainer.addEventListener('click', listener); this.refreshFilterButtonTexts(); }, applyAuthFileFilterState(shouldRender = false) { const filterContainer = document.querySelector('.auth-file-filter'); if (!filterContainer) return; const currentType = this.currentAuthFileFilter || 'all'; const buttons = filterContainer.querySelectorAll('.filter-btn'); if (buttons.length === 0) return; let targetButton = null; buttons.forEach(btn => { if (btn.dataset.type === currentType) { targetButton = btn; } }); if (!targetButton) { targetButton = filterContainer.querySelector('.filter-btn[data-type=\"all\"]') || buttons[0]; if (targetButton) { this.currentAuthFileFilter = targetButton.dataset.type || 'all'; } } if (targetButton) { this.handleFilterClick(targetButton, { skipRender: !shouldRender }); } }, removeAuthFileElements(filenames = []) { if (!Array.isArray(filenames) || filenames.length === 0) { return; } const removalSet = new Set(filenames); this.cachedAuthFiles = (this.cachedAuthFiles || []).filter(file => file && !removalSet.has(file.name)); if (!this.cachedAuthFiles.length) { this.authFilesPagination.currentPage = 1; } this.renderAuthFilesPage(this.authFilesPagination.currentPage); }, refreshFilterButtonTexts() { document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => { const key = btn.getAttribute('data-i18n-text'); if (key) { btn.textContent = i18n.t(key); } }); }, bindAuthFileActionEvents() { const container = document.getElementById('auth-files-list'); if (!container) return; const oldListener = container._authFileActionListener; if (oldListener) { container.removeEventListener('click', oldListener); } const listener = (e) => { const button = e.target.closest('button[data-action]'); if (!button) return; const action = button.dataset.action; const actionsContainer = button.closest('.item-actions'); if (!actionsContainer) return; const filename = actionsContainer.dataset.filename; if (!filename) return; switch (action) { case 'showDetails': this.showAuthFileDetails(filename); break; case 'download': this.downloadAuthFile(filename); break; case 'delete': this.deleteAuthFile(filename); break; } }; container._authFileActionListener = listener; container.addEventListener('click', listener); }, formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, openVertexFilePicker() { const fileInput = document.getElementById('vertex-file-input'); if (fileInput) { fileInput.click(); } }, handleVertexFileSelection(event) { const fileInput = event?.target; const file = fileInput?.files?.[0] || null; if (fileInput) { fileInput.value = ''; } if (file && !file.name.toLowerCase().endsWith('.json')) { this.showNotification(i18n.t('vertex_import.file_required'), 'error'); this.vertexImportState.file = null; this.updateVertexFileDisplay(); this.updateVertexImportButtonState(); return; } this.vertexImportState.file = file; this.updateVertexFileDisplay(file ? file.name : ''); this.updateVertexImportButtonState(); }, updateVertexFileDisplay(filename = '') { const displayInput = document.getElementById('vertex-file-display'); if (!displayInput) return; displayInput.value = filename || ''; }, updateVertexImportButtonState() { const importBtn = document.getElementById('vertex-import-btn'); if (!importBtn) return; const disabled = !this.vertexImportState.file || this.vertexImportState.loading; importBtn.disabled = disabled; }, async importVertexCredential() { if (!this.vertexImportState.file) { this.showNotification(i18n.t('vertex_import.file_required'), 'error'); return; } const locationInput = document.getElementById('vertex-location'); const location = locationInput ? locationInput.value.trim() : ''; const formData = new FormData(); formData.append('file', this.vertexImportState.file, this.vertexImportState.file.name); if (location) { formData.append('location', location); } try { this.vertexImportState.loading = true; this.updateVertexImportButtonState(); const response = await this.apiClient.requestRaw('/vertex/import', { method: 'POST', body: formData }); if (!response.ok) { let errorMessage = `HTTP ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData?.message || errorData?.error || errorMessage; } catch (parseError) { const text = await response.text(); if (text) { errorMessage = text; } } throw new Error(errorMessage); } const result = await response.json(); this.vertexImportState.result = result; this.renderVertexImportResult(result); this.showNotification(i18n.t('vertex_import.success'), 'success'); this.vertexImportState.file = null; this.updateVertexFileDisplay(''); } catch (error) { console.error('Vertex credential import failed:', error); this.showNotification(`${i18n.t('notification.import_failed')}: ${error.message}`, 'error'); } finally { this.vertexImportState.loading = false; this.updateVertexImportButtonState(); const fileInput = document.getElementById('vertex-file-input'); if (fileInput) { fileInput.value = ''; } } }, renderVertexImportResult(result = null) { const container = document.getElementById('vertex-import-result'); const projectEl = document.getElementById('vertex-result-project'); const emailEl = document.getElementById('vertex-result-email'); const locationEl = document.getElementById('vertex-result-location'); const fileEl = document.getElementById('vertex-result-file'); if (!container || !projectEl || !emailEl || !locationEl || !fileEl) return; if (!result) { container.style.display = 'none'; this.vertexImportState.result = null; return; } this.vertexImportState.result = result; projectEl.textContent = result.project || '-'; emailEl.textContent = result.email || '-'; locationEl.textContent = result.location || '-'; fileEl.textContent = result.file || '-'; container.style.display = 'block'; }, showAuthFileDetails(filename, content) { const file = (this.cachedAuthFiles || []).find(f => f && f.name === filename); if (!file) return; const stats = this.resolveAuthFileStats(file, this.authFileStatsCache || {}); const size = typeof file.size === 'number' ? this.formatFileSize(file.size) : '-'; const provider = file.provider || '-'; const type = file.type || '-'; const createdAt = file.created_at ? new Date(file.created_at).toLocaleString('zh-CN') : '-'; const jsonContent = content || JSON.stringify(file, null, 2); // 使用独立的 JSON 弹窗样式,避免被通用 .modal 的 display:none 覆盖 const modalHtml = `

${this.escapeHtml(filename)}

${this.escapeHtml(jsonContent)}
`; const oldModal = document.getElementById('json-modal'); if (oldModal) { oldModal.remove(); } document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = document.getElementById('json-modal'); modal.addEventListener('click', (e) => { if (e.target === modal) { this.closeJsonModal(); } }); this.bindJsonModalEvents(modal); }, bindJsonModalEvents(modal) { modal.addEventListener('click', (e) => { const button = e.target.closest('button[data-action]'); if (!button) return; const action = button.dataset.action; switch (action) { case 'copy': this.copyJsonContent(); break; case 'close': this.closeJsonModal(); break; } }); }, closeJsonModal() { const modal = document.getElementById('json-modal'); if (modal) { modal.remove(); } }, copyJsonContent() { const jsonContent = document.querySelector('.json-content'); if (jsonContent) { const text = jsonContent.textContent; navigator.clipboard.writeText(text).then(() => { this.showNotification('内容已复制到剪贴板', 'success'); }).catch(() => { this.showNotification('复制失败', 'error'); }); } }, async downloadAuthFile(filename) { try { const response = await this.apiClient.requestRaw(`/auth-files/download?name=${encodeURIComponent(filename)}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } 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); this.showNotification(i18n.t('auth_files.download_success'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error'); } }, async deleteAuthFile(filename) { if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return; try { await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' }); this.removeAuthFileElements([filename]); this.clearCache(); await this.loadAuthFiles(); this.showNotification(i18n.t('auth_files.delete_success'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } }, async deleteAllAuthFiles() { const filterType = (this.currentAuthFileFilter || 'all').toLowerCase(); const isFiltered = filterType !== 'all'; const typeLabel = this.generateDynamicTypeLabel(filterType); const confirmMessage = isFiltered ? i18n.t('auth_files.delete_filtered_confirm').replace('{type}', typeLabel) : i18n.t('auth_files.delete_all_confirm'); if (!confirm(confirmMessage)) return; try { if (!isFiltered) { const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' }); const currentNames = (this.cachedAuthFiles || []).map(file => file.name).filter(Boolean); if (currentNames.length > 0) { this.removeAuthFileElements(currentNames); } this.clearCache(); this.currentAuthFileFilter = 'all'; await this.loadAuthFiles(); this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success'); return; } const deletableFiles = (this.cachedAuthFiles || []).filter(file => { if (!file || file.runtime_only) return false; const fileType = (file.type || 'unknown').toLowerCase(); return fileType === filterType; }); if (deletableFiles.length === 0) { this.showNotification(i18n.t('auth_files.delete_filtered_none').replace('{type}', typeLabel), 'info'); return; } let success = 0; let failed = 0; const deletedNames = []; for (const file of deletableFiles) { try { await this.makeRequest(`/auth-files?name=${encodeURIComponent(file.name)}`, { method: 'DELETE' }); success++; deletedNames.push(file.name); } catch (error) { console.error('删除认证文件失败:', file?.name, error); failed++; } } if (deletedNames.length > 0) { this.removeAuthFileElements(deletedNames); } this.clearCache(); this.currentAuthFileFilter = 'all'; await this.loadAuthFiles(); if (failed === 0) { const successMsg = i18n.t('auth_files.delete_filtered_success') .replace('{count}', success) .replace('{type}', typeLabel); this.showNotification(successMsg, 'success'); } else { const warningMsg = i18n.t('auth_files.delete_filtered_partial') .replace('{success}', success) .replace('{failed}', failed) .replace('{type}', typeLabel); this.showNotification(warningMsg, 'warning'); } } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } }, // 触发文件上传选择器 uploadAuthFile() { const authFileInput = document.getElementById('auth-file-input'); if (authFileInput) { authFileInput.click(); } }, // 处理文件上传 async handleFileUpload(event) { const file = event.target.files[0]; if (!file) return; if (!file.name.endsWith('.json')) { this.showNotification(i18n.t('auth_files.upload_error_json'), 'error'); event.target.value = ''; return; } try { const formData = new FormData(); formData.append('file', file, file.name); const response = await this.apiClient.requestRaw('/auth-files', { method: 'POST', body: formData }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `HTTP ${response.status}`); } this.clearCache(); // 清除缓存 await this.loadAuthFiles(); this.showNotification(i18n.t('auth_files.upload_success'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.upload_failed')}: ${error.message}`, 'error'); } finally { // 清空文件输入框,允许重复上传同一文件 event.target.value = ''; } }, normalizeOauthExcludedMap(payload = {}) { const raw = (payload && (payload['oauth-excluded-models'] || payload.items)) || payload || {}; if (!raw || typeof raw !== 'object') { return {}; } const normalized = {}; Object.entries(raw).forEach(([provider, models]) => { const key = typeof provider === 'string' ? provider.trim() : ''; if (!key) return; const list = Array.isArray(models) ? models.map(item => String(item || '').trim()).filter(Boolean) : []; normalized[key.toLowerCase()] = list; }); return normalized; }, resolveOauthExcludedFromConfig(config = null) { const sources = []; if (config && typeof config === 'object') { sources.push(config); } if (this.configCache && typeof this.configCache === 'object') { if (this.configCache['oauth-excluded-models'] !== undefined) { sources.push({ 'oauth-excluded-models': this.configCache['oauth-excluded-models'] }); } if (this.configCache['__full__']) { sources.push(this.configCache['__full__']); } } for (const source of sources) { if (!source || typeof source !== 'object') continue; if (Object.prototype.hasOwnProperty.call(source, 'oauth-excluded-models')) { return { map: this.normalizeOauthExcludedMap(source), found: true }; } } return { map: {}, found: false }; }, applyOauthExcludedFromConfig(config = null, options = {}) { const { render = true } = options || {}; const { map, found } = this.resolveOauthExcludedFromConfig(config); if (!found) { return false; } this.oauthExcludedModels = map; this._oauthExcludedLoading = false; this.setOauthExcludedStatus(''); this.updateOauthExcludedButtonsState(false); if (render) { this.renderOauthExcludedModels(); } return true; }, getFilteredOauthExcludedMap(filterType = this.currentAuthFileFilter) { const map = this.oauthExcludedModels || {}; if (!map || typeof map !== 'object') { return {}; } const type = (filterType || 'all').toLowerCase(); if (type === 'all') { return map; } const result = {}; Object.entries(map).forEach(([provider, models]) => { if ((provider || '').toLowerCase() === type) { result[provider] = models; } }); return result; }, findOauthExcludedEntry(provider) { if (!provider || provider === 'all') { return null; } const normalized = provider.toLowerCase(); const map = this.oauthExcludedModels || {}; for (const [key, models] of Object.entries(map)) { if ((key || '').toLowerCase() === normalized) { return { provider: key, models: Array.isArray(models) ? models : [] }; } } return null; }, setOauthExcludedForm(provider = '', models = null) { const providerSelect = document.getElementById('oauth-excluded-provider-select'); const modelsInput = document.getElementById('oauth-excluded-models'); const normalizedProvider = (provider || '').trim(); if (providerSelect) { const options = Array.from(providerSelect.options || []); let match = options.find(opt => (opt.value || '').toLowerCase() === normalizedProvider.toLowerCase()); if (!match && normalizedProvider) { match = new Option(this.generateDynamicTypeLabel(normalizedProvider) || normalizedProvider, normalizedProvider); providerSelect.appendChild(match); } if (normalizedProvider && match) { providerSelect.value = match.value; } else { providerSelect.value = 'auto'; } } if (modelsInput && models !== null && models !== undefined) { const list = Array.isArray(models) ? models : []; modelsInput.value = list.map(item => item || '').join('\n'); } }, syncOauthExcludedFormWithFilter(overrideModels = false) { const filterType = (this.currentAuthFileFilter || 'all').toLowerCase(); const entry = this.findOauthExcludedEntry(filterType); if (filterType === 'all') { if (overrideModels) { if (entry) { this.setOauthExcludedForm(entry.provider, entry.models); } else { this.setOauthExcludedForm('', ''); } } return; } if (overrideModels) { this.setOauthExcludedForm(filterType, entry ? entry.models : []); } else { this.setOauthExcludedForm(filterType); } }, getOauthExcludedProviderValue() { const providerSelect = document.getElementById('oauth-excluded-provider-select'); const filterFallback = (this.currentAuthFileFilter && this.currentAuthFileFilter !== 'all') ? this.currentAuthFileFilter : ''; let selected = (providerSelect && providerSelect.value) ? providerSelect.value.trim() : ''; if (!selected || selected === 'auto') { return filterFallback; } return selected; }, refreshOauthProviderOptions() { const providerSelect = document.getElementById('oauth-excluded-provider-select'); if (!providerSelect) return; const allowedProviders = ['gemini-cli', 'vertex', 'aistudio', 'antigravity', 'claude', 'codex', 'qwen', 'iflow']; const mapProviders = Object.keys(this.oauthExcludedModels || {}); const filterType = (this.currentAuthFileFilter || '').toLowerCase(); const providers = Array.from(new Set([...allowedProviders, ...mapProviders].filter(Boolean))); const prevValue = providerSelect.value || 'auto'; providerSelect.innerHTML = ''; const addOption = (value, textKey, fallbackText = null) => { const opt = document.createElement('option'); opt.value = value; if (textKey) { opt.setAttribute('data-i18n-text', textKey); opt.textContent = i18n.t(textKey); } else { opt.textContent = fallbackText || value; } providerSelect.appendChild(opt); }; addOption('auto', 'oauth_excluded.provider_auto'); providers.sort((a, b) => a.localeCompare(b)).forEach(item => addOption(item, null, this.generateDynamicTypeLabel(item) || item)); const restoreValue = (() => { if (prevValue && Array.from(providerSelect.options).some(opt => opt.value === prevValue)) { return prevValue; } if (filterType && Array.from(providerSelect.options).some(opt => opt.value === filterType)) { return filterType; } return 'auto'; })(); providerSelect.value = restoreValue; }, parseOauthExcludedModelsInput(input = '') { const tokens = (input || '').split(/[\n,]/).map(token => token.trim()).filter(Boolean); const unique = []; tokens.forEach(token => { if (!unique.includes(token)) { unique.push(token); } }); return unique; }, openOauthExcludedEditor(provider = '', models = null) { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); if (!modal || !modalBody) return; const normalizedProvider = (provider || '').trim(); const fallbackProvider = normalizedProvider || ((this.currentAuthFileFilter && this.currentAuthFileFilter !== 'all') ? this.currentAuthFileFilter : ''); let targetModels = models; if ((targetModels === null || targetModels === undefined) && fallbackProvider) { const existing = this.findOauthExcludedEntry(fallbackProvider); if (existing) { targetModels = existing.models; } } modalBody.innerHTML = `

${fallbackProvider ? i18n.t('oauth_excluded.edit_title', { provider: this.generateDynamicTypeLabel(fallbackProvider) }) : i18n.t('oauth_excluded.add_title') }

${i18n.t('oauth_excluded.provider_hint')}

${i18n.t('oauth_excluded.models_hint')}

`; this.refreshOauthProviderOptions(); this.setOauthExcludedForm(fallbackProvider, targetModels != null ? targetModels : []); this.showModal(); const saveBtn = document.getElementById('oauth-excluded-save'); if (saveBtn) { saveBtn.onclick = () => this.saveOauthExcludedEntry(); } const deleteBtn = document.getElementById('oauth-excluded-delete'); if (deleteBtn) { deleteBtn.onclick = () => this.deleteOauthExcludedEntry(); } const providerSelect = document.getElementById('oauth-excluded-provider-select'); const syncDeleteState = () => { if (deleteBtn) { deleteBtn.disabled = !this.getOauthExcludedProviderValue(); } }; if (providerSelect) { providerSelect.addEventListener('change', syncDeleteState); } syncDeleteState(); this.updateOauthExcludedButtonsState(false); }, buildOauthExcludedItem(provider, models = []) { const providerLabel = this.generateDynamicTypeLabel(provider) || provider; const normalizedModels = Array.isArray(models) ? models.filter(Boolean) : []; const tags = normalizedModels.length ? normalizedModels.map(model => `${this.escapeHtml(String(model))}`).join('') : `${i18n.t('oauth_excluded.no_models')}`; const modelCount = normalizedModels.length; return `
${this.escapeHtml(providerLabel)}
${modelCount > 0 ? i18n.t('oauth_excluded.model_count', { count: modelCount }) : i18n.t('oauth_excluded.no_models')}
${tags}
`; }, renderOauthExcludedModels(filterType = this.currentAuthFileFilter) { const container = document.getElementById('oauth-excluded-list'); const scopeEl = document.getElementById('oauth-excluded-scope'); if (!container) return; const currentType = (filterType || 'all').toLowerCase(); const map = this.getFilteredOauthExcludedMap(currentType); const providers = Object.keys(map || {}); if (scopeEl) { const label = currentType === 'all' ? i18n.t('oauth_excluded.scope_all') : i18n.t('oauth_excluded.scope_provider', { provider: this.generateDynamicTypeLabel(currentType) }); scopeEl.textContent = label; } if (!this.isConnected) { container.innerHTML = `
${i18n.t('oauth_excluded.disconnected')}
`; return; } if (this._oauthExcludedLoading) { container.innerHTML = `
${i18n.t('common.loading')}
`; return; } if (!providers.length) { const emptyKey = currentType === 'all' ? 'oauth_excluded.list_empty_all' : 'oauth_excluded.list_empty_filtered'; container.innerHTML = `
${i18n.t(emptyKey)}
`; return; } const itemsHtml = providers .sort((a, b) => a.localeCompare(b)) .map(provider => this.buildOauthExcludedItem(provider, map[provider])) .join(''); container.innerHTML = itemsHtml; this.refreshOauthProviderOptions(); this.bindOauthExcludedActionEvents(); }, bindOauthExcludedActionEvents() { const container = document.getElementById('oauth-excluded-list'); if (!container) return; if (container._oauthExcludedListener) { container.removeEventListener('click', container._oauthExcludedListener); } const listener = (event) => { const button = event.target.closest('button[data-action]'); if (!button || !container.contains(button)) return; const provider = button.dataset.provider; if (!provider) return; const entry = this.findOauthExcludedEntry(provider); if (button.dataset.action === 'edit') { this.openOauthExcludedEditor(provider, entry ? entry.models : []); } else if (button.dataset.action === 'delete') { this.deleteOauthExcludedEntry(provider); } }; container._oauthExcludedListener = listener; container.addEventListener('click', listener); }, updateOauthExcludedButtonsState(isLoading = false) { const refreshBtn = document.getElementById('oauth-excluded-refresh'); const saveBtn = document.getElementById('oauth-excluded-save'); const deleteBtn = document.getElementById('oauth-excluded-delete'); const addBtn = document.getElementById('oauth-excluded-add'); const disabled = isLoading || !this.isConnected; [refreshBtn, saveBtn, deleteBtn, addBtn].forEach(btn => { if (btn) { btn.disabled = disabled; } }); }, setOauthExcludedStatus(message = '') { const statusEl = document.getElementById('oauth-excluded-status'); if (statusEl) { statusEl.textContent = message || ''; } }, async loadOauthExcludedModels(forceRefresh = false) { if (!this.isConnected) { this.renderOauthExcludedModels(); this.updateOauthExcludedButtonsState(); return; } if (this._oauthExcludedLoading) { return; } this._oauthExcludedLoading = true; this.updateOauthExcludedButtonsState(true); this.setOauthExcludedStatus(i18n.t('oauth_excluded.refreshing')); this.renderOauthExcludedModels(); try { let targetMap = {}; let hasData = false; if (!forceRefresh) { const { map, found } = this.resolveOauthExcludedFromConfig(); if (found) { targetMap = map; hasData = true; } } if (!hasData) { try { const configSection = await this.getConfig('oauth-excluded-models', forceRefresh); if (configSection !== undefined) { targetMap = this.normalizeOauthExcludedMap(configSection); hasData = true; } } catch (configError) { console.warn('从配置获取 OAuth 排除列表失败,尝试回退接口:', configError); } } if (!hasData) { const data = await this.makeRequest('/oauth-excluded-models'); targetMap = this.normalizeOauthExcludedMap(data); hasData = true; } this.oauthExcludedModels = targetMap; this.setOauthExcludedStatus(''); } catch (error) { console.error('加载 OAuth 排除列表失败:', error); const message = `${i18n.t('oauth_excluded.load_failed')}: ${error.message}`; this.setOauthExcludedStatus(message); this.showNotification(message, 'error'); } finally { this._oauthExcludedLoading = false; this.updateOauthExcludedButtonsState(false); this.renderOauthExcludedModels(); } }, async saveOauthExcludedEntry() { if (!this.isConnected) { this.showNotification(i18n.t('notification.connection_required'), 'error'); return; } const modelsInput = document.getElementById('oauth-excluded-models'); if (!modelsInput) return; const providerValue = this.getOauthExcludedProviderValue(); if (!providerValue) { this.showNotification(i18n.t('oauth_excluded.provider_required'), 'error'); return; } const models = this.parseOauthExcludedModelsInput(modelsInput.value); this.updateOauthExcludedButtonsState(true); this.setOauthExcludedStatus(i18n.t('oauth_excluded.saving')); try { await this.makeRequest('/oauth-excluded-models', { method: 'PATCH', body: JSON.stringify({ provider: providerValue, models }) }); const successKey = models.length === 0 ? 'oauth_excluded.delete_success' : 'oauth_excluded.save_success'; this.showNotification(i18n.t(successKey), 'success'); this.clearCache('oauth-excluded-models'); await this.loadOauthExcludedModels(true); this.closeModal(); } catch (error) { this.showNotification(`${i18n.t('oauth_excluded.save_failed')}: ${error.message}`, 'error'); } finally { this.setOauthExcludedStatus(''); this.updateOauthExcludedButtonsState(false); } }, async deleteOauthExcludedEntry(providerOverride = null) { if (!this.isConnected) { this.showNotification(i18n.t('notification.connection_required'), 'error'); return; } const providerValue = (providerOverride || this.getOauthExcludedProviderValue() || '').trim(); if (!providerValue) { this.showNotification(i18n.t('oauth_excluded.provider_required'), 'error'); return; } if (!confirm(i18n.t('oauth_excluded.delete_confirm', { provider: providerValue }))) { return; } this.updateOauthExcludedButtonsState(true); this.setOauthExcludedStatus(i18n.t('oauth_excluded.deleting')); try { await this.makeRequest(`/oauth-excluded-models?provider=${encodeURIComponent(providerValue)}`, { method: 'DELETE' }); this.showNotification(i18n.t('oauth_excluded.delete_success'), 'success'); this.clearCache('oauth-excluded-models'); await this.loadOauthExcludedModels(true); this.closeModal(); } catch (error) { this.showNotification(`${i18n.t('oauth_excluded.delete_failed')}: ${error.message}`, 'error'); } finally { this.setOauthExcludedStatus(''); this.updateOauthExcludedButtonsState(false); } }, registerAuthFilesListeners() { if (!this.events || typeof this.events.on !== 'function') { return; } this.events.on('data:config-loaded', async (event) => { const detail = event?.detail || {}; const config = detail.config || {}; const keyStats = detail.keyStats || null; try { await this.loadAuthFiles(keyStats); } catch (error) { console.error('加载认证文件失败:', error); } try { const applied = this.applyOauthExcludedFromConfig(config, { render: true }); if (!applied) { await this.loadOauthExcludedModels(true); } } catch (error) { console.error('加载 OAuth 排除列表失败:', error); } }); this.events.on('connection:status-changed', (event) => { const detail = event?.detail || {}; this.updateOauthExcludedButtonsState(false); if (detail.isConnected) { if (!this.applyOauthExcludedFromConfig(null, { render: true })) { this.renderOauthExcludedModels(); } } else { this.renderOauthExcludedModels(); } }); } };