// AI 提供商配置相关方法模块 // 这些函数依赖于 CLIProxyManager 实例上的 makeRequest/getConfig/clearCache/showNotification 等能力, // 以及 apiKeysModule 中的工具方法(如 applyHeadersToConfig/renderHeaderBadges)。 const getStatsBySource = (stats) => { if (stats && typeof stats === 'object' && stats.bySource) { return stats.bySource; } return stats || {}; }; const buildModelEndpoint = (baseUrl) => { if (!baseUrl) return ''; try { return new URL('/v1/model', baseUrl).toString(); } catch (_) { const trimmed = String(baseUrl).trim().replace(/\/+$/g, ''); return trimmed ? `${trimmed}/v1/model` : ''; } }; const normalizeModelList = (payload) => { const toModel = (entry) => { if (typeof entry === 'string') { return { name: entry }; } if (!entry || typeof entry !== 'object') { return null; } const name = entry.id || entry.name || entry.model || entry.value; if (!name) return null; const alias = entry.alias || entry.display_name || entry.displayName; const description = entry.description || entry.note || entry.comment; const model = { name: String(name) }; if (alias && alias !== name) { model.alias = String(alias); } if (description) { model.description = String(description); } return model; }; if (Array.isArray(payload)) { return payload.map(toModel).filter(Boolean); } if (payload && typeof payload === 'object') { if (Array.isArray(payload.data)) { return payload.data.map(toModel).filter(Boolean); } if (Array.isArray(payload.models)) { return payload.models.map(toModel).filter(Boolean); } } return []; }; export async function loadGeminiKeys() { try { const config = await this.getConfig(); const keys = this.getGeminiKeysFromConfig(config); await this.renderGeminiKeys(keys); } catch (error) { console.error('加载Gemini密钥失败:', error); } } export function 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); } export async function 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') { normalized = { ...item }; } else if (typeof item === 'string') { const trimmed = item.trim(); if (trimmed) { normalized = { 'api-key': trimmed }; } } if (normalized && !normalized['base-url'] && normalized['base_url']) { normalized['base-url'] = normalized['base_url']; } return normalized; }).filter(config => config && config['api-key']); this.cachedGeminiKeys = normalizedList; if (normalizedList.length === 0) { container.innerHTML = `

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

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

`; return; } if (!keyStats) { keyStats = await this.getKeyStats(); } const statsBySource = getStatsBySource(keyStats); container.innerHTML = normalizedList.map((config, index) => { const rawKey = config['api-key'] || ''; const masked = this.maskApiKey(rawKey || ''); const maskedDisplay = this.escapeHtml(masked); const usageStats = (rawKey && (statsBySource[rawKey] || statsBySource[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}
${i18n.t('common.api_key')}: ${maskedDisplay}
${baseUrl ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}
` : ''} ${this.renderHeaderBadges(config.headers)}
${i18n.t('stats.success')}: ${usageStats.success} ${i18n.t('stats.failure')}: ${usageStats.failure}
`; }).join(''); } export function showAddGeminiKeyModal() { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `

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

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

${i18n.t('common.custom_headers_hint')}

