diff --git a/app.js b/app.js index 5060a7f..9abb8e6 100644 --- a/app.js +++ b/app.js @@ -27,6 +27,10 @@ class CLIProxyManager { // 日志时间戳(用于增量加载) this.latestLogTimestamp = null; + // Auth file filter state cache + this.currentAuthFileFilter = 'all'; + this.cachedAuthFiles = []; + // 主题管理 this.currentTheme = 'light'; @@ -1542,8 +1546,8 @@ class CLIProxyManager { this.renderApiKeys(config['api-keys']); } - // Gemini 密钥 - await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [], keyStats); + // Gemini keys + await this.renderGeminiKeys(this.getGeminiKeysFromConfig(config), keyStats); // Codex 密钥 await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [], keyStats); @@ -2594,35 +2598,67 @@ class CLIProxyManager { } } - // 加载Gemini密钥 + // Load Gemini keys async loadGeminiKeys() { try { const config = await this.getConfig(); - let keys = Array.isArray(config['gemini-api-key']) ? config['gemini-api-key'] : []; - if (keys.length === 0) { - const legacyKeys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []; - keys = legacyKeys.map(key => ({ 'api-key': key })); - } + const keys = this.getGeminiKeysFromConfig(config); await this.renderGeminiKeys(keys); } catch (error) { console.error('加载Gemini密钥失败:', error); } } - // 渲染Gemini密钥列表 + // Extract Gemini keys from config with backward compatibility + getGeminiKeysFromConfig(config) { + if (!config) { + return []; + } + + const geminiKeys = Array.isArray(config['gemini-api-key']) ? config['gemini-api-key'] : []; + if (geminiKeys.length > 0) { + return geminiKeys; + } + + const legacyKeys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []; + return legacyKeys + .map(item => { + if (item && typeof item === 'object') { + return { ...item }; + } + if (typeof item === 'string') { + const trimmed = item.trim(); + if (trimmed) { + return { 'api-key': trimmed }; + } + } + return null; + }) + .filter(Boolean); + } + + // Render Gemini key list async renderGeminiKeys(keys, keyStats = null) { const container = document.getElementById('gemini-keys-list'); if (!container) { return; } const normalizedList = (Array.isArray(keys) ? keys : []).map(item => { + let normalized = null; if (item && typeof item === 'object') { - return { ...item }; + normalized = { ...item }; + } else if (typeof item === 'string') { + const trimmed = item.trim(); + if (trimmed) { + normalized = { 'api-key': trimmed }; + } } - if (typeof item === 'string') { - return { 'api-key': item }; + + if (normalized && !normalized['base-url'] && normalized['base_url']) { + normalized['base-url'] = normalized['base_url']; } - return null; + + return normalized; }).filter(config => config && config['api-key']); this.cachedGeminiKeys = normalizedList; @@ -2649,12 +2685,13 @@ class CLIProxyManager { const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 }; const configJson = JSON.stringify(config).replace(/"/g, '"'); const apiKeyJson = JSON.stringify(rawKey || '').replace(/"/g, '"'); + const baseUrl = config['base-url'] || config['base_url'] || ''; return `
${i18n.t('ai_providers.gemini_item_title')} #${index + 1}
${this.maskApiKey(rawKey || '')}
- ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''} + ${baseUrl ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}
` : ''} ${this.renderHeaderBadges(config.headers)}
@@ -3680,8 +3717,10 @@ class CLIProxyManager { // 渲染认证文件列表 async renderAuthFiles(files, keyStats = null) { const container = document.getElementById('auth-files-list'); + const visibleFiles = Array.isArray(files) ? files.filter(file => file.disabled !== true) : []; + this.cachedAuthFiles = visibleFiles.map(file => ({ ...file })); - if (files.length === 0) { + if (visibleFiles.length === 0) { container.innerHTML = `
@@ -3689,6 +3728,9 @@ class CLIProxyManager {

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

`; + this.updateFilterButtons(new Set(['all'])); + this.bindAuthFileFilterEvents(); + this.applyAuthFileFilterState(); return; } @@ -3700,7 +3742,7 @@ class CLIProxyManager { // 收集所有文件类型(使用API返回的type字段) const existingTypes = new Set(['all']); // 'all' 总是存在 - files.forEach(file => { + visibleFiles.forEach(file => { if (file.type) { existingTypes.add(file.type); } @@ -3709,7 +3751,7 @@ class CLIProxyManager { // 更新筛选按钮显示 this.updateFilterButtons(existingTypes); - container.innerHTML = files.map(file => { + container.innerHTML = visibleFiles.map(file => { // 认证文件的统计匹配逻辑: // 1. 首先尝试完整文件名匹配 // 2. 如果没有匹配,尝试脱敏文件名匹配(去掉扩展名后的脱敏版本) @@ -3814,7 +3856,7 @@ class CLIProxyManager {
`; return ` -
+
${typeBadge}${file.name}
@@ -3842,6 +3884,9 @@ class CLIProxyManager { // 绑定认证文件操作按钮事件(使用事件委托) this.bindAuthFileActionEvents(); + + // Reapply current filter state + this.applyAuthFileFilterState(); } // 更新筛选按钮显示 @@ -3924,6 +3969,7 @@ class CLIProxyManager { // 获取筛选类型 const filterType = clickedBtn.dataset.type; + this.currentAuthFileFilter = filterType || 'all'; // 筛选文件 const fileItems = document.querySelectorAll('.file-item'); @@ -3975,6 +4021,65 @@ class CLIProxyManager { this.refreshFilterButtonTexts(); } + // Apply current filter selection to the list + applyAuthFileFilterState() { + 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); + } + } + + // Remove deleted auth files from cache and DOM instantly + 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)); + + const container = document.getElementById('auth-files-list'); + if (!container) return; + + const fileItems = container.querySelectorAll('.file-item'); + fileItems.forEach(item => { + const fileNameAttr = item.getAttribute('data-file-name'); + if (fileNameAttr && removalSet.has(fileNameAttr)) { + item.remove(); + } + }); + + if (!container.querySelector('.file-item')) { + container.innerHTML = ` +
+ +

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

+

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

+
+ `; + } + } + // 刷新筛选按钮文本(根据 data-i18n-text) refreshFilterButtonTexts() { document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => { @@ -4234,23 +4339,86 @@ class CLIProxyManager { try { await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' }); + this.removeAuthFileElements([filename]); this.clearCache(); // 清除缓存 - this.loadAuthFiles(); + await this.loadAuthFiles(); this.showNotification(i18n.t('auth_files.delete_success'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } } - // 删除所有认证文件 + // Delete auth files (respect current filter) async deleteAllAuthFiles() { - if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return; + 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 { - const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' }); + 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.loadAuthFiles(); - this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success'); + 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'); } diff --git a/i18n.js b/i18n.js index 6d2eb44..ae07fc2 100644 --- a/i18n.js +++ b/i18n.js @@ -225,11 +225,15 @@ const i18n = { 'auth_files.delete_button': '删除', 'auth_files.delete_confirm': '确定要删除文件', 'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!', + 'auth_files.delete_filtered_confirm': '确定要删除筛选出的 {type} 认证文件吗?此操作不可恢复!', 'auth_files.upload_error_json': '只能上传JSON文件', 'auth_files.upload_success': '文件上传成功', 'auth_files.download_success': '文件下载成功', 'auth_files.delete_success': '文件删除成功', 'auth_files.delete_all_success': '成功删除', + 'auth_files.delete_filtered_success': '成功删除 {count} 个 {type} 认证文件', + 'auth_files.delete_filtered_partial': '{type} 认证文件删除完成,成功 {success} 个,失败 {failed} 个', + 'auth_files.delete_filtered_none': '当前筛选类型 ({type}) 下没有可删除的认证文件', 'auth_files.files_count': '个文件', 'auth_files.filter_all': '全部', 'auth_files.filter_qwen': 'Qwen', @@ -684,11 +688,15 @@ const i18n = { 'auth_files.delete_button': 'Delete', 'auth_files.delete_confirm': 'Are you sure you want to delete file', 'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!', + 'auth_files.delete_filtered_confirm': 'Are you sure you want to delete all {type} auth files? This operation cannot be undone!', 'auth_files.upload_error_json': 'Only JSON files are allowed', 'auth_files.upload_success': 'File uploaded successfully', 'auth_files.download_success': 'File downloaded successfully', 'auth_files.delete_success': 'File deleted successfully', 'auth_files.delete_all_success': 'Successfully deleted', + 'auth_files.delete_filtered_success': 'Deleted {count} {type} auth files successfully', + 'auth_files.delete_filtered_partial': '{type} auth files deletion finished: {success} succeeded, {failed} failed', + 'auth_files.delete_filtered_none': 'No deletable auth files under the current filter ({type})', 'auth_files.files_count': 'files', 'auth_files.filter_all': 'All', 'auth_files.filter_qwen': 'Qwen',