@@ -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
+ ? `
+ `;
+
+ 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 = `
@@ -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 @@