`; modal.style.display = 'block'; this.populateGeminiKeyFields('new-gemini-keys-wrapper'); this.populateHeaderFields('new-gemini-headers-wrapper'); } export async function addGeminiKey() { const entries = this.collectGeminiKeyFieldInputs('new-gemini-keys-wrapper'); const headers = this.collectHeaderInputs('new-gemini-headers-wrapper'); if (!entries.length) { this.showNotification(i18n.t('notification.gemini_multi_input_required'), 'error'); return; } try { const data = await this.makeRequest('/gemini-api-key'); let currentKeys = Array.isArray(data['gemini-api-key']) ? data['gemini-api-key'] : []; const existingKeys = new Set(currentKeys.map(item => item && item['api-key']).filter(Boolean)); const batchSeen = new Set(); let successCount = 0; let skippedCount = 0; let failedCount = 0; for (const entry of entries) { const apiKey = entry['api-key']; if (!apiKey) { continue; } if (batchSeen.has(apiKey)) { skippedCount++; continue; } batchSeen.add(apiKey); if (existingKeys.has(apiKey)) { skippedCount++; continue; } const newConfig = { 'api-key': apiKey }; const baseUrl = entry['base-url']; if (baseUrl) { newConfig['base-url'] = baseUrl; } else { delete newConfig['base-url']; } this.applyHeadersToConfig(newConfig, headers); const nextKeys = [...currentKeys, newConfig]; try { await this.makeRequest('/gemini-api-key', { method: 'PUT', body: JSON.stringify(nextKeys) }); currentKeys = nextKeys; existingKeys.add(apiKey); successCount++; } catch (error) { failedCount++; console.error('Gemini key add failed:', error); } } this.clearCache(); // 清除缓存 this.closeModal(); this.loadGeminiKeys(); if (successCount === 1 && skippedCount === 0 && failedCount === 0) { this.showNotification(i18n.t('notification.gemini_key_added'), 'success'); return; } const summaryTemplate = i18n.t('notification.gemini_multi_summary'); const summary = summaryTemplate .replace('{success}', successCount) .replace('{skipped}', skippedCount) .replace('{failed}', failedCount); const status = failedCount > 0 ? 'warning' : (successCount > 0 ? 'success' : 'info'); this.showNotification(summary, status); } catch (error) { this.showNotification(`${i18n.t('notification.gemini_multi_failed')}: ${error.message}`, 'error'); } } export function 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); } export function 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)); } export function 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; } export function editGeminiKey(index, config) { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); this.currentGeminiEditConfig = config || {}; modalBody.innerHTML = `

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

${i18n.t('common.custom_headers_hint')}

`; modal.style.display = 'block'; this.populateGeminiKeyFields('edit-gemini-keys-wrapper', [config], { allowRemoval: false }); this.populateHeaderFields('edit-gemini-headers-wrapper', config.headers || null); } export async function updateGeminiKey(index) { 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) { this.showNotification(i18n.t('notification.please_enter') + ' ' + i18n.t('notification.gemini_api_key'), 'error'); return; } try { const existingConfig = (this.cachedGeminiKeys && this.cachedGeminiKeys[index]) || this.currentGeminiEditConfig || {}; const newConfig = { ...existingConfig, 'api-key': newKey }; if (baseUrl) { newConfig['base-url'] = baseUrl; } else { delete newConfig['base-url']; } this.applyHeadersToConfig(newConfig, headers); await this.makeRequest('/gemini-api-key', { method: 'PATCH', body: JSON.stringify({ index, value: newConfig }) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadGeminiKeys(); this.currentGeminiEditConfig = null; this.showNotification(i18n.t('notification.gemini_key_updated'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); } } export async function deleteGeminiKey(apiKey) { if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return; try { await this.makeRequest(`/gemini-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); this.clearCache(); // 清除缓存 this.loadGeminiKeys(); this.showNotification(i18n.t('notification.gemini_key_deleted'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } } // Codex providers export async function loadCodexKeys() { try { const config = await this.getConfig(); const keys = Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : []; await this.renderCodexKeys(keys); } catch (error) { console.error('加载Codex密钥失败:', error); } } export async function renderCodexKeys(keys, keyStats = null) { const container = document.getElementById('codex-keys-list'); if (!container) { return; } const list = Array.isArray(keys) ? keys : []; if (list.length === 0) { container.innerHTML = `

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

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

`; return; } // 使用传入的keyStats,如果没有则获取一次 if (!keyStats) { keyStats = await this.getKeyStats(); } const statsBySource = getStatsBySource(keyStats); container.innerHTML = list.map((config, index) => { const rawKey = config['api-key'] || ''; const masked = this.maskApiKey(rawKey || ''); const maskedDisplay = this.escapeHtml(masked); const usageStats = (rawKey && (statsBySource[rawKey] || statsBySource[masked])) || { success: 0, failure: 0 }; const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"'); return `
${i18n.t('ai_providers.codex_item_title')} #${index + 1}
${i18n.t('common.api_key')}: ${maskedDisplay}
${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''} ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''} ${this.renderHeaderBadges(config.headers)}
${i18n.t('stats.success')}: ${usageStats.success} ${i18n.t('stats.failure')}: ${usageStats.failure}
`; }).join(''); } export function showAddCodexKeyModal() { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `

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

${i18n.t('common.custom_headers_hint')}

