diff --git a/i18n.js b/i18n.js index 0a328ea..f71f7fb 100644 --- a/i18n.js +++ b/i18n.js @@ -149,6 +149,10 @@ const i18n = { 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥', 'ai_providers.gemini_edit_modal_key_label': 'API密钥:', 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?', + 'ai_providers.excluded_models_label': '排除的模型 (可选):', + 'ai_providers.excluded_models_placeholder': '用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash', + 'ai_providers.excluded_models_hint': '留空表示不过滤;保存时会自动去重并忽略空白。', + 'ai_providers.excluded_models_count': '排除 {count} 个模型', 'ai_providers.codex_title': 'Codex API 配置', 'ai_providers.codex_add_button': '添加配置', @@ -755,6 +759,10 @@ const i18n = { 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key', 'ai_providers.gemini_edit_modal_key_label': 'API Key:', 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?', + 'ai_providers.excluded_models_label': 'Excluded models (optional):', + 'ai_providers.excluded_models_placeholder': 'Comma or newline separated, e.g. gemini-1.5-pro, gemini-1.5-flash', + 'ai_providers.excluded_models_hint': 'Leave empty to allow all models; values are trimmed and deduplicated automatically.', + 'ai_providers.excluded_models_count': 'Excluding {count} models', 'ai_providers.codex_title': 'Codex API Configuration', 'ai_providers.codex_add_button': 'Add Configuration', diff --git a/src/modules/ai-providers.js b/src/modules/ai-providers.js index 82d4d5c..5eabe4f 100644 --- a/src/modules/ai-providers.js +++ b/src/modules/ai-providers.js @@ -59,6 +59,59 @@ const normalizeModelList = (payload) => { return []; }; +const normalizeExcludedModels = (input) => { + const rawList = Array.isArray(input) + ? input + : (typeof input === 'string' ? input.split(/[\n,]/) : []); + const seen = new Set(); + const normalized = []; + + rawList.forEach(item => { + if (item === undefined || item === null) { + return; + } + const trimmed = String(item).trim(); + if (!trimmed) return; + const key = trimmed.toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + normalized.push(trimmed); + }); + + return normalized; +}; + +export function collectExcludedModels(textareaId) { + const textarea = document.getElementById(textareaId); + if (!textarea) return []; + return normalizeExcludedModels(textarea.value); +} + +export function setExcludedModelsValue(textareaId, models = []) { + const textarea = document.getElementById(textareaId); + if (!textarea) return; + textarea.value = normalizeExcludedModels(models).join('\n'); +} + +export function renderExcludedModelBadges(models) { + const normalized = normalizeExcludedModels(models); + if (!normalized.length) { + return ''; + } + const badges = normalized.map(model => ` + + ${this.escapeHtml(model)} + + `).join(''); + + return ` +
${i18n.t('ai_providers.excluded_models_hint')}
+ +${i18n.t('ai_providers.excluded_models_hint')}
+ +${i18n.t('ai_providers.excluded_models_hint')}
+ +${i18n.t('common.custom_headers_hint')}
@@ -539,6 +617,7 @@ export function showAddCodexKeyModal() { modal.style.display = 'block'; this.populateHeaderFields('new-codex-headers-wrapper'); + this.setExcludedModelsValue('new-codex-excluded-models'); } export async function addCodexKey() { @@ -546,6 +625,7 @@ export async function addCodexKey() { const baseUrl = document.getElementById('new-codex-url').value.trim(); const proxyUrl = document.getElementById('new-codex-proxy').value.trim(); const headers = this.collectHeaderInputs('new-codex-headers-wrapper'); + const excludedModels = this.collectExcludedModels('new-codex-excluded-models'); if (!apiKey) { this.showNotification(i18n.t('notification.field_required'), 'error'); @@ -560,7 +640,7 @@ export async function addCodexKey() { const data = await this.makeRequest('/codex-api-key'); const currentKeys = this.normalizeArrayResponse(data, 'codex-api-key').map(item => ({ ...item })); - const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, {}, headers); + const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, {}, headers, excludedModels); currentKeys.push(newConfig); @@ -596,6 +676,11 @@ export function editCodexKey(index, config) {${i18n.t('ai_providers.excluded_models_hint')}
+ +${i18n.t('common.custom_headers_hint')}
@@ -610,6 +695,7 @@ export function editCodexKey(index, config) { modal.style.display = 'block'; this.populateHeaderFields('edit-codex-headers-wrapper', config.headers || null); + this.setExcludedModelsValue('edit-codex-excluded-models', config['excluded-models'] || []); } export async function updateCodexKey(index) { @@ -617,6 +703,7 @@ export async function updateCodexKey(index) { const baseUrl = document.getElementById('edit-codex-url').value.trim(); const proxyUrl = document.getElementById('edit-codex-proxy').value.trim(); const headers = this.collectHeaderInputs('edit-codex-headers-wrapper'); + const excludedModels = this.collectExcludedModels('edit-codex-excluded-models'); if (!apiKey) { this.showNotification(i18n.t('notification.field_required'), 'error'); @@ -636,7 +723,7 @@ export async function updateCodexKey(index) { } const original = currentList[index] ? { ...currentList[index] } : {}; - const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, original, headers); + const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, original, headers, excludedModels); await this.makeRequest('/codex-api-key', { method: 'PATCH', @@ -1607,5 +1694,8 @@ export const aiProvidersModule = { populateModelFields, collectModelInputs, renderModelBadges, + renderExcludedModelBadges, + collectExcludedModels, + setExcludedModelsValue, validateOpenAIProviderInput }; diff --git a/src/modules/api-keys.js b/src/modules/api-keys.js index fa0b715..07d5199 100644 --- a/src/modules/api-keys.js +++ b/src/modules/api-keys.js @@ -230,7 +230,7 @@ export const apiKeysModule = { }, // 构造Codex配置,保持未展示的字段 - buildCodexConfig(apiKey, baseUrl, proxyUrl, original = {}, headers = null) { + buildCodexConfig(apiKey, baseUrl, proxyUrl, original = {}, headers = null, excludedModels = null) { const result = { ...original, 'api-key': apiKey, @@ -238,6 +238,9 @@ export const apiKeysModule = { 'proxy-url': proxyUrl || '' }; this.applyHeadersToConfig(result, headers); + if (Array.isArray(excludedModels)) { + result['excluded-models'] = excludedModels; + } return result; }, diff --git a/styles.css b/styles.css index 22d2f84..7289df6 100644 --- a/styles.css +++ b/styles.css @@ -2221,6 +2221,12 @@ input:checked+.slider:before { font-size: 0.85rem; } +.provider-item .excluded-models .provider-model-tag { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + .provider-model-tag .model-name { font-weight: 600; }