import { Fragment, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; import { EmptyState } from '@/components/ui/EmptyState'; import { HeaderInputList } from '@/components/ui/HeaderInputList'; import { ModelInputList, modelsToEntries, entriesToModels } from '@/components/ui/ModelInputList'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconCheck, IconX } from '@/components/ui/icons'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { ampcodeApi, modelsApi, providersApi, usageApi } from '@/services/api'; import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig, ApiKeyEntry, AmpcodeConfig, AmpcodeModelMapping, } from '@/types'; import type { KeyStats, KeyStatBucket } from '@/utils/usage'; import type { ModelInfo } from '@/utils/models'; import { headersToEntries, buildHeaderObject, type HeaderEntry } from '@/utils/headers'; import { maskApiKey } from '@/utils/format'; import styles from './AiProvidersPage.module.scss'; type ProviderModal = | { type: 'gemini'; index: number | null } | { type: 'codex'; index: number | null } | { type: 'claude'; index: number | null } | { type: 'ampcode'; index: null } | { type: 'openai'; index: number | null }; interface ModelEntry { name: string; alias: string; } interface OpenAIFormState { name: string; baseUrl: string; headers: HeaderEntry[]; testModel?: string; modelEntries: ModelEntry[]; apiKeyEntries: ApiKeyEntry[]; } interface AmpcodeFormState { upstreamUrl: string; upstreamApiKey: string; restrictManagementToLocalhost: boolean; forceModelMappings: boolean; mappingEntries: ModelEntry[]; } const DISABLE_ALL_MODELS_RULE = '*'; const hasDisableAllModelsRule = (models?: string[]) => Array.isArray(models) && models.some((model) => String(model ?? '').trim() === DISABLE_ALL_MODELS_RULE); const stripDisableAllModelsRule = (models?: string[]) => Array.isArray(models) ? models.filter((model) => String(model ?? '').trim() !== DISABLE_ALL_MODELS_RULE) : []; const withDisableAllModelsRule = (models?: string[]) => { const base = stripDisableAllModelsRule(models); return [...base, DISABLE_ALL_MODELS_RULE]; }; const withoutDisableAllModelsRule = (models?: string[]) => { const base = stripDisableAllModelsRule(models); return base; }; const parseExcludedModels = (text: string): string[] => text .split(/[\n,]+/) .map((item) => item.trim()) .filter(Boolean); const excludedModelsToText = (models?: string[]) => Array.isArray(models) ? models.join('\n') : ''; const buildOpenAIModelsEndpoint = (baseUrl: string): string => { const trimmed = String(baseUrl || '') .trim() .replace(/\/+$/g, ''); if (!trimmed) return ''; return trimmed.endsWith('/v1') ? `${trimmed}/models` : `${trimmed}/v1/models`; }; const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => { const trimmed = String(baseUrl || '') .trim() .replace(/\/+$/g, ''); if (!trimmed) return ''; if (trimmed.endsWith('/chat/completions')) { return trimmed; } return trimmed.endsWith('/v1') ? `${trimmed}/chat/completions` : `${trimmed}/v1/chat/completions`; }; const OPENAI_TEST_TIMEOUT_MS = 30_000; // 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致 const getStatsBySource = ( apiKey: string, keyStats: KeyStats, maskFn: (key: string) => string ): KeyStatBucket => { const bySource = keyStats.bySource ?? {}; const masked = maskFn(apiKey); return bySource[apiKey] || bySource[masked] || { success: 0, failure: 0 }; }; // 对于 OpenAI 提供商,汇总所有 apiKeyEntries 的统计 - 与旧版逻辑一致 const getOpenAIProviderStats = ( apiKeyEntries: ApiKeyEntry[] | undefined, keyStats: KeyStats, maskFn: (key: string) => string ): KeyStatBucket => { const bySource = keyStats.bySource ?? {}; let totalSuccess = 0; let totalFailure = 0; (apiKeyEntries || []).forEach((entry) => { const key = entry?.apiKey || ''; if (!key) return; const masked = maskFn(key); const stats = bySource[key] || bySource[masked] || { success: 0, failure: 0 }; totalSuccess += stats.success; totalFailure += stats.failure; }); return { success: totalSuccess, failure: totalFailure }; }; const buildApiKeyEntry = (input?: Partial): ApiKeyEntry => ({ apiKey: input?.apiKey ?? '', proxyUrl: input?.proxyUrl ?? '', headers: input?.headers ?? {}, }); const ampcodeMappingsToEntries = (mappings?: AmpcodeModelMapping[]): ModelEntry[] => { if (!Array.isArray(mappings) || mappings.length === 0) { return [{ name: '', alias: '' }]; } return mappings.map((mapping) => ({ name: mapping.from ?? '', alias: mapping.to ?? '', })); }; const entriesToAmpcodeMappings = (entries: ModelEntry[]): AmpcodeModelMapping[] => { const seen = new Set(); const mappings: AmpcodeModelMapping[] = []; entries.forEach((entry) => { const from = entry.name.trim(); const to = entry.alias.trim(); if (!from || !to) return; const key = from.toLowerCase(); if (seen.has(key)) return; seen.add(key); mappings.push({ from, to }); }); return mappings; }; const buildAmpcodeFormState = (ampcode?: AmpcodeConfig | null): AmpcodeFormState => ({ upstreamUrl: ampcode?.upstreamUrl ?? '', upstreamApiKey: '', restrictManagementToLocalhost: ampcode?.restrictManagementToLocalhost ?? true, forceModelMappings: ampcode?.forceModelMappings ?? false, mappingEntries: ampcodeMappingsToEntries(ampcode?.modelMappings), }); export function AiProvidersPage() { const { t } = useTranslation(); const { showNotification } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const clearCache = useConfigStore((state) => state.clearCache); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [geminiKeys, setGeminiKeys] = useState([]); const [codexConfigs, setCodexConfigs] = useState([]); const [claudeConfigs, setClaudeConfigs] = useState([]); const [openaiProviders, setOpenaiProviders] = useState([]); const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); const [modal, setModal] = useState(null); const [geminiForm, setGeminiForm] = useState({ apiKey: '', baseUrl: '', headers: {}, excludedModels: [], excludedText: '', }); const [providerForm, setProviderForm] = useState< ProviderKeyConfig & { modelEntries: ModelEntry[]; excludedText: string } >({ apiKey: '', baseUrl: '', proxyUrl: '', headers: {}, models: [], excludedModels: [], modelEntries: [{ name: '', alias: '' }], excludedText: '', }); const [openaiForm, setOpenaiForm] = useState({ name: '', baseUrl: '', headers: [], apiKeyEntries: [buildApiKeyEntry()], modelEntries: [{ name: '', alias: '' }], }); const [ampcodeForm, setAmpcodeForm] = useState(() => buildAmpcodeFormState(null) ); const [ampcodeModalLoading, setAmpcodeModalLoading] = useState(false); const [ampcodeLoaded, setAmpcodeLoaded] = useState(false); const [ampcodeMappingsDirty, setAmpcodeMappingsDirty] = useState(false); const [ampcodeModalError, setAmpcodeModalError] = useState(''); const [ampcodeSaving, setAmpcodeSaving] = useState(false); const [openaiDiscoveryOpen, setOpenaiDiscoveryOpen] = useState(false); const [openaiDiscoveryEndpoint, setOpenaiDiscoveryEndpoint] = useState(''); const [openaiDiscoveryModels, setOpenaiDiscoveryModels] = useState([]); const [openaiDiscoveryLoading, setOpenaiDiscoveryLoading] = useState(false); const [openaiDiscoveryError, setOpenaiDiscoveryError] = useState(''); const [openaiDiscoverySearch, setOpenaiDiscoverySearch] = useState(''); const [openaiDiscoverySelected, setOpenaiDiscoverySelected] = useState>(new Set()); const [openaiTestModel, setOpenaiTestModel] = useState(''); const [openaiTestStatus, setOpenaiTestStatus] = useState< 'idle' | 'loading' | 'success' | 'error' >('idle'); const [openaiTestMessage, setOpenaiTestMessage] = useState(''); const [saving, setSaving] = useState(false); const [configSwitchingKey, setConfigSwitchingKey] = useState(null); const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]); const filteredOpenaiDiscoveryModels = useMemo(() => { const filter = openaiDiscoverySearch.trim().toLowerCase(); if (!filter) return openaiDiscoveryModels; return openaiDiscoveryModels.filter((model) => { const name = (model.name || '').toLowerCase(); const alias = (model.alias || '').toLowerCase(); const desc = (model.description || '').toLowerCase(); return name.includes(filter) || alias.includes(filter) || desc.includes(filter); }); }, [openaiDiscoveryModels, openaiDiscoverySearch]); const openaiAvailableModels = useMemo( () => openaiForm.modelEntries.map((entry) => entry.name.trim()).filter(Boolean), [openaiForm.modelEntries] ); // 加载 key 统计 const loadKeyStats = useCallback(async () => { try { const stats = await usageApi.getKeyStats(); setKeyStats(stats); } catch { // 静默失败 } }, []); const loadConfigs = async () => { setLoading(true); setError(''); try { const data = await fetchConfig(); setGeminiKeys(data?.geminiApiKeys || []); setCodexConfigs(data?.codexApiKeys || []); setClaudeConfigs(data?.claudeApiKeys || []); setOpenaiProviders(data?.openaiCompatibility || []); try { const ampcode = await ampcodeApi.getAmpcode(); updateConfigValue('ampcode', ampcode); clearCache('ampcode'); } catch { // ignore } } catch (err: any) { setError(err?.message || t('notification.refresh_failed')); } finally { setLoading(false); } }; useEffect(() => { loadConfigs(); loadKeyStats(); }, [loadKeyStats]); useEffect(() => { if (config?.geminiApiKeys) setGeminiKeys(config.geminiApiKeys); if (config?.codexApiKeys) setCodexConfigs(config.codexApiKeys); if (config?.claudeApiKeys) setClaudeConfigs(config.claudeApiKeys); if (config?.openaiCompatibility) setOpenaiProviders(config.openaiCompatibility); }, [ config?.geminiApiKeys, config?.codexApiKeys, config?.claudeApiKeys, config?.openaiCompatibility, ]); const closeModal = () => { setModal(null); setGeminiForm({ apiKey: '', baseUrl: '', headers: {}, excludedModels: [], excludedText: '', }); setProviderForm({ apiKey: '', baseUrl: '', proxyUrl: '', headers: {}, models: [], excludedModels: [], modelEntries: [{ name: '', alias: '' }], excludedText: '', }); setOpenaiForm({ name: '', baseUrl: '', headers: [], apiKeyEntries: [buildApiKeyEntry()], modelEntries: [{ name: '', alias: '' }], testModel: undefined, }); setAmpcodeForm(buildAmpcodeFormState(null)); setAmpcodeModalLoading(false); setAmpcodeLoaded(false); setAmpcodeMappingsDirty(false); setAmpcodeModalError(''); setAmpcodeSaving(false); setOpenaiDiscoveryOpen(false); setOpenaiDiscoveryModels([]); setOpenaiDiscoverySelected(new Set()); setOpenaiDiscoverySearch(''); setOpenaiDiscoveryError(''); setOpenaiDiscoveryEndpoint(''); setOpenaiTestModel(''); setOpenaiTestStatus('idle'); setOpenaiTestMessage(''); }; const openGeminiModal = (index: number | null) => { if (index !== null) { const entry = geminiKeys[index]; setGeminiForm({ ...entry, excludedText: excludedModelsToText(entry?.excludedModels), }); } setModal({ type: 'gemini', index }); }; const openProviderModal = (type: 'codex' | 'claude', index: number | null) => { const source = type === 'codex' ? codexConfigs : claudeConfigs; if (index !== null) { const entry = source[index]; setProviderForm({ ...entry, modelEntries: modelsToEntries(entry?.models), excludedText: excludedModelsToText(entry?.excludedModels), }); } setModal({ type, index }); }; const openAmpcodeModal = () => { setAmpcodeModalLoading(true); setAmpcodeLoaded(false); setAmpcodeMappingsDirty(false); setAmpcodeModalError(''); setAmpcodeForm(buildAmpcodeFormState(config?.ampcode ?? null)); setModal({ type: 'ampcode', index: null }); void (async () => { try { const ampcode = await ampcodeApi.getAmpcode(); setAmpcodeLoaded(true); updateConfigValue('ampcode', ampcode); clearCache('ampcode'); setAmpcodeForm(buildAmpcodeFormState(ampcode)); } catch (err: any) { setAmpcodeModalError(err?.message || t('notification.refresh_failed')); } finally { setAmpcodeModalLoading(false); } })(); }; const openOpenaiModal = (index: number | null) => { if (index !== null) { const entry = openaiProviders[index]; const modelEntries = modelsToEntries(entry.models); setOpenaiForm({ name: entry.name, baseUrl: entry.baseUrl, headers: headersToEntries(entry.headers), testModel: entry.testModel, modelEntries, apiKeyEntries: entry.apiKeyEntries?.length ? entry.apiKeyEntries : [buildApiKeyEntry()], }); const available = modelEntries.map((m) => m.name.trim()).filter(Boolean); const initialModel = entry.testModel && available.includes(entry.testModel) ? entry.testModel : available[0] || ''; setOpenaiTestModel(initialModel); } else { setOpenaiTestModel(''); } setOpenaiTestStatus('idle'); setOpenaiTestMessage(''); setModal({ type: 'openai', index }); }; const closeOpenaiModelDiscovery = () => { setOpenaiDiscoveryOpen(false); setOpenaiDiscoveryModels([]); setOpenaiDiscoverySelected(new Set()); setOpenaiDiscoverySearch(''); setOpenaiDiscoveryError(''); }; const fetchOpenaiModelDiscovery = async ({ allowFallback = true, }: { allowFallback?: boolean } = {}) => { const baseUrl = openaiForm.baseUrl.trim(); if (!baseUrl) return; setOpenaiDiscoveryLoading(true); setOpenaiDiscoveryError(''); try { const headers = buildHeaderObject(openaiForm.headers); const firstKey = openaiForm.apiKeyEntries .find((entry) => entry.apiKey?.trim()) ?.apiKey?.trim(); const hasAuthHeader = Boolean(headers.Authorization || headers['authorization']); const list = await modelsApi.fetchModels( baseUrl, hasAuthHeader ? undefined : firstKey, headers ); setOpenaiDiscoveryModels(list); } catch (err: any) { if (allowFallback) { try { const list = await modelsApi.fetchModels(baseUrl); setOpenaiDiscoveryModels(list); return; } catch (fallbackErr: any) { const message = fallbackErr?.message || err?.message || ''; setOpenaiDiscoveryModels([]); setOpenaiDiscoveryError(`${t('ai_providers.openai_models_fetch_error')}: ${message}`); } } else { setOpenaiDiscoveryModels([]); setOpenaiDiscoveryError( `${t('ai_providers.openai_models_fetch_error')}: ${err?.message || ''}` ); } } finally { setOpenaiDiscoveryLoading(false); } }; const openOpenaiModelDiscovery = () => { const baseUrl = openaiForm.baseUrl.trim(); if (!baseUrl) { showNotification(t('ai_providers.openai_models_fetch_invalid_url'), 'error'); return; } setOpenaiDiscoveryEndpoint(buildOpenAIModelsEndpoint(baseUrl)); setOpenaiDiscoveryModels([]); setOpenaiDiscoverySearch(''); setOpenaiDiscoverySelected(new Set()); setOpenaiDiscoveryError(''); setOpenaiDiscoveryOpen(true); void fetchOpenaiModelDiscovery(); }; const toggleOpenaiModelSelection = (name: string) => { setOpenaiDiscoverySelected((prev) => { const next = new Set(prev); if (next.has(name)) { next.delete(name); } else { next.add(name); } return next; }); }; const applyOpenaiModelDiscoverySelection = () => { const selectedModels = openaiDiscoveryModels.filter((model) => openaiDiscoverySelected.has(model.name) ); if (!selectedModels.length) { closeOpenaiModelDiscovery(); return; } const mergedMap = new Map(); openaiForm.modelEntries.forEach((entry) => { const name = entry.name.trim(); if (!name) return; mergedMap.set(name, { name, alias: entry.alias?.trim() || '' }); }); let addedCount = 0; selectedModels.forEach((model) => { const name = model.name.trim(); if (!name || mergedMap.has(name)) return; mergedMap.set(name, { name, alias: model.alias ?? '' }); addedCount += 1; }); const mergedEntries = Array.from(mergedMap.values()); setOpenaiForm((prev) => ({ ...prev, modelEntries: mergedEntries.length ? mergedEntries : [{ name: '', alias: '' }], })); closeOpenaiModelDiscovery(); if (addedCount > 0) { showNotification( t('ai_providers.openai_models_fetch_added', { count: addedCount }), 'success' ); } }; useEffect(() => { if (modal?.type !== 'openai') return; if (openaiAvailableModels.length === 0) { if (openaiTestModel) { setOpenaiTestModel(''); setOpenaiTestStatus('idle'); setOpenaiTestMessage(''); } return; } if (!openaiTestModel || !openaiAvailableModels.includes(openaiTestModel)) { setOpenaiTestModel(openaiAvailableModels[0]); setOpenaiTestStatus('idle'); setOpenaiTestMessage(''); } }, [modal?.type, openaiAvailableModels, openaiTestModel]); const testOpenaiProviderConnection = async () => { const baseUrl = openaiForm.baseUrl.trim(); if (!baseUrl) { const message = t('notification.openai_test_url_required'); setOpenaiTestStatus('error'); setOpenaiTestMessage(message); showNotification(message, 'error'); return; } const endpoint = buildOpenAIChatCompletionsEndpoint(baseUrl); if (!endpoint) { const message = t('notification.openai_test_url_required'); setOpenaiTestStatus('error'); setOpenaiTestMessage(message); showNotification(message, 'error'); return; } const firstKeyEntry = openaiForm.apiKeyEntries.find((entry) => entry.apiKey?.trim()); if (!firstKeyEntry) { const message = t('notification.openai_test_key_required'); setOpenaiTestStatus('error'); setOpenaiTestMessage(message); showNotification(message, 'error'); return; } const modelName = openaiTestModel.trim() || openaiAvailableModels[0] || ''; if (!modelName) { const message = t('notification.openai_test_model_required'); setOpenaiTestStatus('error'); setOpenaiTestMessage(message); showNotification(message, 'error'); return; } const customHeaders = buildHeaderObject(openaiForm.headers); const headers: Record = { 'Content-Type': 'application/json', ...customHeaders, }; if (!headers.Authorization && !headers['authorization']) { headers.Authorization = `Bearer ${firstKeyEntry.apiKey.trim()}`; } setOpenaiTestStatus('loading'); setOpenaiTestMessage(t('ai_providers.openai_test_running')); const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), OPENAI_TEST_TIMEOUT_MS); try { const response = await fetch(endpoint, { method: 'POST', headers, signal: controller.signal, body: JSON.stringify({ model: modelName, messages: [{ role: 'user', content: 'Hi' }], stream: false, max_tokens: 5, }), }); const rawText = await response.text(); if (!response.ok) { let errorMessage = `${response.status} ${response.statusText}`; try { const parsed = rawText ? JSON.parse(rawText) : null; errorMessage = parsed?.error?.message || parsed?.message || errorMessage; } catch { if (rawText) { errorMessage = rawText; } } throw new Error(errorMessage); } setOpenaiTestStatus('success'); setOpenaiTestMessage(t('ai_providers.openai_test_success')); } catch (err: any) { setOpenaiTestStatus('error'); if (err?.name === 'AbortError') { setOpenaiTestMessage( t('ai_providers.openai_test_timeout', { seconds: OPENAI_TEST_TIMEOUT_MS / 1000 }) ); } else { setOpenaiTestMessage(`${t('ai_providers.openai_test_failed')}: ${err?.message || ''}`); } } finally { window.clearTimeout(timeoutId); } }; const clearAmpcodeUpstreamApiKey = async () => { if (!window.confirm(t('ai_providers.ampcode_clear_upstream_api_key_confirm'))) return; setAmpcodeSaving(true); setAmpcodeModalError(''); try { await ampcodeApi.clearUpstreamApiKey(); const previous = config?.ampcode ?? {}; const next: AmpcodeConfig = { ...previous }; delete (next as any).upstreamApiKey; updateConfigValue('ampcode', next); clearCache('ampcode'); showNotification(t('notification.ampcode_upstream_api_key_cleared'), 'success'); } catch (err: any) { const message = err?.message || ''; setAmpcodeModalError(message); showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); } finally { setAmpcodeSaving(false); } }; const saveAmpcode = async () => { if (!ampcodeLoaded && ampcodeMappingsDirty) { const confirmed = window.confirm(t('ai_providers.ampcode_mappings_overwrite_confirm')); if (!confirmed) return; } setAmpcodeSaving(true); setAmpcodeModalError(''); try { const upstreamUrl = ampcodeForm.upstreamUrl.trim(); const overrideKey = ampcodeForm.upstreamApiKey.trim(); const modelMappings = entriesToAmpcodeMappings(ampcodeForm.mappingEntries); if (upstreamUrl) { await ampcodeApi.updateUpstreamUrl(upstreamUrl); } else { await ampcodeApi.clearUpstreamUrl(); } await ampcodeApi.updateRestrictManagementToLocalhost( ampcodeForm.restrictManagementToLocalhost ); await ampcodeApi.updateForceModelMappings(ampcodeForm.forceModelMappings); if (ampcodeLoaded || ampcodeMappingsDirty) { if (modelMappings.length) { await ampcodeApi.saveModelMappings(modelMappings); } else { await ampcodeApi.clearModelMappings(); } } if (overrideKey) { await ampcodeApi.updateUpstreamApiKey(overrideKey); } const previous = config?.ampcode ?? {}; const next: AmpcodeConfig = { ...previous, upstreamUrl: upstreamUrl || undefined, restrictManagementToLocalhost: ampcodeForm.restrictManagementToLocalhost, forceModelMappings: ampcodeForm.forceModelMappings, }; if (overrideKey) { next.upstreamApiKey = overrideKey; } if (ampcodeLoaded || ampcodeMappingsDirty) { if (modelMappings.length) { next.modelMappings = modelMappings; } else { delete (next as any).modelMappings; } } updateConfigValue('ampcode', next); clearCache('ampcode'); showNotification(t('notification.ampcode_updated'), 'success'); closeModal(); } catch (err: any) { const message = err?.message || ''; setAmpcodeModalError(message); showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); } finally { setAmpcodeSaving(false); } }; const saveGemini = async () => { setSaving(true); try { const payload: GeminiKeyConfig = { apiKey: geminiForm.apiKey.trim(), baseUrl: geminiForm.baseUrl?.trim() || undefined, headers: buildHeaderObject(headersToEntries(geminiForm.headers as any)), excludedModels: parseExcludedModels(geminiForm.excludedText), }; const nextList = modal?.type === 'gemini' && modal.index !== null ? geminiKeys.map((item, idx) => (idx === modal.index ? payload : item)) : [...geminiKeys, payload]; await providersApi.saveGeminiKeys(nextList); setGeminiKeys(nextList); updateConfigValue('gemini-api-key', nextList); clearCache('gemini-api-key'); const message = modal?.index !== null ? t('notification.gemini_key_updated') : t('notification.gemini_key_added'); showNotification(message, 'success'); closeModal(); } catch (err: any) { showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error'); } finally { setSaving(false); } }; const deleteGemini = async (apiKey: string) => { if (!window.confirm(t('ai_providers.gemini_delete_confirm'))) return; try { await providersApi.deleteGeminiKey(apiKey); const next = geminiKeys.filter((item) => item.apiKey !== apiKey); setGeminiKeys(next); updateConfigValue('gemini-api-key', next); clearCache('gemini-api-key'); showNotification(t('notification.gemini_key_deleted'), 'success'); } catch (err: any) { showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); } }; const setConfigEnabled = async ( provider: 'gemini' | 'codex' | 'claude', index: number, enabled: boolean ) => { if (provider === 'gemini') { const current = geminiKeys[index]; if (!current) return; const switchingKey = `${provider}:${current.apiKey}`; setConfigSwitchingKey(switchingKey); const previousList = geminiKeys; const nextExcluded = enabled ? withoutDisableAllModelsRule(current.excludedModels) : withDisableAllModelsRule(current.excludedModels); const nextItem: GeminiKeyConfig = { ...current, excludedModels: nextExcluded }; const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item)); setGeminiKeys(nextList); updateConfigValue('gemini-api-key', nextList); clearCache('gemini-api-key'); try { await providersApi.saveGeminiKeys(nextList); showNotification( enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success' ); } catch (err: any) { setGeminiKeys(previousList); updateConfigValue('gemini-api-key', previousList); clearCache('gemini-api-key'); showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error'); } finally { setConfigSwitchingKey(null); } return; } const source = provider === 'codex' ? codexConfigs : claudeConfigs; const current = source[index]; if (!current) return; const switchingKey = `${provider}:${current.apiKey}`; setConfigSwitchingKey(switchingKey); const previousList = source; const nextExcluded = enabled ? withoutDisableAllModelsRule(current.excludedModels) : withDisableAllModelsRule(current.excludedModels); const nextItem: ProviderKeyConfig = { ...current, excludedModels: nextExcluded }; const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item)); if (provider === 'codex') { setCodexConfigs(nextList); updateConfigValue('codex-api-key', nextList); clearCache('codex-api-key'); } else { setClaudeConfigs(nextList); updateConfigValue('claude-api-key', nextList); clearCache('claude-api-key'); } try { if (provider === 'codex') { await providersApi.saveCodexConfigs(nextList); } else { await providersApi.saveClaudeConfigs(nextList); } showNotification( enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success' ); } catch (err: any) { if (provider === 'codex') { setCodexConfigs(previousList); updateConfigValue('codex-api-key', previousList); clearCache('codex-api-key'); } else { setClaudeConfigs(previousList); updateConfigValue('claude-api-key', previousList); clearCache('claude-api-key'); } showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error'); } finally { setConfigSwitchingKey(null); } }; const saveProvider = async (type: 'codex' | 'claude') => { const baseUrl = (providerForm.baseUrl ?? '').trim(); if (!baseUrl) { showNotification(t('codex_base_url_required'), 'error'); return; } setSaving(true); try { const source = type === 'codex' ? codexConfigs : claudeConfigs; const payload: ProviderKeyConfig = { apiKey: providerForm.apiKey.trim(), baseUrl, proxyUrl: providerForm.proxyUrl?.trim() || undefined, headers: buildHeaderObject(headersToEntries(providerForm.headers as any)), models: entriesToModels(providerForm.modelEntries), excludedModels: parseExcludedModels(providerForm.excludedText), }; const nextList = modal?.type === type && modal.index !== null ? source.map((item, idx) => (idx === modal.index ? payload : item)) : [...source, payload]; if (type === 'codex') { await providersApi.saveCodexConfigs(nextList); setCodexConfigs(nextList); updateConfigValue('codex-api-key', nextList); clearCache('codex-api-key'); const message = modal?.index !== null ? t('notification.codex_config_updated') : t('notification.codex_config_added'); showNotification(message, 'success'); } else { await providersApi.saveClaudeConfigs(nextList); setClaudeConfigs(nextList); updateConfigValue('claude-api-key', nextList); clearCache('claude-api-key'); const message = modal?.index !== null ? t('notification.claude_config_updated') : t('notification.claude_config_added'); showNotification(message, 'success'); } closeModal(); } catch (err: any) { showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error'); } finally { setSaving(false); } }; const deleteProviderEntry = async (type: 'codex' | 'claude', apiKey: string) => { if (!window.confirm(t(`ai_providers.${type}_delete_confirm` as any))) return; try { if (type === 'codex') { await providersApi.deleteCodexConfig(apiKey); const next = codexConfigs.filter((item) => item.apiKey !== apiKey); setCodexConfigs(next); updateConfigValue('codex-api-key', next); clearCache('codex-api-key'); showNotification(t('notification.codex_config_deleted'), 'success'); } else { await providersApi.deleteClaudeConfig(apiKey); const next = claudeConfigs.filter((item) => item.apiKey !== apiKey); setClaudeConfigs(next); updateConfigValue('claude-api-key', next); clearCache('claude-api-key'); showNotification(t('notification.claude_config_deleted'), 'success'); } } catch (err: any) { showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); } }; const saveOpenai = async () => { setSaving(true); try { const payload: OpenAIProviderConfig = { name: openaiForm.name.trim(), baseUrl: openaiForm.baseUrl.trim(), headers: buildHeaderObject(openaiForm.headers), apiKeyEntries: openaiForm.apiKeyEntries.map((entry) => ({ apiKey: entry.apiKey.trim(), proxyUrl: entry.proxyUrl?.trim() || undefined, headers: entry.headers, })), }; if (openaiForm.testModel) payload.testModel = openaiForm.testModel.trim(); const models = entriesToModels(openaiForm.modelEntries); if (models.length) payload.models = models; const nextList = modal?.type === 'openai' && modal.index !== null ? openaiProviders.map((item, idx) => (idx === modal.index ? payload : item)) : [...openaiProviders, payload]; await providersApi.saveOpenAIProviders(nextList); setOpenaiProviders(nextList); updateConfigValue('openai-compatibility', nextList); clearCache('openai-compatibility'); const message = modal?.index !== null ? t('notification.openai_provider_updated') : t('notification.openai_provider_added'); showNotification(message, 'success'); closeModal(); } catch (err: any) { showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error'); } finally { setSaving(false); } }; const deleteOpenai = async (name: string) => { if (!window.confirm(t('ai_providers.openai_delete_confirm'))) return; try { await providersApi.deleteOpenAIProvider(name); const next = openaiProviders.filter((item) => item.name !== name); setOpenaiProviders(next); updateConfigValue('openai-compatibility', next); clearCache('openai-compatibility'); showNotification(t('notification.openai_provider_deleted'), 'success'); } catch (err: any) { showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); } }; const renderKeyEntries = (entries: ApiKeyEntry[]) => { const list = entries.length ? entries : [buildApiKeyEntry()]; const updateEntry = (idx: number, field: keyof ApiKeyEntry, value: string) => { const next = list.map((entry, i) => (i === idx ? { ...entry, [field]: value } : entry)); setOpenaiForm((prev) => ({ ...prev, apiKeyEntries: next })); }; const removeEntry = (idx: number) => { const next = list.filter((_, i) => i !== idx); setOpenaiForm((prev) => ({ ...prev, apiKeyEntries: next.length ? next : [buildApiKeyEntry()], })); }; const addEntry = () => { setOpenaiForm((prev) => ({ ...prev, apiKeyEntries: [...list, buildApiKeyEntry()] })); }; return (
{list.map((entry, index) => (
updateEntry(index, 'apiKey', e.target.value)} /> updateEntry(index, 'proxyUrl', e.target.value)} />
))}
); }; const renderList = ( items: T[], keyField: (item: T) => string, renderContent: (item: T, index: number) => ReactNode, onEdit: (index: number) => void, onDelete: (item: T) => void, addLabel: string, deleteLabel?: string, options?: { getRowDisabled?: (item: T, index: number) => boolean; renderExtraActions?: (item: T, index: number) => ReactNode; } ) => { if (loading) { return
{t('common.loading')}
; } if (!items.length) { return ( onEdit(-1)} disabled={disableControls}> {addLabel} } /> ); } return (
{items.map((item, index) => { const rowDisabled = options?.getRowDisabled ? options.getRowDisabled(item, index) : false; return (
{renderContent(item, index)}
{options?.renderExtraActions ? options.renderExtraActions(item, index) : null}
); })}
); }; return (

{t('ai_providers.title')}

{error &&
{error}
} openGeminiModal(null)} disabled={disableControls || saving || Boolean(configSwitchingKey)} > {t('ai_providers.gemini_add_button')} } > {renderList( geminiKeys, (item) => item.apiKey, (item, index) => { const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); const headerEntries = Object.entries(item.headers || {}); const configDisabled = hasDisableAllModelsRule(item.excludedModels); const excludedModels = item.excludedModels ?? []; return (
{t('ai_providers.gemini_item_title')} #{index + 1}
{/* API Key 行 */}
{t('common.api_key')}: {maskApiKey(item.apiKey)}
{/* Base URL 行 */} {item.baseUrl && (
{t('common.base_url')}: {item.baseUrl}
)} {/* 自定义请求头徽章 */} {headerEntries.length > 0 && (
{headerEntries.map(([key, value]) => ( {key}: {value} ))}
)} {configDisabled && (
{t('ai_providers.config_disabled_badge')}
)} {/* 排除模型徽章 */} {excludedModels.length ? (
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
{excludedModels.map((model) => ( {model} ))}
) : null} {/* 成功/失败统计 */}
{t('stats.success')}: {stats.success} {t('stats.failure')}: {stats.failure}
); }, (index) => openGeminiModal(index), (item) => deleteGemini(item.apiKey), t('ai_providers.gemini_add_button'), undefined, { getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels), renderExtraActions: (item, index) => ( void setConfigEnabled('gemini', index, value)} /> ), } )}
openProviderModal('codex', null)} disabled={disableControls || saving || Boolean(configSwitchingKey)} > {t('ai_providers.codex_add_button')} } > {renderList( codexConfigs, (item) => item.apiKey, (item, _index) => { const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); const headerEntries = Object.entries(item.headers || {}); const configDisabled = hasDisableAllModelsRule(item.excludedModels); const excludedModels = item.excludedModels ?? []; return (
{t('ai_providers.codex_item_title')}
{/* API Key 行 */}
{t('common.api_key')}: {maskApiKey(item.apiKey)}
{/* Base URL 行 */} {item.baseUrl && (
{t('common.base_url')}: {item.baseUrl}
)} {/* Proxy URL 行 */} {item.proxyUrl && (
{t('common.proxy_url')}: {item.proxyUrl}
)} {/* 自定义请求头徽章 */} {headerEntries.length > 0 && (
{headerEntries.map(([key, value]) => ( {key}: {value} ))}
)} {configDisabled && (
{t('ai_providers.config_disabled_badge')}
)} {/* 排除模型徽章 */} {excludedModels.length ? (
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
{excludedModels.map((model) => ( {model} ))}
) : null} {/* 成功/失败统计 */}
{t('stats.success')}: {stats.success} {t('stats.failure')}: {stats.failure}
); }, (index) => openProviderModal('codex', index), (item) => deleteProviderEntry('codex', item.apiKey), t('ai_providers.codex_add_button'), undefined, { getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels), renderExtraActions: (item, index) => ( void setConfigEnabled('codex', index, value)} /> ), } )}
openProviderModal('claude', null)} disabled={disableControls || saving || Boolean(configSwitchingKey)} > {t('ai_providers.claude_add_button')} } > {renderList( claudeConfigs, (item) => item.apiKey, (item, _index) => { const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); const headerEntries = Object.entries(item.headers || {}); const configDisabled = hasDisableAllModelsRule(item.excludedModels); const excludedModels = item.excludedModels ?? []; return (
{t('ai_providers.claude_item_title')}
{/* API Key 行 */}
{t('common.api_key')}: {maskApiKey(item.apiKey)}
{/* Base URL 行 */} {item.baseUrl && (
{t('common.base_url')}: {item.baseUrl}
)} {/* Proxy URL 行 */} {item.proxyUrl && (
{t('common.proxy_url')}: {item.proxyUrl}
)} {/* 自定义请求头徽章 */} {headerEntries.length > 0 && (
{headerEntries.map(([key, value]) => ( {key}: {value} ))}
)} {configDisabled && (
{t('ai_providers.config_disabled_badge')}
)} {/* 模型列表 */} {item.models?.length ? (
{t('ai_providers.claude_models_count')}: {item.models.length} {item.models.map((model) => ( {model.name} {model.alias && model.alias !== model.name && ( {model.alias} )} ))}
) : null} {/* 排除模型徽章 */} {excludedModels.length ? (
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
{excludedModels.map((model) => ( {model} ))}
) : null} {/* 成功/失败统计 */}
{t('stats.success')}: {stats.success} {t('stats.failure')}: {stats.failure}
); }, (index) => openProviderModal('claude', index), (item) => deleteProviderEntry('claude', item.apiKey), t('ai_providers.claude_add_button'), undefined, { getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels), renderExtraActions: (item, index) => ( void setConfigEnabled('claude', index, value)} /> ), } )}
{t('common.edit')} } > {loading ? (
{t('common.loading')}
) : ( <>
{t('ai_providers.ampcode_upstream_url_label')}: {config?.ampcode?.upstreamUrl || t('common.not_set')}
{t('ai_providers.ampcode_upstream_api_key_label')}: {config?.ampcode?.upstreamApiKey ? maskApiKey(config.ampcode.upstreamApiKey) : t('common.not_set')}
{t('ai_providers.ampcode_restrict_management_label')}: {(config?.ampcode?.restrictManagementToLocalhost ?? true) ? t('common.yes') : t('common.no')}
{t('ai_providers.ampcode_force_model_mappings_label')}: {(config?.ampcode?.forceModelMappings ?? false) ? t('common.yes') : t('common.no')}
{t('ai_providers.ampcode_model_mappings_count')}: {config?.ampcode?.modelMappings?.length || 0}
{config?.ampcode?.modelMappings?.length ? (
{config.ampcode.modelMappings.slice(0, 5).map((mapping) => ( {mapping.from} {mapping.to} ))} {config.ampcode.modelMappings.length > 5 && ( +{config.ampcode.modelMappings.length - 5} )}
) : null} )}
openOpenaiModal(null)} disabled={disableControls || saving || Boolean(configSwitchingKey)} > {t('ai_providers.openai_add_button')} } > {renderList( openaiProviders, (item) => item.name, (item, _index) => { const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, maskApiKey); const headerEntries = Object.entries(item.headers || {}); const apiKeyEntries = item.apiKeyEntries || []; return (
{item.name}
{/* Base URL 行 */}
{t('common.base_url')}: {item.baseUrl}
{/* 自定义请求头徽章 */} {headerEntries.length > 0 && (
{headerEntries.map(([key, value]) => ( {key}: {value} ))}
)} {/* API密钥条目二级卡片 */} {apiKeyEntries.length > 0 && (
{t('ai_providers.openai_keys_count')}: {apiKeyEntries.length}
{apiKeyEntries.map((entry, entryIndex) => { const entryStats = getStatsBySource(entry.apiKey, keyStats, maskApiKey); return (
{entryIndex + 1} {maskApiKey(entry.apiKey)} {entry.proxyUrl && ( {entry.proxyUrl} )}
{entryStats.success} {entryStats.failure}
); })}
)} {/* 模型数量标签 */}
{t('ai_providers.openai_models_count')}: {item.models?.length || 0}
{/* 模型列表徽章 */} {item.models?.length ? (
{item.models.map((model) => ( {model.name} {model.alias && model.alias !== model.name && ( {model.alias} )} ))}
) : null} {/* 测试模型 */} {item.testModel && (
Test Model: {item.testModel}
)} {/* 成功/失败统计(汇总) */}
{t('stats.success')}: {stats.success} {t('stats.failure')}: {stats.failure}
); }, (index) => openOpenaiModal(index), (item) => deleteOpenai(item.name), t('ai_providers.openai_add_button') )}
{/* Ampcode Modal */} } > {ampcodeModalError &&
{ampcodeModalError}
} setAmpcodeForm((prev) => ({ ...prev, upstreamUrl: e.target.value }))} disabled={ampcodeModalLoading || ampcodeSaving} hint={t('ai_providers.ampcode_upstream_url_hint')} /> setAmpcodeForm((prev) => ({ ...prev, upstreamApiKey: e.target.value })) } disabled={ampcodeModalLoading || ampcodeSaving} hint={t('ai_providers.ampcode_upstream_api_key_hint')} />
{t('ai_providers.ampcode_upstream_api_key_current', { key: config?.ampcode?.upstreamApiKey ? maskApiKey(config.ampcode.upstreamApiKey) : t('common.not_set'), })}
setAmpcodeForm((prev) => ({ ...prev, restrictManagementToLocalhost: value })) } disabled={ampcodeModalLoading || ampcodeSaving} />
{t('ai_providers.ampcode_restrict_management_hint')}
setAmpcodeForm((prev) => ({ ...prev, forceModelMappings: value })) } disabled={ampcodeModalLoading || ampcodeSaving} />
{t('ai_providers.ampcode_force_model_mappings_hint')}
{ setAmpcodeMappingsDirty(true); setAmpcodeForm((prev) => ({ ...prev, mappingEntries: entries })); }} addLabel={t('ai_providers.ampcode_model_mappings_add_btn')} namePlaceholder={t('ai_providers.ampcode_model_mappings_from_placeholder')} aliasPlaceholder={t('ai_providers.ampcode_model_mappings_to_placeholder')} disabled={ampcodeModalLoading || ampcodeSaving} />
{t('ai_providers.ampcode_model_mappings_hint')}
{/* Gemini Modal */} } > setGeminiForm((prev) => ({ ...prev, apiKey: e.target.value }))} /> setGeminiForm((prev) => ({ ...prev, baseUrl: e.target.value }))} /> setGeminiForm((prev) => ({ ...prev, headers: buildHeaderObject(entries) })) } addLabel={t('common.custom_headers_add')} keyPlaceholder={t('common.custom_headers_key_placeholder')} valuePlaceholder={t('common.custom_headers_value_placeholder')} />