`; modal.style.display = 'block'; this.populateHeaderFields('new-codex-headers-wrapper'); } export async function addCodexKey() { const apiKey = document.getElementById('new-codex-key').value.trim(); 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'); if (!apiKey) { this.showNotification(i18n.t('notification.field_required'), 'error'); return; } if (!baseUrl) { this.showNotification(i18n.t('notification.codex_base_url_required'), 'error'); return; } try { 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); currentKeys.push(newConfig); await this.makeRequest('/codex-api-key', { method: 'PUT', body: JSON.stringify(currentKeys) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadCodexKeys(); this.showNotification(i18n.t('notification.codex_config_added'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error'); } } export function editCodexKey(index, config) { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `

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

${i18n.t('common.custom_headers_hint')}

`; modal.style.display = 'block'; this.populateHeaderFields('edit-codex-headers-wrapper', config.headers || null); } export async function updateCodexKey(index) { const apiKey = document.getElementById('edit-codex-key').value.trim(); 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'); if (!apiKey) { this.showNotification(i18n.t('notification.field_required'), 'error'); return; } if (!baseUrl) { this.showNotification(i18n.t('notification.codex_base_url_required'), 'error'); return; } try { const listResponse = await this.makeRequest('/codex-api-key'); const currentList = this.normalizeArrayResponse(listResponse, 'codex-api-key'); if (!Array.isArray(currentList) || index < 0 || index >= currentList.length) { throw new Error('Invalid codex configuration index'); } const original = currentList[index] ? { ...currentList[index] } : {}; const newConfig = this.buildCodexConfig(apiKey, baseUrl, proxyUrl, original, headers); await this.makeRequest('/codex-api-key', { method: 'PATCH', body: JSON.stringify({ index, value: newConfig }) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadCodexKeys(); this.showNotification(i18n.t('notification.codex_config_updated'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); } } export async function deleteCodexKey(apiKey) { if (!confirm(i18n.t('ai_providers.codex_delete_confirm'))) return; try { await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); this.clearCache(); // 清除缓存 this.loadCodexKeys(); this.showNotification(i18n.t('notification.codex_config_deleted'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } } // Claude providers export async function loadClaudeKeys() { try { const config = await this.getConfig(); const keys = Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : []; await this.renderClaudeKeys(keys); } catch (error) { console.error('加载Claude密钥失败:', error); } } export async function renderClaudeKeys(keys, keyStats = null) { const container = document.getElementById('claude-keys-list'); if (!container) { return; } const list = Array.isArray(keys) ? keys : []; if (list.length === 0) { container.innerHTML = `

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

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

`; return; } // 使用传入的keyStats,如果没有则获取一次 if (!keyStats) { keyStats = await this.getKeyStats(); } const statsBySource = getStatsBySource(keyStats); container.innerHTML = list.map((config, index) => { const rawKey = config['api-key'] || ''; const masked = this.maskApiKey(rawKey || ''); const maskedDisplay = this.escapeHtml(masked); const usageStats = (rawKey && (statsBySource[rawKey] || statsBySource[masked])) || { success: 0, failure: 0 }; const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"'); const models = Array.isArray(config.models) ? config.models : []; const modelsCountHtml = models.length ? `
${i18n.t('ai_providers.claude_models_count')}: ${models.length}
` : ''; const modelsBadgesHtml = this.renderModelBadges(models); return `
${i18n.t('ai_providers.claude_item_title')} #${index + 1}
${i18n.t('common.api_key')}: ${maskedDisplay}
${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''} ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''} ${this.renderHeaderBadges(config.headers)} ${modelsCountHtml} ${modelsBadgesHtml}
${i18n.t('stats.success')}: ${usageStats.success} ${i18n.t('stats.failure')}: ${usageStats.failure}
`; }).join(''); } export function showAddClaudeKeyModal() { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `

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

${i18n.t('common.custom_headers_hint')}

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

`; modal.style.display = 'block'; this.populateHeaderFields('new-claude-headers-wrapper'); this.populateModelFields('new-claude-models-wrapper'); } export async function addClaudeKey() { const apiKey = document.getElementById('new-claude-key').value.trim(); const baseUrl = document.getElementById('new-claude-url').value.trim(); const proxyUrl = document.getElementById('new-claude-proxy').value.trim(); const headers = this.collectHeaderInputs('new-claude-headers-wrapper'); const models = this.collectModelInputs('new-claude-models-wrapper'); if (!apiKey) { this.showNotification(i18n.t('notification.field_required'), 'error'); return; } try { const data = await this.makeRequest('/claude-api-key'); const currentKeys = data['claude-api-key'] || []; const newConfig = { 'api-key': apiKey }; if (baseUrl) { newConfig['base-url'] = baseUrl; } if (proxyUrl) { newConfig['proxy-url'] = proxyUrl; } this.applyHeadersToConfig(newConfig, headers); if (models.length) { newConfig.models = models; } currentKeys.push(newConfig); await this.makeRequest('/claude-api-key', { method: 'PUT', body: JSON.stringify(currentKeys) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadClaudeKeys(); this.showNotification(i18n.t('notification.claude_config_added'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error'); } } export function editClaudeKey(index, config) { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `

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

