diff --git a/app.js b/app.js index 4828470..3cb3fea 100644 --- a/app.js +++ b/app.js @@ -36,6 +36,9 @@ class CLIProxyManager { totalPages: 1 }; this.authFileStatsCache = {}; + this.authFileSearchQuery = ''; + this.authFilesPageSizeKey = 'authFilesPageSize'; + this.loadAuthFilePreferences(); // Vertex AI credential import state this.vertexImportState = { @@ -63,6 +66,31 @@ class CLIProxyManager { this.init(); } + loadAuthFilePreferences() { + try { + if (typeof localStorage === 'undefined') { + return; + } + const savedPageSize = parseInt(localStorage.getItem(this.authFilesPageSizeKey), 10); + if (Number.isFinite(savedPageSize)) { + this.authFilesPagination.pageSize = this.normalizeAuthFilesPageSize(savedPageSize); + } + } catch (error) { + console.warn('Failed to restore auth file preferences:', error); + } + } + + normalizeAuthFilesPageSize(value) { + const defaultSize = 9; + const minSize = 3; + const maxSize = 60; + const parsed = parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return defaultSize; + } + return Math.min(maxSize, Math.max(minSize, parsed)); + } + // 简易防抖,减少频繁写 localStorage debounce(fn, delay = 400) { let timer; @@ -623,6 +651,9 @@ class CLIProxyManager { authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); } this.bindAuthFilesPaginationEvents(); + this.bindAuthFilesSearchControl(); + this.bindAuthFilesPageSizeControl(); + this.syncAuthFileControls(); // Vertex AI credential import const vertexSelectFile = document.getElementById('vertex-select-file'); @@ -2573,6 +2604,68 @@ class CLIProxyManager { return Object.keys(headers).length ? headers : null; } + addApiKeyEntryField(wrapperId, entry = {}) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return; + + const row = document.createElement('div'); + row.className = 'api-key-input-row'; + const keyValue = typeof entry?.['api-key'] === 'string' ? entry['api-key'] : ''; + const proxyValue = typeof entry?.['proxy-url'] === 'string' ? entry['proxy-url'] : ''; + row.innerHTML = ` +
+ + + +
+ `; + + const removeBtn = row.querySelector('.api-key-remove-btn'); + if (removeBtn) { + removeBtn.addEventListener('click', () => { + wrapper.removeChild(row); + if (wrapper.childElementCount === 0) { + this.addApiKeyEntryField(wrapperId); + } + }); + } + + wrapper.appendChild(row); + } + + populateApiKeyEntryFields(wrapperId, entries = []) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return; + wrapper.innerHTML = ''; + + if (!Array.isArray(entries) || entries.length === 0) { + this.addApiKeyEntryField(wrapperId); + return; + } + + entries.forEach(entry => this.addApiKeyEntryField(wrapperId, entry)); + } + + collectApiKeyEntryInputs(wrapperId) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return []; + + const rows = Array.from(wrapper.querySelectorAll('.api-key-input-row')); + const entries = []; + + rows.forEach(row => { + const keyInput = row.querySelector('.api-key-value-input'); + const proxyInput = row.querySelector('.api-key-proxy-input'); + const key = keyInput ? keyInput.value.trim() : ''; + const proxy = proxyInput ? proxyInput.value.trim() : ''; + if (key) { + entries.push({ 'api-key': key, 'proxy-url': proxy }); + } + }); + + return entries; + } + // 规范化并写入请求头 applyHeadersToConfig(target, headers) { if (!target) { @@ -2852,13 +2945,10 @@ class CLIProxyManager { modalBody.innerHTML = `

${i18n.t('ai_providers.gemini_add_modal_title')}

- - +

${i18n.t('ai_providers.gemini_add_modal_key_hint')}

