feat(app.js, i18n, index.html, styles.css): enhance auth file management with search and pagination controls

- Implemented search functionality for auth files, allowing users to filter by name, type, or provider.
- Added pagination controls to manage the display of auth files, improving navigation through large datasets.
- Updated internationalization strings to support new search and pagination features in both English and Chinese.
- Enhanced styles for the auth file toolbar, search input, and pagination controls for better user experience.
This commit is contained in:
Supra4E8C
2025-11-15 14:54:10 +08:00
parent 295befe42b
commit edb723c12b
4 changed files with 481 additions and 90 deletions

380
app.js
View File

@@ -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 = `
<div class="input-group api-key-input-group">
<input type="text" class="api-key-value-input" placeholder="${i18n.t('ai_providers.openai_key_placeholder')}" value="${this.escapeHtml(keyValue)}">
<input type="text" class="api-key-proxy-input" placeholder="${i18n.t('ai_providers.openai_proxy_placeholder')}" value="${this.escapeHtml(proxyValue)}">
<button type="button" class="btn btn-small btn-danger api-key-remove-btn"><i class="fas fa-trash"></i></button>
</div>
`;
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 = `
<h3>${i18n.t('ai_providers.gemini_add_modal_title')}</h3>
<div class="form-group">
<label for="new-gemini-key">${i18n.t('ai_providers.gemini_add_modal_key_label')}</label>
<textarea id="new-gemini-key" rows="6" placeholder="${i18n.t('ai_providers.gemini_add_modal_key_placeholder')}"></textarea>
<label>${i18n.t('ai_providers.gemini_add_modal_key_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.gemini_add_modal_key_hint')}</p>
</div>
<div class="form-group">
<label for="new-gemini-url">${i18n.t('ai_providers.gemini_add_modal_url_label')}</label>
<input type="text" id="new-gemini-url" placeholder="${i18n.t('ai_providers.gemini_add_modal_url_placeholder')}">
<div id="new-gemini-keys-wrapper" class="api-key-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addGeminiKeyField('new-gemini-keys-wrapper')">${i18n.t('ai_providers.gemini_keys_add_btn')}</button>
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
@@ -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
? `<button type="button" class="btn btn-small btn-danger gemini-key-remove-btn"><i class="fas fa-trash"></i></button>`
: '';
row.innerHTML = `
<div class="input-group api-key-input-group">
<input type="text" class="api-key-value-input" placeholder="${i18n.t('ai_providers.gemini_add_modal_key_placeholder')}" value="${this.escapeHtml(apiKeyValue)}">
<input type="text" class="api-key-proxy-input" placeholder="${i18n.t('ai_providers.gemini_base_url_placeholder')}" value="${this.escapeHtml(baseUrlValue)}">
${removeButtonHtml}
</div>
`;
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 = `
<h3>${i18n.t('ai_providers.gemini_edit_modal_title')}</h3>
<div class="form-group">
<label for="edit-gemini-key">${i18n.t('ai_providers.gemini_edit_modal_key_label')}</label>
<input type="text" id="edit-gemini-key" value="${config['api-key'] ? this.escapeHtml(config['api-key']) : ''}">
</div>
<div class="form-group">
<label for="edit-gemini-url">${i18n.t('ai_providers.gemini_edit_modal_url_label')}</label>
<input type="text" id="edit-gemini-url" value="${config['base-url'] ? this.escapeHtml(config['base-url']) : ''}" placeholder="${i18n.t('ai_providers.gemini_add_modal_url_placeholder')}">
<label>${i18n.t('ai_providers.gemini_edit_modal_key_label')}</label>
<div id="edit-gemini-keys-wrapper" class="api-key-input-list"></div>
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
@@ -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 {
<input type="text" id="new-provider-url" placeholder="${i18n.t('ai_providers.openai_add_modal_url_placeholder')}">
</div>
<div class="form-group">
<label for="new-provider-keys">${i18n.t('ai_providers.openai_add_modal_keys_label')}</label>
<textarea id="new-provider-keys" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_placeholder')}"></textarea>
</div>
<div class="form-group">
<label for="new-provider-proxies">${i18n.t('ai_providers.openai_add_modal_keys_proxy_label')}</label>
<textarea id="new-provider-proxies" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_proxy_placeholder')}"></textarea>
<label>${i18n.t('ai_providers.openai_add_modal_keys_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.openai_keys_hint')}</p>
<div id="new-openai-keys-wrapper" class="api-key-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addApiKeyEntryField('new-openai-keys-wrapper')">${i18n.t('ai_providers.openai_keys_add_btn')}</button>
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
@@ -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 {
<input type="text" id="edit-provider-url" value="${provider?.['base-url'] ? this.escapeHtml(provider['base-url']) : ''}">
</div>
<div class="form-group">
<label for="edit-provider-keys">${i18n.t('ai_providers.openai_edit_modal_keys_label')}</label>
<textarea id="edit-provider-keys" rows="3">${this.escapeHtml(apiKeysText)}</textarea>
</div>
<div class="form-group">
<label for="edit-provider-proxies">${i18n.t('ai_providers.openai_edit_modal_keys_proxy_label')}</label>
<textarea id="edit-provider-proxies" rows="3">${this.escapeHtml(proxiesText)}</textarea>
<label>${i18n.t('ai_providers.openai_edit_modal_keys_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.openai_keys_hint')}</p>
<div id="edit-openai-keys-wrapper" class="api-key-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addApiKeyEntryField('edit-openai-keys-wrapper')">${i18n.t('ai_providers.openai_keys_add_btn')}</button>
</div>
<div class="form-group">
<label>${i18n.t('common.custom_headers_label')}</label>
@@ -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 = `
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h3>${i18n.t('auth_files.empty_title')}</h3>
<p>${i18n.t('auth_files.empty_desc')}</p>
<h3>${i18n.t(titleKey)}</h3>
<p>${i18n.t(descKey)}</p>
</div>
`;
this.authFilesPagination.currentPage = 1;

58
i18n.js
View File

@@ -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',

View File

@@ -519,6 +519,27 @@
</div>
</div>
<div class="card-content">
<div class="auth-file-toolbar">
<div class="auth-file-search-group">
<label for="auth-files-search-input"
data-i18n="auth_files.search_label">搜索配置文件</label>
<div class="auth-file-search">
<i class="fas fa-search"></i>
<input type="text" id="auth-files-search-input"
data-i18n-placeholder="auth_files.search_placeholder"
placeholder="搜索文件名或类型">
</div>
</div>
<div class="auth-file-page-size">
<label for="auth-files-page-size-input"
data-i18n="auth_files.page_size_label">单页数量</label>
<div class="page-size-input">
<input type="number" id="auth-files-page-size-input" min="3" max="60"
step="1">
<span data-i18n="auth_files.page_size_unit">个/页</span>
</div>
</div>
</div>
<div id="auth-files-list" class="file-list file-grid"></div>
<div id="auth-files-pagination" class="pagination-controls" style="display: none;">
<button class="btn btn-secondary pagination-btn" data-action="prev">

View File

@@ -1630,6 +1630,93 @@ input:checked+.slider:before {
white-space: nowrap;
}
/* 认证文件工具栏 */
.auth-file-toolbar {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: flex-end;
margin-bottom: 18px;
}
.auth-file-search-group {
flex: 1;
min-width: 240px;
display: flex;
flex-direction: column;
gap: 6px;
}
.auth-file-search-group label,
.auth-file-page-size label {
font-weight: 600;
color: var(--text-secondary);
}
.auth-file-search {
display: flex;
align-items: center;
border: 1px solid var(--border-primary);
border-radius: 10px;
padding: 0 12px;
background: var(--bg-secondary);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.03);
}
[data-theme="dark"] .auth-file-search {
background: var(--bg-tertiary);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.04);
}
.auth-file-search i {
color: var(--text-tertiary);
margin-right: 8px;
}
.auth-file-search input {
border: none;
background: transparent;
color: var(--text-primary);
width: 100%;
padding: 10px 0;
font-size: 0.95rem;
}
.auth-file-search input:focus {
outline: none;
}
.auth-file-page-size {
min-width: 180px;
display: flex;
flex-direction: column;
gap: 6px;
}
.page-size-input {
display: flex;
align-items: center;
gap: 8px;
border: 1px solid var(--border-primary);
border-radius: 10px;
padding: 8px 12px;
background: var(--bg-quaternary);
}
.page-size-input input {
width: 80px;
padding: 6px 8px;
border: 1px solid var(--border-secondary);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
}
.page-size-input span {
font-size: 0.9rem;
color: var(--text-tertiary);
}
/* 认证文件筛选按钮 */
.auth-file-filter {
display: flex;
@@ -2873,6 +2960,31 @@ input:checked+.slider:before {
align-self: center;
}
.api-key-input-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.api-key-input-group {
display: flex;
gap: 8px;
width: 100%;
}
.api-key-input-group .api-key-value-input {
flex: 2;
}
.api-key-input-group .api-key-proxy-input {
flex: 1;
min-width: 180px;
}
.api-key-input-group .api-key-remove-btn {
align-self: center;
}
.header-input-list {
display: flex;
flex-direction: column;