${i18n.t('common.custom_headers_hint')}

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

`; modal.style.display = 'block'; this.populateHeaderFields('edit-claude-headers-wrapper', config.headers || null); this.populateModelFields('edit-claude-models-wrapper', Array.isArray(config.models) ? config.models : []); } export async function updateClaudeKey(index) { const apiKey = document.getElementById('edit-claude-key').value.trim(); const baseUrl = document.getElementById('edit-claude-url').value.trim(); const proxyUrl = document.getElementById('edit-claude-proxy').value.trim(); const headers = this.collectHeaderInputs('edit-claude-headers-wrapper'); const models = this.collectModelInputs('edit-claude-models-wrapper'); if (!apiKey) { this.showNotification(i18n.t('notification.field_required'), 'error'); return; } try { const newConfig = { 'api-key': apiKey }; if (baseUrl) { newConfig['base-url'] = baseUrl; } if (proxyUrl) { newConfig['proxy-url'] = proxyUrl; } this.applyHeadersToConfig(newConfig, headers); if (models.length) { newConfig.models = models; } await this.makeRequest('/claude-api-key', { method: 'PATCH', body: JSON.stringify({ index, value: newConfig }) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadClaudeKeys(); this.showNotification(i18n.t('notification.claude_config_updated'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); } } export async function deleteClaudeKey(apiKey) { if (!confirm(i18n.t('ai_providers.claude_delete_confirm'))) return; try { await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); this.clearCache(); // 清除缓存 this.loadClaudeKeys(); this.showNotification(i18n.t('notification.claude_config_deleted'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } } // OpenAI compatible providers export async function loadOpenAIProviders() { try { const config = await this.getConfig(); const providers = Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : []; await this.renderOpenAIProviders(providers); } catch (error) { console.error('加载OpenAI提供商失败:', error); } } export async function renderOpenAIProviders(providers, keyStats = null) { const container = document.getElementById('openai-providers-list'); if (!container) { return; } const list = Array.isArray(providers) ? providers : []; if (list.length === 0) { container.innerHTML = `

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

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

`; // 重置样式 container.style.maxHeight = ''; container.style.overflowY = ''; return; } // 根据提供商数量设置滚动条 if (list.length > 5) { container.style.maxHeight = '400px'; container.style.overflowY = 'auto'; } else { container.style.maxHeight = ''; container.style.overflowY = ''; } // 使用传入的keyStats,如果没有则获取一次 if (!keyStats) { keyStats = await this.getKeyStats(); } const statsBySource = getStatsBySource(keyStats); container.innerHTML = list.map((provider, index) => { const item = typeof provider === 'object' && provider !== null ? provider : {}; // 处理两种API密钥格式:新的 api-key-entries 和旧的 api-keys let apiKeyEntries = []; if (Array.isArray(item['api-key-entries'])) { // 新格式:{api-key: "...", proxy-url: "..."} apiKeyEntries = item['api-key-entries']; } else if (Array.isArray(item['api-keys'])) { // 旧格式:简单的字符串数组 apiKeyEntries = item['api-keys'].map(key => ({ 'api-key': key })); } const models = Array.isArray(item.models) ? item.models : []; const name = item.name || ''; const baseUrl = item['base-url'] || ''; let totalSuccess = 0; let totalFailure = 0; apiKeyEntries.forEach(entry => { const key = entry && entry['api-key'] ? entry['api-key'] : ''; if (!key) return; const masked = this.maskApiKey(key); const usageStats = statsBySource[key] || statsBySource[masked] || { success: 0, failure: 0 }; totalSuccess += usageStats.success; totalFailure += usageStats.failure; }); const deleteArg = JSON.stringify(name).replace(/"/g, '"'); return `
${this.escapeHtml(name)}
${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}
${this.renderHeaderBadges(item.headers)}
${i18n.t('ai_providers.openai_keys_count')}: ${apiKeyEntries.length}
${i18n.t('ai_providers.openai_models_count')}: ${models.length}
${this.renderModelBadges(models)}
${i18n.t('stats.success')}: ${totalSuccess} ${i18n.t('stats.failure')}: ${totalFailure}
`; }).join(''); } const getOpenAIContext = (mode = 'new') => { const isEdit = mode === 'edit'; return { mode: isEdit ? 'edit' : 'new', baseUrlInputId: isEdit ? 'edit-provider-url' : 'new-provider-url', apiKeyWrapperId: isEdit ? 'edit-openai-keys-wrapper' : 'new-openai-keys-wrapper', headerWrapperId: isEdit ? 'edit-openai-headers-wrapper' : 'new-openai-headers-wrapper', modelWrapperId: isEdit ? 'edit-provider-models-wrapper' : 'new-provider-models-wrapper' }; }; function ensureOpenAIModelDiscoveryCard(manager) { let overlay = document.getElementById('openai-model-discovery'); if (overlay) { return overlay; } overlay = document.createElement('div'); overlay.id = 'openai-model-discovery'; overlay.className = 'model-discovery-overlay'; overlay.innerHTML = `

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

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

