// 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 '';
const trimmed = String(baseUrl).trim().replace(/\/+$/g, '');
if (!trimmed) return '';
// 如果 base 已以 /v1 结尾,直接拼 /models;否则拼 /v1/models,避免丢失中间路径
if (trimmed.endsWith('/v1')) {
return `${trimmed}/models`;
}
return `${trimmed}/v1/models`;
};
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')}
`;
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')}
`;
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')}
`;
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')}
`;
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')}
`;
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')}
`;
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 = `
`;
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')}
`;
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')}
`;
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
};