mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
1610 lines
67 KiB
JavaScript
1610 lines
67 KiB
JavaScript
// 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 = `
|
||
<div class="empty-state">
|
||
<i class="fab fa-google"></i>
|
||
<h3>${i18n.t('ai_providers.gemini_empty_title')}</h3>
|
||
<p>${i18n.t('ai_providers.gemini_empty_desc')}</p>
|
||
</div>
|
||
`;
|
||
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 `
|
||
<div class="key-item">
|
||
<div class="item-content">
|
||
<div class="item-title">${i18n.t('ai_providers.gemini_item_title')} #${index + 1}</div>
|
||
<div class="item-subtitle">${i18n.t('common.api_key')}: ${maskedDisplay}</div>
|
||
${baseUrl ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}</div>` : ''}
|
||
${this.renderHeaderBadges(config.headers)}
|
||
<div class="item-stats">
|
||
<span class="stat-badge stat-success">
|
||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${usageStats.success}
|
||
</span>
|
||
<span class="stat-badge stat-failure">
|
||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${usageStats.failure}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<button class="btn btn-secondary" onclick="manager.editGeminiKey(${index}, ${configJson})">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-danger" onclick="manager.deleteGeminiKey(${apiKeyJson})">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
export function showAddGeminiKeyModal() {
|
||
const modal = document.getElementById('modal');
|
||
const modalBody = document.getElementById('modal-body');
|
||
|
||
modalBody.innerHTML = `
|
||
<h3>${i18n.t('ai_providers.gemini_add_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<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 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>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="new-gemini-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-gemini-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.addGeminiKey()">${i18n.t('common.add')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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
|
||
? `<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);
|
||
}
|
||
|
||
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 = `
|
||
<h3>${i18n.t('ai_providers.gemini_edit_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<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>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="edit-gemini-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-gemini-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.updateGeminiKey(${index})">${i18n.t('common.update')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="empty-state">
|
||
<i class="fas fa-code"></i>
|
||
<h3>${i18n.t('ai_providers.codex_empty_title')}</h3>
|
||
<p>${i18n.t('ai_providers.codex_empty_desc')}</p>
|
||
</div>
|
||
`;
|
||
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 `
|
||
<div class="provider-item">
|
||
<div class="item-content">
|
||
<div class="item-title">${i18n.t('ai_providers.codex_item_title')} #${index + 1}</div>
|
||
<div class="item-subtitle">${i18n.t('common.api_key')}: ${maskedDisplay}</div>
|
||
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
|
||
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
|
||
${this.renderHeaderBadges(config.headers)}
|
||
<div class="item-stats">
|
||
<span class="stat-badge stat-success">
|
||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${usageStats.success}
|
||
</span>
|
||
<span class="stat-badge stat-failure">
|
||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${usageStats.failure}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<button class="btn btn-secondary" onclick="manager.editCodexKey(${index}, ${JSON.stringify(config).replace(/\"/g, '"')})">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-danger" onclick="manager.deleteCodexKey(${deleteArg})">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
export function showAddCodexKeyModal() {
|
||
const modal = document.getElementById('modal');
|
||
const modalBody = document.getElementById('modal-body');
|
||
|
||
modalBody.innerHTML = `
|
||
<h3>${i18n.t('ai_providers.codex_add_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<label for="new-codex-key">${i18n.t('ai_providers.codex_add_modal_key_label')}</label>
|
||
<input type="text" id="new-codex-key" placeholder="${i18n.t('ai_providers.codex_add_modal_key_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-codex-url">${i18n.t('ai_providers.codex_add_modal_url_label')}</label>
|
||
<input type="text" id="new-codex-url" placeholder="${i18n.t('ai_providers.codex_add_modal_url_placeholder')}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-codex-proxy">${i18n.t('ai_providers.codex_add_modal_proxy_label')}</label>
|
||
<input type="text" id="new-codex-proxy" placeholder="${i18n.t('ai_providers.codex_add_modal_proxy_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('common.custom_headers_label')}</label>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="new-codex-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-codex-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.addCodexKey()">${i18n.t('common.add')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<h3>${i18n.t('ai_providers.codex_edit_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<label for="edit-codex-key">${i18n.t('ai_providers.codex_edit_modal_key_label')}</label>
|
||
<input type="text" id="edit-codex-key" value="${config['api-key']}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-codex-url">${i18n.t('ai_providers.codex_edit_modal_url_label')}</label>
|
||
<input type="text" id="edit-codex-url" value="${config['base-url'] || ''}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-codex-proxy">${i18n.t('ai_providers.codex_edit_modal_proxy_label')}</label>
|
||
<input type="text" id="edit-codex-proxy" value="${config['proxy-url'] || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('common.custom_headers_label')}</label>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="edit-codex-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-codex-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">${i18n.t('common.update')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="empty-state">
|
||
<i class="fas fa-brain"></i>
|
||
<h3>${i18n.t('ai_providers.claude_empty_title')}</h3>
|
||
<p>${i18n.t('ai_providers.claude_empty_desc')}</p>
|
||
</div>
|
||
`;
|
||
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
|
||
? `<div class="item-subtitle">${i18n.t('ai_providers.claude_models_count')}: ${models.length}</div>`
|
||
: '';
|
||
const modelsBadgesHtml = this.renderModelBadges(models);
|
||
return `
|
||
<div class="provider-item">
|
||
<div class="item-content">
|
||
<div class="item-title">${i18n.t('ai_providers.claude_item_title')} #${index + 1}</div>
|
||
<div class="item-subtitle">${i18n.t('common.api_key')}: ${maskedDisplay}</div>
|
||
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
|
||
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
|
||
${this.renderHeaderBadges(config.headers)}
|
||
${modelsCountHtml}
|
||
${modelsBadgesHtml}
|
||
<div class="item-stats">
|
||
<span class="stat-badge stat-success">
|
||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${usageStats.success}
|
||
</span>
|
||
<span class="stat-badge stat-failure">
|
||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${usageStats.failure}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<button class="btn btn-secondary" onclick="manager.editClaudeKey(${index}, ${JSON.stringify(config).replace(/\"/g, '"')})">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-danger" onclick="manager.deleteClaudeKey(${deleteArg})">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
export function showAddClaudeKeyModal() {
|
||
const modal = document.getElementById('modal');
|
||
const modalBody = document.getElementById('modal-body');
|
||
|
||
modalBody.innerHTML = `
|
||
<h3>${i18n.t('ai_providers.claude_add_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<label for="new-claude-key">${i18n.t('ai_providers.claude_add_modal_key_label')}</label>
|
||
<input type="text" id="new-claude-key" placeholder="${i18n.t('ai_providers.claude_add_modal_key_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-claude-url">${i18n.t('ai_providers.claude_add_modal_url_label')}</label>
|
||
<input type="text" id="new-claude-url" placeholder="${i18n.t('ai_providers.claude_add_modal_url_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-claude-proxy">${i18n.t('ai_providers.claude_add_modal_proxy_label')}</label>
|
||
<input type="text" id="new-claude-proxy" placeholder="${i18n.t('ai_providers.claude_add_modal_proxy_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('common.custom_headers_label')}</label>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="new-claude-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-claude-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('ai_providers.claude_models_label')}</label>
|
||
<p class="form-hint">${i18n.t('ai_providers.claude_models_hint')}</p>
|
||
<div id="new-claude-models-wrapper" class="model-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('new-claude-models-wrapper')">${i18n.t('ai_providers.claude_models_add_btn')}</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.addClaudeKey()">${i18n.t('common.add')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<h3>${i18n.t('ai_providers.claude_edit_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<label for="edit-claude-key">${i18n.t('ai_providers.claude_edit_modal_key_label')}</label>
|
||
<input type="text" id="edit-claude-key" value="${config['api-key']}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-claude-url">${i18n.t('ai_providers.claude_edit_modal_url_label')}</label>
|
||
<input type="text" id="edit-claude-url" value="${config['base-url'] || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-claude-proxy">${i18n.t('ai_providers.claude_edit_modal_proxy_label')}</label>
|
||
<input type="text" id="edit-claude-proxy" value="${config['proxy-url'] || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('common.custom_headers_label')}</label>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="edit-claude-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-claude-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('ai_providers.claude_models_label')}</label>
|
||
<p class="form-hint">${i18n.t('ai_providers.claude_models_hint')}</p>
|
||
<div id="edit-claude-models-wrapper" class="model-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('edit-claude-models-wrapper')">${i18n.t('ai_providers.claude_models_add_btn')}</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">${i18n.t('common.update')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="empty-state">
|
||
<i class="fas fa-plug"></i>
|
||
<h3>${i18n.t('ai_providers.openai_empty_title')}</h3>
|
||
<p>${i18n.t('ai_providers.openai_empty_desc')}</p>
|
||
</div>
|
||
`;
|
||
// 重置样式
|
||
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 `
|
||
<div class="provider-item">
|
||
<div class="item-content">
|
||
<div class="item-title">${this.escapeHtml(name)}</div>
|
||
<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(baseUrl)}</div>
|
||
${this.renderHeaderBadges(item.headers)}
|
||
<div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${apiKeyEntries.length}</div>
|
||
<div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${models.length}</div>
|
||
${this.renderModelBadges(models)}
|
||
<div class="item-stats">
|
||
<span class="stat-badge stat-success">
|
||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${totalSuccess}
|
||
</span>
|
||
<span class="stat-badge stat-failure">
|
||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${totalFailure}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<button class="btn btn-secondary" onclick="manager.editOpenAIProvider(${index}, ${JSON.stringify(item).replace(/\"/g, '"')})">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-danger" onclick="manager.deleteOpenAIProvider(${deleteArg})">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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 = `
|
||
<div class="model-discovery-card">
|
||
<div class="model-discovery-header">
|
||
<div class="model-discovery-title">
|
||
<h3>${i18n.t('ai_providers.openai_models_fetch_title')}</h3>
|
||
<p class="form-hint">${i18n.t('ai_providers.openai_models_fetch_hint')}</p>
|
||
</div>
|
||
<button type="button" class="btn btn-secondary" id="openai-model-discovery-back">${i18n.t('ai_providers.openai_models_fetch_back')}</button>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('ai_providers.openai_models_fetch_url_label')}</label>
|
||
<div class="input-group">
|
||
<input type="text" id="openai-model-discovery-url" readonly>
|
||
<button type="button" class="btn btn-secondary" id="openai-model-discovery-refresh">${i18n.t('ai_providers.openai_models_fetch_refresh')}</button>
|
||
</div>
|
||
</div>
|
||
<div id="openai-model-discovery-status" class="model-discovery-status"></div>
|
||
<div id="openai-model-discovery-list" class="model-discovery-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="openai-model-discovery-cancel">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" id="openai-model-discovery-apply">${i18n.t('ai_providers.openai_models_fetch_apply')}</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="model-discovery-empty">
|
||
<i class="fas fa-box-open"></i>
|
||
<span>${i18n.t('ai_providers.openai_models_fetch_empty')}</span>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = models.map((model, index) => {
|
||
const name = this.escapeHtml(model.name || '');
|
||
const alias = model.alias ? `<span class="model-discovery-alias">${this.escapeHtml(model.alias)}</span>` : '';
|
||
const desc = model.description ? `<div class="model-discovery-desc">${this.escapeHtml(model.description)}</div>` : '';
|
||
return `
|
||
<label class="model-discovery-row">
|
||
<input type="checkbox" class="model-discovery-checkbox" data-model-index="${index}" checked>
|
||
<div class="model-discovery-meta">
|
||
<div class="model-discovery-name">${name} ${alias}</div>
|
||
${desc}
|
||
</div>
|
||
</label>
|
||
`;
|
||
}).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 = '<div class="model-discovery-empty"><i class="fas fa-spinner fa-spin"></i></div>';
|
||
}
|
||
|
||
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 = `
|
||
<h3>${i18n.t('ai_providers.openai_add_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<label for="new-provider-name">${i18n.t('ai_providers.openai_add_modal_name_label')}</label>
|
||
<input type="text" id="new-provider-name" placeholder="${i18n.t('ai_providers.openai_add_modal_name_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="new-provider-url">${i18n.t('ai_providers.openai_add_modal_url_label')}</label>
|
||
<input type="text" id="new-provider-url" placeholder="${i18n.t('ai_providers.openai_add_modal_url_placeholder')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<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>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="new-openai-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('new-openai-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('ai_providers.openai_add_modal_models_label')}</label>
|
||
<p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p>
|
||
<div id="new-provider-models-wrapper" class="model-input-list"></div>
|
||
<div class="model-actions-inline">
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('new-provider-models-wrapper')">${i18n.t('ai_providers.openai_models_add_btn')}</button>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.openOpenAIModelDiscovery('new')">
|
||
<i class="fas fa-download"></i> ${i18n.t('ai_providers.openai_models_fetch_button')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.addOpenAIProvider()">${i18n.t('common.add')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<h3>${i18n.t('ai_providers.openai_edit_modal_title')}</h3>
|
||
<div class="form-group">
|
||
<label for="edit-provider-name">${i18n.t('ai_providers.openai_edit_modal_name_label')}</label>
|
||
<input type="text" id="edit-provider-name" value="${provider?.name ? this.escapeHtml(provider.name) : ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-provider-url">${i18n.t('ai_providers.openai_edit_modal_url_label')}</label>
|
||
<input type="text" id="edit-provider-url" value="${provider?.['base-url'] ? this.escapeHtml(provider['base-url']) : ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<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>
|
||
<p class="form-hint">${i18n.t('common.custom_headers_hint')}</p>
|
||
<div id="edit-openai-headers-wrapper" class="header-input-list"></div>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addHeaderField('edit-openai-headers-wrapper')">${i18n.t('common.custom_headers_add')}</button>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>${i18n.t('ai_providers.openai_edit_modal_models_label')}</label>
|
||
<p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p>
|
||
<div id="edit-provider-models-wrapper" class="model-input-list"></div>
|
||
<div class="model-actions-inline">
|
||
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('edit-provider-models-wrapper')">${i18n.t('ai_providers.openai_models_add_btn')}</button>
|
||
<button type="button" class="btn btn-secondary" onclick="manager.openOpenAIModelDiscovery('edit')">
|
||
<i class="fas fa-download"></i> ${i18n.t('ai_providers.openai_models_fetch_button')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||
<button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">${i18n.t('common.update')}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="input-group">
|
||
<input type="text" class="model-name-input" placeholder="${i18n.t('common.model_name_placeholder')}" value="${model.name ? this.escapeHtml(model.name) : ''}">
|
||
<input type="text" class="model-alias-input" placeholder="${i18n.t('common.model_alias_placeholder')}" value="${model.alias ? this.escapeHtml(model.alias) : ''}">
|
||
<button type="button" class="btn btn-small btn-danger model-remove-btn"><i class="fas fa-trash"></i></button>
|
||
</div>
|
||
`;
|
||
|
||
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 `
|
||
<div class="provider-models">
|
||
${models.map(model => `
|
||
<span class="provider-model-tag">
|
||
<span class="model-name">${this.escapeHtml(model.name || '')}</span>
|
||
${model.alias ? `<span class="model-alias">${this.escapeHtml(model.alias)}</span>` : ''}
|
||
</span>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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
|
||
};
|