`; document.body.appendChild(overlay); const bind = (id, handler) => { const el = document.getElementById(id); if (el) { el.addEventListener('click', handler); } }; bind('openai-model-discovery-back', () => manager.closeOpenAIModelDiscovery()); bind('openai-model-discovery-cancel', () => manager.closeOpenAIModelDiscovery()); bind('openai-model-discovery-refresh', () => manager.refreshOpenAIModelDiscovery()); bind('openai-model-discovery-apply', () => manager.applyOpenAIModelDiscoverySelection()); return overlay; } export function setOpenAIModelDiscoveryStatus(message = '', type = 'info') { const status = document.getElementById('openai-model-discovery-status'); if (!status) return; status.textContent = message; status.className = `model-discovery-status ${type}`; } export function renderOpenAIModelDiscoveryList(models = []) { const list = document.getElementById('openai-model-discovery-list'); if (!list) return; if (!models.length) { list.innerHTML = `
${i18n.t('ai_providers.openai_models_fetch_empty')}
`; return; } list.innerHTML = models.map((model, index) => { const name = this.escapeHtml(model.name || ''); const alias = model.alias ? `${this.escapeHtml(model.alias)}` : ''; const desc = model.description ? `
${this.escapeHtml(model.description)}
` : ''; return ` `; }).join(''); } export function openOpenAIModelDiscovery(mode = 'new') { const context = getOpenAIContext(mode); const baseInput = document.getElementById(context.baseUrlInputId); const baseUrl = baseInput ? baseInput.value.trim() : ''; if (!baseUrl) { this.showNotification(i18n.t('ai_providers.openai_models_fetch_invalid_url'), 'error'); return; } const endpoint = buildModelEndpoint(baseUrl); if (!endpoint) { this.showNotification(i18n.t('ai_providers.openai_models_fetch_invalid_url'), 'error'); return; } const apiKeyEntries = this.collectApiKeyEntryInputs(context.apiKeyWrapperId); const firstKey = Array.isArray(apiKeyEntries) ? apiKeyEntries.find(entry => entry && entry['api-key']) : null; const headers = this.collectHeaderInputs(context.headerWrapperId) || {}; if (firstKey && !headers.Authorization && !headers.authorization) { headers.Authorization = `Bearer ${firstKey['api-key']}`; } ensureOpenAIModelDiscoveryCard(this).classList.add('active'); this.openAIModelDiscoveryContext = { ...context, endpoint, headers, discoveredModels: [] }; const urlInput = document.getElementById('openai-model-discovery-url'); if (urlInput) { urlInput.value = endpoint; } this.renderOpenAIModelDiscoveryList([]); this.setOpenAIModelDiscoveryStatus(i18n.t('ai_providers.openai_models_fetch_loading'), 'info'); this.refreshOpenAIModelDiscovery(); } export async function refreshOpenAIModelDiscovery() { const context = this.openAIModelDiscoveryContext; if (!context || !context.endpoint) { return; } this.setOpenAIModelDiscoveryStatus(i18n.t('ai_providers.openai_models_fetch_loading'), 'info'); const list = document.getElementById('openai-model-discovery-list'); if (list) { list.innerHTML = '
'; } try { const response = await fetch(context.endpoint, { headers: context.headers || {} }); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } let data; try { data = await response.json(); } catch (err) { const text = await response.text(); throw new Error(text || err.message || 'Invalid JSON'); } const models = normalizeModelList(data); context.discoveredModels = models; this.renderOpenAIModelDiscoveryList(models); if (!models.length) { this.setOpenAIModelDiscoveryStatus(i18n.t('ai_providers.openai_models_fetch_empty'), 'warning'); } else { this.setOpenAIModelDiscoveryStatus('', 'info'); } } catch (error) { context.discoveredModels = []; this.renderOpenAIModelDiscoveryList([]); this.setOpenAIModelDiscoveryStatus(`${i18n.t('ai_providers.openai_models_fetch_error')}: ${error.message}`, 'error'); } } export function applyOpenAIModelDiscoverySelection() { const context = this.openAIModelDiscoveryContext; if (!context || !Array.isArray(context.discoveredModels) || !context.discoveredModels.length) { this.closeOpenAIModelDiscovery(); return; } const list = document.getElementById('openai-model-discovery-list'); if (!list) { this.closeOpenAIModelDiscovery(); return; } const selectedIndices = Array.from(list.querySelectorAll('.model-discovery-checkbox:checked')) .map(input => Number.parseInt(input.getAttribute('data-model-index') || '-1', 10)) .filter(index => Number.isFinite(index) && index >= 0 && index < context.discoveredModels.length); const selectedModels = selectedIndices.map(index => context.discoveredModels[index]); if (!selectedModels.length) { this.closeOpenAIModelDiscovery(); return; } const existing = this.collectModelInputs(context.modelWrapperId); const mergedMap = new Map(); existing.forEach(model => { if (model && model.name) { mergedMap.set(model.name, { ...model }); } }); let addedCount = 0; selectedModels.forEach(model => { const name = model && model.name; if (!name) return; if (!mergedMap.has(name)) { mergedMap.set(name, { name, ...(model.alias ? { alias: model.alias } : {}) }); addedCount++; } }); this.populateModelFields(context.modelWrapperId, Array.from(mergedMap.values())); this.closeOpenAIModelDiscovery(); if (addedCount > 0) { const template = i18n.t('ai_providers.openai_models_fetch_added'); const message = template.replace('{count}', addedCount); this.showNotification(message, 'success'); } } export function closeOpenAIModelDiscovery() { const overlay = document.getElementById('openai-model-discovery'); if (overlay) { overlay.classList.remove('active'); } this.openAIModelDiscoveryContext = null; } export function showAddOpenAIProviderModal() { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `

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

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