-
-
- - +
+
@@ -2873,25 +2963,16 @@ class CLIProxyManager { `; modal.style.display = 'block'; + this.populateGeminiKeyFields('new-gemini-keys-wrapper'); this.populateHeaderFields('new-gemini-headers-wrapper'); } // 添加Gemini密钥 async addGeminiKey() { - const keyInput = document.getElementById('new-gemini-key'); - const baseUrlInput = document.getElementById('new-gemini-url'); - if (!keyInput) { - return; - } - - const keys = keyInput.value - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); - const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : ''; + const entries = this.collectGeminiKeyFieldInputs('new-gemini-keys-wrapper'); const headers = this.collectHeaderInputs('new-gemini-headers-wrapper'); - if (keys.length === 0) { + if (!entries.length) { this.showNotification(i18n.t('notification.gemini_multi_input_required'), 'error'); return; } @@ -2906,7 +2987,8 @@ class CLIProxyManager { let skippedCount = 0; let failedCount = 0; - for (const apiKey of keys) { + for (const entry of entries) { + const apiKey = entry['api-key']; if (!apiKey) { continue; } @@ -2923,6 +3005,7 @@ class CLIProxyManager { } const newConfig = { 'api-key': apiKey }; + const baseUrl = entry['base-url']; if (baseUrl) { newConfig['base-url'] = baseUrl; } else { @@ -2967,6 +3050,76 @@ class CLIProxyManager { } } + addGeminiKeyField(wrapperId, entry = {}, options = {}) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return; + + const row = document.createElement('div'); + row.className = 'api-key-input-row'; + const apiKeyValue = typeof entry?.['api-key'] === 'string' ? entry['api-key'] : ''; + const baseUrlValue = typeof entry?.['base-url'] === 'string' + ? entry['base-url'] + : (typeof entry?.['base_url'] === 'string' ? entry['base_url'] : ''); + const allowRemoval = options.allowRemoval !== false; + const removeButtonHtml = allowRemoval + ? `` + : ''; + row.innerHTML = ` +
+ + + ${removeButtonHtml} +
+ `; + + if (allowRemoval) { + const removeBtn = row.querySelector('.gemini-key-remove-btn'); + if (removeBtn) { + removeBtn.addEventListener('click', () => { + wrapper.removeChild(row); + if (wrapper.childElementCount === 0) { + this.addGeminiKeyField(wrapperId, {}, options); + } + }); + } + } + + wrapper.appendChild(row); + } + + populateGeminiKeyFields(wrapperId, entries = [], options = {}) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return; + wrapper.innerHTML = ''; + + if (!Array.isArray(entries) || entries.length === 0) { + this.addGeminiKeyField(wrapperId, {}, options); + return; + } + + entries.forEach(entry => this.addGeminiKeyField(wrapperId, entry, options)); + } + + collectGeminiKeyFieldInputs(wrapperId) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return []; + + const rows = Array.from(wrapper.querySelectorAll('.api-key-input-row')); + const entries = []; + + rows.forEach(row => { + const keyInput = row.querySelector('.api-key-value-input'); + const urlInput = row.querySelector('.api-key-proxy-input'); + const apiKey = keyInput ? keyInput.value.trim() : ''; + const baseUrl = urlInput ? urlInput.value.trim() : ''; + if (apiKey) { + entries.push({ 'api-key': apiKey, 'base-url': baseUrl }); + } + }); + + return entries; + } + // 编辑Gemini密钥 editGeminiKey(index, config) { const modal = document.getElementById('modal'); @@ -2976,12 +3129,8 @@ class CLIProxyManager { modalBody.innerHTML = `

${i18n.t('ai_providers.gemini_edit_modal_title')}

- - -
-
- - + +
@@ -2996,14 +3145,20 @@ class CLIProxyManager { `; modal.style.display = 'block'; + this.populateGeminiKeyFields('edit-gemini-keys-wrapper', [config], { allowRemoval: false }); this.populateHeaderFields('edit-gemini-headers-wrapper', config.headers || null); } // 更新Gemini密钥 async updateGeminiKey(index) { - const newKey = document.getElementById('edit-gemini-key').value.trim(); - const baseUrlInput = document.getElementById('edit-gemini-url'); - const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : ''; + const entries = this.collectGeminiKeyFieldInputs('edit-gemini-keys-wrapper'); + if (!entries.length) { + this.showNotification(i18n.t('notification.please_enter') + ' ' + i18n.t('notification.gemini_api_key'), 'error'); + return; + } + const entry = entries[0]; + const newKey = entry['api-key']; + const baseUrl = entry['base-url'] || ''; const headers = this.collectHeaderInputs('edit-gemini-headers-wrapper'); if (!newKey) { @@ -3669,12 +3824,10 @@ class CLIProxyManager {
- - -
-
- - + +

${i18n.t('ai_providers.openai_keys_hint')}

+
+
@@ -3697,14 +3850,14 @@ class CLIProxyManager { modal.style.display = 'block'; this.populateModelFields('new-provider-models-wrapper', []); this.populateHeaderFields('new-openai-headers-wrapper'); + this.populateApiKeyEntryFields('new-openai-keys-wrapper'); } // 添加OpenAI提供商 async addOpenAIProvider() { const name = document.getElementById('new-provider-name').value.trim(); const baseUrl = document.getElementById('new-provider-url').value.trim(); - const keysText = document.getElementById('new-provider-keys').value.trim(); - const proxiesText = document.getElementById('new-provider-proxies').value.trim(); + const apiKeyEntries = this.collectApiKeyEntryInputs('new-openai-keys-wrapper'); const models = this.collectModelInputs('new-provider-models-wrapper'); const headers = this.collectHeaderInputs('new-openai-headers-wrapper'); @@ -3716,13 +3869,6 @@ class CLIProxyManager { const data = await this.makeRequest('/openai-compatibility'); const currentProviders = data['openai-compatibility'] || []; - const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; - const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : []; - const apiKeyEntries = apiKeys.map((key, idx) => ({ - 'api-key': key, - 'proxy-url': proxies[idx] || '' - })); - const newProvider = { name, 'base-url': baseUrl, @@ -3762,8 +3908,6 @@ class CLIProxyManager { apiKeyEntries = provider['api-keys'].map(key => ({ 'api-key': key, 'proxy-url': '' })); } - const apiKeysText = apiKeyEntries.map(entry => entry?.['api-key'] || '').join('\n'); - const proxiesText = apiKeyEntries.map(entry => entry?.['proxy-url'] || '').join('\n'); const models = Array.isArray(provider?.models) ? provider.models : []; modalBody.innerHTML = ` @@ -3777,12 +3921,10 @@ class CLIProxyManager {
- - -
-
- - + +

${i18n.t('ai_providers.openai_keys_hint')}

+
+
@@ -3805,14 +3947,14 @@ class CLIProxyManager { modal.style.display = 'block'; this.populateModelFields('edit-provider-models-wrapper', models); this.populateHeaderFields('edit-openai-headers-wrapper', provider?.headers || null); + this.populateApiKeyEntryFields('edit-openai-keys-wrapper', apiKeyEntries); } // 更新OpenAI提供商 async updateOpenAIProvider(index) { const name = document.getElementById('edit-provider-name').value.trim(); const baseUrl = document.getElementById('edit-provider-url').value.trim(); - const keysText = document.getElementById('edit-provider-keys').value.trim(); - const proxiesText = document.getElementById('edit-provider-proxies').value.trim(); + const apiKeyEntries = this.collectApiKeyEntryInputs('edit-openai-keys-wrapper'); const models = this.collectModelInputs('edit-provider-models-wrapper'); const headers = this.collectHeaderInputs('edit-openai-headers-wrapper'); @@ -3821,13 +3963,6 @@ class CLIProxyManager { } try { - const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; - const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : []; - const apiKeyEntries = apiKeys.map((key, idx) => ({ - 'api-key': key, - 'proxy-url': proxies[idx] || '' - })); - const updatedProvider = { name, 'base-url': baseUrl, @@ -3894,6 +4029,7 @@ class CLIProxyManager { this.cachedAuthFiles = visibleFiles.map(file => ({ ...file })); this.authFileStatsCache = stats || {}; + this.syncAuthFileControls(); if (this.cachedAuthFiles.length === 0) { container.innerHTML = ` @@ -4112,15 +4248,116 @@ class CLIProxyManager { getFilteredAuthFiles(filterType = this.currentAuthFileFilter) { const files = Array.isArray(this.cachedAuthFiles) ? this.cachedAuthFiles : []; const filterValue = (filterType || 'all').toLowerCase(); - if (filterValue === 'all') { - return files; - } + const keyword = (this.authFileSearchQuery || '').trim().toLowerCase(); + return files.filter(file => { - const type = (file?.type || 'unknown').toLowerCase(); - return type === filterValue; + if (!file) return false; + const type = (file.type || 'unknown').toLowerCase(); + const name = (file.name || '').toLowerCase(); + const provider = (file.provider || '').toLowerCase(); + + const matchesType = filterValue === 'all' ? true : type === filterValue; + if (!matchesType) 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; @@ -4128,13 +4365,22 @@ class CLIProxyManager { 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('auth_files.empty_title')}

-

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

+

${i18n.t(titleKey)}

+

${i18n.t(descKey)}

`; this.authFilesPagination.currentPage = 1; diff --git a/i18n.js b/i18n.js index fc2ed59..91b786f 100644 --- a/i18n.js +++ b/i18n.js @@ -141,14 +141,13 @@ const i18n = { 'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥', 'ai_providers.gemini_item_title': 'Gemini密钥', 'ai_providers.gemini_add_modal_title': '添加Gemini API密钥', - 'ai_providers.gemini_add_modal_key_label': 'API密钥列表:', - 'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥(每行一个)', - 'ai_providers.gemini_add_modal_key_hint': '可一次粘贴多个密钥,每行一个。', - 'ai_providers.gemini_add_modal_url_label': 'Base URL (可选):', - 'ai_providers.gemini_add_modal_url_placeholder': '例如: https://generativelanguage.googleapis.com', + 'ai_providers.gemini_add_modal_key_label': 'API密钥', + 'ai_providers.gemini_add_modal_key_placeholder': '输入 Gemini API 密钥', + 'ai_providers.gemini_add_modal_key_hint': '逐条输入密钥,可同时指定可选 Base URL。', + 'ai_providers.gemini_keys_add_btn': '添加密钥', + 'ai_providers.gemini_base_url_placeholder': '可选 Base URL,如 https://generativelanguage.googleapis.com', 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥', 'ai_providers.gemini_edit_modal_key_label': 'API密钥:', - 'ai_providers.gemini_edit_modal_url_label': 'Base URL (可选):', 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?', 'ai_providers.codex_title': 'Codex API 配置', @@ -200,10 +199,12 @@ const i18n = { 'ai_providers.openai_add_modal_name_placeholder': '例如: openrouter', 'ai_providers.openai_add_modal_url_label': 'Base URL:', 'ai_providers.openai_add_modal_url_placeholder': '例如: https://openrouter.ai/api/v1', - 'ai_providers.openai_add_modal_keys_label': 'API密钥 (每行一个):', - 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', - 'ai_providers.openai_add_modal_keys_proxy_label': '代理 URL (按行对应,可选):', - 'ai_providers.openai_add_modal_keys_proxy_placeholder': 'socks5://proxy.example.com:1080\n', + 'ai_providers.openai_add_modal_keys_label': 'API密钥', + 'ai_providers.openai_edit_modal_keys_label': 'API密钥', + 'ai_providers.openai_keys_hint': '每个密钥可搭配一个可选代理地址,更便于管理。', + 'ai_providers.openai_keys_add_btn': '添加密钥', + 'ai_providers.openai_key_placeholder': '输入 sk- 开头的密钥', + 'ai_providers.openai_proxy_placeholder': '可选代理 URL (如 socks5://...)', 'ai_providers.openai_add_modal_models_label': '模型列表 (name[, alias] 每行一个):', 'ai_providers.openai_models_hint': '示例:gpt-4o-mini 或 moonshotai/kimi-k2:free, kimi-k2', 'ai_providers.openai_model_name_placeholder': '模型名称,如 moonshotai/kimi-k2:free', @@ -212,8 +213,7 @@ const i18n = { 'ai_providers.openai_edit_modal_title': '编辑OpenAI兼容提供商', 'ai_providers.openai_edit_modal_name_label': '提供商名称:', 'ai_providers.openai_edit_modal_url_label': 'Base URL:', - 'ai_providers.openai_edit_modal_keys_label': 'API密钥 (每行一个):', - 'ai_providers.openai_edit_modal_keys_proxy_label': '代理 URL (按行对应,可选):', + 'ai_providers.openai_edit_modal_keys_label': 'API密钥', 'ai_providers.openai_edit_modal_models_label': '模型列表 (name[, alias] 每行一个):', 'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?', 'ai_providers.openai_keys_count': '密钥数量', @@ -228,6 +228,8 @@ const i18n = { 'auth_files.delete_all_button': '删除全部', 'auth_files.empty_title': '暂无认证文件', 'auth_files.empty_desc': '点击上方按钮上传第一个文件', + 'auth_files.search_empty_title': '没有匹配的配置文件', + 'auth_files.search_empty_desc': '请调整筛选条件或清空搜索关键字再试一次。', 'auth_files.file_size': '大小', 'auth_files.file_modified': '修改时间', 'auth_files.download_button': '下载', @@ -247,6 +249,10 @@ const i18n = { 'auth_files.pagination_prev': '上一页', 'auth_files.pagination_next': '下一页', 'auth_files.pagination_info': '第 {current} / {total} 页 · 共 {count} 个文件', + 'auth_files.search_label': '搜索配置文件', + 'auth_files.search_placeholder': '输入名称、类型或提供方关键字', + 'auth_files.page_size_label': '单页数量', + 'auth_files.page_size_unit': '个/页', 'auth_files.filter_all': '全部', 'auth_files.filter_qwen': 'Qwen', 'auth_files.filter_gemini': 'Gemini', @@ -638,13 +644,12 @@ const i18n = { 'ai_providers.gemini_item_title': 'Gemini Key', 'ai_providers.gemini_add_modal_title': 'Add Gemini API Key', 'ai_providers.gemini_add_modal_key_label': 'API Keys:', - 'ai_providers.gemini_add_modal_key_placeholder': 'Enter Gemini API keys (one per line)', - 'ai_providers.gemini_add_modal_key_hint': 'You can paste multiple keys, one per line.', - 'ai_providers.gemini_add_modal_url_label': 'Base URL (optional):', - 'ai_providers.gemini_add_modal_url_placeholder': 'e.g. https://generativelanguage.googleapis.com', + 'ai_providers.gemini_add_modal_key_placeholder': 'Enter Gemini API key', + 'ai_providers.gemini_add_modal_key_hint': 'Add keys one by one and optionally specify a Base URL.', + 'ai_providers.gemini_keys_add_btn': 'Add Key', + 'ai_providers.gemini_base_url_placeholder': 'Optional Base URL, e.g. https://generativelanguage.googleapis.com', 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key', 'ai_providers.gemini_edit_modal_key_label': 'API Key:', - 'ai_providers.gemini_edit_modal_url_label': 'Base URL (optional):', 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?', 'ai_providers.codex_title': 'Codex API Configuration', @@ -696,10 +701,12 @@ const i18n = { 'ai_providers.openai_add_modal_name_placeholder': 'e.g.: openrouter', 'ai_providers.openai_add_modal_url_label': 'Base URL:', 'ai_providers.openai_add_modal_url_placeholder': 'e.g.: https://openrouter.ai/api/v1', - 'ai_providers.openai_add_modal_keys_label': 'API Keys (one per line):', - 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', - 'ai_providers.openai_add_modal_keys_proxy_label': 'Proxy URL (one per line, optional):', - 'ai_providers.openai_add_modal_keys_proxy_placeholder': 'socks5://proxy.example.com:1080\n', + 'ai_providers.openai_add_modal_keys_label': 'API Keys', + 'ai_providers.openai_edit_modal_keys_label': 'API Keys', + 'ai_providers.openai_keys_hint': 'Add each key separately with an optional proxy URL to keep things organized.', + 'ai_providers.openai_keys_add_btn': 'Add Key', + 'ai_providers.openai_key_placeholder': 'sk-... key', + 'ai_providers.openai_proxy_placeholder': 'Optional proxy URL (e.g. socks5://...)', 'ai_providers.openai_add_modal_models_label': 'Model List (name[, alias] one per line):', 'ai_providers.openai_models_hint': 'Example: gpt-4o-mini or moonshotai/kimi-k2:free, kimi-k2', 'ai_providers.openai_model_name_placeholder': 'Model name, e.g. moonshotai/kimi-k2:free', @@ -708,8 +715,7 @@ const i18n = { 'ai_providers.openai_edit_modal_title': 'Edit OpenAI Compatible Provider', 'ai_providers.openai_edit_modal_name_label': 'Provider Name:', 'ai_providers.openai_edit_modal_url_label': 'Base URL:', - 'ai_providers.openai_edit_modal_keys_label': 'API Keys (one per line):', - 'ai_providers.openai_edit_modal_keys_proxy_label': 'Proxy URL (one per line, optional):', + 'ai_providers.openai_edit_modal_keys_label': 'API Keys', 'ai_providers.openai_edit_modal_models_label': 'Model List (name[, alias] one per line):', 'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?', 'ai_providers.openai_keys_count': 'Keys Count', @@ -724,6 +730,8 @@ const i18n = { 'auth_files.delete_all_button': 'Delete All', 'auth_files.empty_title': 'No Auth Files', 'auth_files.empty_desc': 'Click the button above to upload the first file', + 'auth_files.search_empty_title': 'No matching files', + 'auth_files.search_empty_desc': 'Try changing the filters or clearing the search box.', 'auth_files.file_size': 'Size', 'auth_files.file_modified': 'Modified', 'auth_files.download_button': 'Download', @@ -743,6 +751,10 @@ const i18n = { 'auth_files.pagination_prev': 'Previous', 'auth_files.pagination_next': 'Next', 'auth_files.pagination_info': 'Page {current} / {total} · {count} files', + 'auth_files.search_label': 'Search configs', + 'auth_files.search_placeholder': 'Filter by name, type, or provider', + 'auth_files.page_size_label': 'Per page', + 'auth_files.page_size_unit': 'items', 'auth_files.filter_all': 'All', 'auth_files.filter_qwen': 'Qwen', 'auth_files.filter_gemini': 'Gemini', diff --git a/index.html b/index.html index 97c1077..0d00155 100644 --- a/index.html +++ b/index.html @@ -519,6 +519,27 @@
+
+
+ + +
+
+ +
+ + 个/页 +
+
+