${i18n.t('common.custom_headers_hint')}

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

`; modal.style.display = 'block'; this.populateModelFields('new-provider-models-wrapper', []); this.populateHeaderFields('new-openai-headers-wrapper'); this.populateApiKeyEntryFields('new-openai-keys-wrapper'); } export async function addOpenAIProvider() { const name = document.getElementById('new-provider-name').value.trim(); const baseUrl = document.getElementById('new-provider-url').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'); if (!this.validateOpenAIProviderInput(name, baseUrl, models)) { return; } try { const data = await this.makeRequest('/openai-compatibility'); const currentProviders = data['openai-compatibility'] || []; const newProvider = { name, 'base-url': baseUrl, 'api-key-entries': apiKeyEntries, models }; this.applyHeadersToConfig(newProvider, headers); currentProviders.push(newProvider); await this.makeRequest('/openai-compatibility', { method: 'PUT', body: JSON.stringify(currentProviders) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadOpenAIProviders(); this.showNotification(i18n.t('notification.openai_provider_added'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error'); } } export function editOpenAIProvider(index, provider) { const modal = document.getElementById('modal'); const modalBody = document.getElementById('modal-body'); // 处理两种API密钥格式:新的 api-key-entries 和旧的 api-keys let apiKeyEntries = []; if (Array.isArray(provider?.['api-key-entries'])) { // 新格式:{api-key: "...", proxy-url: "..."} apiKeyEntries = provider['api-key-entries']; } else if (Array.isArray(provider?.['api-keys'])) { // 旧格式:简单的字符串数组 apiKeyEntries = provider['api-keys'].map(key => ({ 'api-key': key, 'proxy-url': '' })); } const models = Array.isArray(provider?.models) ? provider.models : []; modalBody.innerHTML = `

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

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

${i18n.t('common.custom_headers_hint')}

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

`; 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); } export async function updateOpenAIProvider(index) { const name = document.getElementById('edit-provider-name').value.trim(); const baseUrl = document.getElementById('edit-provider-url').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'); if (!this.validateOpenAIProviderInput(name, baseUrl, models)) { return; } try { const updatedProvider = { name, 'base-url': baseUrl, 'api-key-entries': apiKeyEntries, models }; this.applyHeadersToConfig(updatedProvider, headers); await this.makeRequest('/openai-compatibility', { method: 'PATCH', body: JSON.stringify({ index, value: updatedProvider }) }); this.clearCache(); // 清除缓存 this.closeModal(); this.loadOpenAIProviders(); this.showNotification(i18n.t('notification.openai_provider_updated'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); } } export async function deleteOpenAIProvider(name) { if (!confirm(i18n.t('ai_providers.openai_delete_confirm'))) return; try { await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' }); this.clearCache(); // 清除缓存 this.loadOpenAIProviders(); this.showNotification(i18n.t('notification.openai_provider_deleted'), 'success'); } catch (error) { this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } } export function addModelField(wrapperId, model = {}) { const wrapper = document.getElementById(wrapperId); if (!wrapper) return; const row = document.createElement('div'); row.className = 'model-input-row'; row.innerHTML = `
`; const removeBtn = row.querySelector('.model-remove-btn'); if (removeBtn) { removeBtn.addEventListener('click', () => { wrapper.removeChild(row); }); } wrapper.appendChild(row); } export function populateModelFields(wrapperId, models = []) { const wrapper = document.getElementById(wrapperId); if (!wrapper) return; wrapper.innerHTML = ''; if (!models.length) { this.addModelField(wrapperId); return; } models.forEach(model => this.addModelField(wrapperId, model)); } export function collectModelInputs(wrapperId) { const wrapper = document.getElementById(wrapperId); if (!wrapper) return []; const rows = Array.from(wrapper.querySelectorAll('.model-input-row')); const models = []; rows.forEach(row => { const nameInput = row.querySelector('.model-name-input'); const aliasInput = row.querySelector('.model-alias-input'); const name = nameInput ? nameInput.value.trim() : ''; const alias = aliasInput ? aliasInput.value.trim() : ''; if (name) { const model = { name }; if (alias) { model.alias = alias; } models.push(model); } }); return models; } export function renderModelBadges(models) { if (!models || models.length === 0) { return ''; } return `
${models.map(model => ` ${this.escapeHtml(model.name || '')} ${model.alias ? `${this.escapeHtml(model.alias)}` : ''} `).join('')}
`; } export function validateOpenAIProviderInput(name, baseUrl, models) { if (!name || !baseUrl) { this.showNotification(i18n.t('notification.openai_provider_required'), 'error'); return false; } const invalidModel = models.find(model => !model.name); if (invalidModel) { this.showNotification(i18n.t('notification.openai_model_name_required'), 'error'); return false; } return true; } export const aiProvidersModule = { loadGeminiKeys, getGeminiKeysFromConfig, renderGeminiKeys, showAddGeminiKeyModal, addGeminiKey, addGeminiKeyField, populateGeminiKeyFields, collectGeminiKeyFieldInputs, editGeminiKey, updateGeminiKey, deleteGeminiKey, loadCodexKeys, renderCodexKeys, showAddCodexKeyModal, addCodexKey, editCodexKey, updateCodexKey, deleteCodexKey, loadClaudeKeys, renderClaudeKeys, showAddClaudeKeyModal, addClaudeKey, editClaudeKey, updateClaudeKey, deleteClaudeKey, loadOpenAIProviders, renderOpenAIProviders, showAddOpenAIProviderModal, addOpenAIProvider, editOpenAIProvider, updateOpenAIProvider, deleteOpenAIProvider, openOpenAIModelDiscovery, refreshOpenAIModelDiscovery, renderOpenAIModelDiscoveryList, setOpenAIModelDiscoveryStatus, applyOpenAIModelDiscoverySelection, closeOpenAIModelDiscovery, addModelField, populateModelFields, collectModelInputs, renderModelBadges, validateOpenAIProviderInput };