import { useState, useEffect } from 'react'; import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd'; import { SaveOutlined, DeleteOutlined, ReloadOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons'; import { settingsApi, mcpPluginApi } from '../services/api'; import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types'; import { eventBus, EventNames } from '../store/eventBus'; const { Title, Text } = Typography; const { Option } = Select; const { useBreakpoint } = Grid; const { TextArea } = Input; export default function SettingsPage() { const screens = useBreakpoint(); const isMobile = !screens.md; // md断点是768px const [form] = Form.useForm(); const [modal, contextHolder] = Modal.useModal(); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [hasSettings, setHasSettings] = useState(false); const [isDefaultSettings, setIsDefaultSettings] = useState(false); const [modelOptions, setModelOptions] = useState>([]); const [fetchingModels, setFetchingModels] = useState(false); const [modelsFetched, setModelsFetched] = useState(false); const [testingApi, setTestingApi] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string; response_time_ms?: number; response_preview?: string; error?: string; error_type?: string; suggestions?: string[]; } | null>(null); const [showTestResult, setShowTestResult] = useState(false); // 预设相关状态 const [activeTab, setActiveTab] = useState('current'); const [presets, setPresets] = useState([]); const [presetsLoading, setPresetsLoading] = useState(false); const [activePresetId, setActivePresetId] = useState(); const [editingPreset, setEditingPreset] = useState(null); const [isPresetModalVisible, setIsPresetModalVisible] = useState(false); const [testingPresetId, setTestingPresetId] = useState(null); const [presetForm] = Form.useForm(); // 预设编辑窗口的模型列表状态(独立于当前配置的模型列表) const [presetModelOptions, setPresetModelOptions] = useState>([]); const [fetchingPresetModels, setFetchingPresetModels] = useState(false); const [presetModelsFetched, setPresetModelsFetched] = useState(false); useEffect(() => { loadSettings(); if (activeTab === 'presets') { loadPresets(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (activeTab === 'presets') { loadPresets(); } else if (activeTab === 'current') { // 切换到当前配置Tab时,刷新设置以获取最新数据 loadSettings(); // 清除旧的测试结果,因为可能是其他配置的测试结果 setTestResult(null); setShowTestResult(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab]); const loadSettings = async () => { setInitialLoading(true); try { const settings = await settingsApi.getSettings(); form.setFieldsValue(settings); // 判断是否为默认设置(id='0'表示来自.env的默认配置) if (settings.id === '0' || !settings.id) { setIsDefaultSettings(true); setHasSettings(false); } else { setIsDefaultSettings(false); setHasSettings(true); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { // 如果404表示还没有设置,使用默认值 if (error?.response?.status === 404) { setHasSettings(false); setIsDefaultSettings(true); form.setFieldsValue({ api_provider: 'openai', api_base_url: 'https://api.openai.com/v1', llm_model: 'gpt-4', temperature: 0.7, max_tokens: 2000, }); } else { message.error('加载设置失败'); } } finally { setInitialLoading(false); } }; const handleSave = async (values: SettingsUpdate) => { setLoading(true); try { // 检查是否与 MCP 缓存的配置不一致 const verifiedConfigStr = localStorage.getItem('mcp_verified_config'); let configChanged = false; if (verifiedConfigStr) { try { const verifiedConfig = JSON.parse(verifiedConfigStr); configChanged = verifiedConfig.provider !== values.api_provider || verifiedConfig.baseUrl !== values.api_base_url || verifiedConfig.model !== values.llm_model; } catch (e) { console.error('Failed to parse verified config:', e); } } await settingsApi.saveSettings(values); message.success('设置已保存'); setHasSettings(true); setIsDefaultSettings(false); // 保存后清除测试结果,因为配置可能已变更 setTestResult(null); setShowTestResult(false); // 手动保存配置后,需要同步更新预设激活状态 // 因为用户手动修改的配置可能与之前激活的预设不一致了 // 重新加载预设列表以确保状态正确(后端在save时会自动取消激活状态) if (activePresetId) { // 检查当前保存的配置是否与激活预设一致 const activePreset = presets.find(p => p.id === activePresetId); if (activePreset) { const presetConfig = activePreset.config; const configMismatch = presetConfig.api_provider !== values.api_provider || presetConfig.api_key !== values.api_key || presetConfig.api_base_url !== values.api_base_url || presetConfig.llm_model !== values.llm_model || presetConfig.temperature !== values.temperature || presetConfig.max_tokens !== values.max_tokens; if (configMismatch) { // 配置已变更,清除前端的激活状态标记 setActivePresetId(undefined); message.info('配置已更改,预设激活状态已取消'); // 刷新预设列表以同步后端取消激活的状态 loadPresets(); } } } // 如果配置发生变化,需要处理 MCP 插件 if (configChanged) { // 清除 MCP 验证缓存 localStorage.removeItem('mcp_verified_config'); // 检查并禁用所有 MCP 插件 try { const plugins = await mcpPluginApi.getPlugins(); const activePlugins = plugins.filter(p => p.enabled); if (activePlugins.length > 0) { // 禁用所有插件 message.loading({ content: '正在禁用 MCP 插件...', key: 'disable_mcp' }); await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false))); message.success({ content: '已禁用所有 MCP 插件', key: 'disable_mcp' }); // 显示提示弹窗 modal.warning({ title: ( API 配置已更改 ), centered: true, content: (
请完成以下步骤:
  1. 前往 MCP 插件管理页面
  2. 重新进行"模型能力检查"
  3. 确认新模型支持 Function Calling 后再启用插件
), okText: '前往 MCP 页面', cancelText: '稍后处理', onOk: () => { eventBus.emit(EventNames.SWITCH_TO_MCP_VIEW); }, }); } } catch (err) { console.error('Failed to disable MCP plugins:', err); } } } catch { message.error('保存设置失败'); } finally { setLoading(false); } }; const handleReset = () => { modal.confirm({ title: '重置设置', content: '确定要重置为默认值吗?', centered: true, okText: '确定', cancelText: '取消', onOk: () => { form.setFieldsValue({ api_provider: 'openai', api_key: '', api_base_url: 'https://api.openai.com/v1', llm_model: 'gpt-4', temperature: 0.7, max_tokens: 2000, }); message.info('已重置为默认值,请点击保存'); }, }); }; const handleDelete = () => { modal.confirm({ title: '删除设置', content: '确定要删除所有设置吗?此操作不可恢复。', centered: true, okText: '确定', cancelText: '取消', okType: 'danger', onOk: async () => { setLoading(true); try { await settingsApi.deleteSettings(); message.success('设置已删除'); setHasSettings(false); form.resetFields(); } catch { message.error('删除设置失败'); } finally { setLoading(false); } }, }); }; const apiProviders = [ { value: 'openai', label: 'OpenAI Compatible', defaultUrl: 'https://api.openai.com/v1' }, // { value: 'anthropic', label: 'Anthropic (Claude)', defaultUrl: 'https://api.anthropic.com' }, { value: 'gemini', label: 'Google Gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta' }, ]; const handleProviderChange = (value: string) => { const provider = apiProviders.find(p => p.value === value); if (provider && provider.defaultUrl) { form.setFieldValue('api_base_url', provider.defaultUrl); } // 清空模型列表,需要重新获取 setModelOptions([]); setModelsFetched(false); }; const handleFetchModels = async (silent: boolean = false) => { const apiKey = form.getFieldValue('api_key'); const apiBaseUrl = form.getFieldValue('api_base_url'); const provider = form.getFieldValue('api_provider'); if (!apiKey || !apiBaseUrl) { if (!silent) { message.warning('请先填写 API 密钥和 API 地址'); } return; } setFetchingModels(true); try { const response = await settingsApi.getAvailableModels({ api_key: apiKey, api_base_url: apiBaseUrl, provider: provider || 'openai' }); setModelOptions(response.models); setModelsFetched(true); if (!silent) { message.success(`成功获取 ${response.count || response.models.length} 个可用模型`); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { const errorMsg = error?.response?.data?.detail || '获取模型列表失败'; if (!silent) { message.error(errorMsg); } setModelOptions([]); setModelsFetched(true); // 即使失败也标记为已尝试,避免重复请求 } finally { setFetchingModels(false); } }; const handleModelSelectFocus = () => { // 如果还没有获取过模型列表,自动获取 if (!modelsFetched && !fetchingModels) { handleFetchModels(true); // silent模式,不显示成功消息 } }; const handleTestConnection = async () => { const apiKey = form.getFieldValue('api_key'); const apiBaseUrl = form.getFieldValue('api_base_url'); const provider = form.getFieldValue('api_provider'); const modelName = form.getFieldValue('llm_model'); const temperature = form.getFieldValue('temperature'); const maxTokens = form.getFieldValue('max_tokens'); if (!apiKey || !apiBaseUrl || !provider || !modelName) { message.warning('请先填写完整的配置信息'); return; } setTestingApi(true); setTestResult(null); try { const result = await settingsApi.testApiConnection({ api_key: apiKey, api_base_url: apiBaseUrl, provider: provider, llm_model: modelName, temperature: temperature, max_tokens: maxTokens }); setTestResult(result); setShowTestResult(true); if (result.success) { message.success(`测试成功!响应时间: ${result.response_time_ms}ms`); } else { message.error('API 测试失败,请查看详细信息'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { const errorMsg = error?.response?.data?.detail || '测试请求失败'; message.error(errorMsg); setTestResult({ success: false, message: '测试请求失败', error: errorMsg, error_type: 'RequestError', suggestions: ['请检查网络连接', '请确认后端服务是否正常运行'] }); setShowTestResult(true); } finally { setTestingApi(false); } }; // ========== 预设管理函数 ========== const loadPresets = async () => { setPresetsLoading(true); try { const response = await settingsApi.getPresets(); setPresets(response.presets); setActivePresetId(response.active_preset_id); } catch (error) { message.error('加载预设失败'); console.error(error); } finally { setPresetsLoading(false); } }; const showPresetModal = (preset?: APIKeyPreset) => { // 重置预设模型列表状态 setPresetModelOptions([]); setPresetModelsFetched(false); if (preset) { setEditingPreset(preset); presetForm.setFieldsValue({ name: preset.name, description: preset.description, ...preset.config, }); } else { setEditingPreset(null); presetForm.resetFields(); presetForm.setFieldsValue({ api_provider: 'openai', api_base_url: 'https://api.openai.com/v1', temperature: 0.7, max_tokens: 2000, }); } setIsPresetModalVisible(true); }; const handlePresetCancel = () => { setIsPresetModalVisible(false); setEditingPreset(null); presetForm.resetFields(); // 清除预设模型列表状态 setPresetModelOptions([]); setPresetModelsFetched(false); }; // 预设编辑窗口:获取模型列表 const handleFetchPresetModels = async (silent: boolean = false) => { const apiKey = presetForm.getFieldValue('api_key'); const apiBaseUrl = presetForm.getFieldValue('api_base_url'); const provider = presetForm.getFieldValue('api_provider'); if (!apiKey || !apiBaseUrl) { if (!silent) { message.warning('请先填写 API 密钥和 API 地址'); } return; } setFetchingPresetModels(true); try { const response = await settingsApi.getAvailableModels({ api_key: apiKey, api_base_url: apiBaseUrl, provider: provider || 'openai' }); setPresetModelOptions(response.models); setPresetModelsFetched(true); if (!silent) { message.success(`成功获取 ${response.count || response.models.length} 个可用模型`); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { const errorMsg = error?.response?.data?.detail || '获取模型列表失败'; if (!silent) { message.error(errorMsg); } setPresetModelOptions([]); setPresetModelsFetched(true); } finally { setFetchingPresetModels(false); } }; // 预设编辑窗口:模型选择框获得焦点时自动获取 const handlePresetModelSelectFocus = () => { if (!presetModelsFetched && !fetchingPresetModels) { handleFetchPresetModels(true); } }; // 预设编辑窗口:提供商变更时更新默认URL并清空模型列表 const handlePresetProviderChange = (value: string) => { const provider = apiProviders.find(p => p.value === value); if (provider && provider.defaultUrl) { presetForm.setFieldValue('api_base_url', provider.defaultUrl); } // 清空模型列表,需要重新获取 setPresetModelOptions([]); setPresetModelsFetched(false); }; const handlePresetSave = async () => { try { const values = await presetForm.validateFields(); const config: APIKeyPresetConfig = { api_provider: values.api_provider, api_key: values.api_key, api_base_url: values.api_base_url, llm_model: values.llm_model, temperature: values.temperature, max_tokens: values.max_tokens, }; if (editingPreset) { await settingsApi.updatePreset(editingPreset.id, { name: values.name, description: values.description, config, }); message.success('预设已更新'); } else { const request: PresetCreateRequest = { name: values.name, description: values.description, config, }; await settingsApi.createPreset(request); message.success('预设已创建'); } handlePresetCancel(); loadPresets(); } catch (error) { console.error('保存失败:', error); } }; const handlePresetDelete = async (presetId: string) => { try { await settingsApi.deletePreset(presetId); message.success('预设已删除'); loadPresets(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { message.error(error.response?.data?.detail || '删除失败'); console.error(error); } }; const handlePresetActivate = async (presetId: string, presetName: string) => { try { // 获取预设配置用于比较 const preset = presets.find(p => p.id === presetId); await settingsApi.activatePreset(presetId); message.success(`已激活预设: ${presetName}`); // 激活预设后清除当前配置Tab的测试结果 setTestResult(null); setShowTestResult(false); // 清除模型列表缓存,因为API配置可能已变更 setModelOptions([]); setModelsFetched(false); loadPresets(); loadSettings(); // 重新加载当前配置 // 检查是否与 MCP 缓存的配置不一致 if (preset) { const verifiedConfigStr = localStorage.getItem('mcp_verified_config'); let configChanged = false; if (verifiedConfigStr) { try { const verifiedConfig = JSON.parse(verifiedConfigStr); configChanged = verifiedConfig.provider !== preset.config.api_provider || verifiedConfig.baseUrl !== preset.config.api_base_url || verifiedConfig.model !== preset.config.llm_model; } catch (e) { console.error('Failed to parse verified config:', e); configChanged = true; // 解析失败也视为配置变化 } } else { // 没有缓存的配置,如果有启用的插件也需要处理 configChanged = true; } if (configChanged) { // 清除 MCP 验证缓存 localStorage.removeItem('mcp_verified_config'); // 检查并禁用所有 MCP 插件 try { const plugins = await mcpPluginApi.getPlugins(); const activePlugins = plugins.filter(p => p.enabled); if (activePlugins.length > 0) { // 禁用所有插件 message.loading({ content: '正在禁用 MCP 插件...', key: 'disable_mcp' }); await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false))); message.success({ content: '已禁用所有 MCP 插件', key: 'disable_mcp' }); // 显示提示弹窗 modal.warning({ title: ( API 配置已更改 ), centered: true, content: (
请完成以下步骤:
  1. 前往 MCP 插件管理页面
  2. 重新进行"模型能力检查"
  3. 确认新模型支持 Function Calling 后再启用插件
), okText: '前往 MCP 页面', cancelText: '稍后处理', onOk: () => { eventBus.emit(EventNames.SWITCH_TO_MCP_VIEW); }, }); } } catch (err) { console.error('Failed to disable MCP plugins:', err); } } } } catch (error) { message.error('激活失败'); console.error(error); } }; const handlePresetTest = async (presetId: string) => { setTestingPresetId(presetId); try { const result = await settingsApi.testPreset(presetId); if (result.success) { modal.success({ title: '测试成功', centered: true, width: isMobile ? '90%' : 600, content: (
✓ API 连接正常
提供商: {result.provider?.toUpperCase() || 'N/A'}
模型: {result.model || 'N/A'}
{result.response_time_ms !== undefined && (
响应时间: {result.response_time_ms}ms
)}
), }); } else { modal.error({ title: '测试失败', centered: true, width: isMobile ? '90%' : 600, content: (
{result.error && (
错误信息: {result.error}
)} {result.suggestions && result.suggestions.length > 0 && (
💡 建议:
    {result.suggestions.map((s, i) => (
  • {s}
  • ))}
)}
), }); } } catch (error) { message.error('测试失败'); console.error(error); } finally { setTestingPresetId(null); } }; const handleCreateFromCurrent = () => { const currentConfig = form.getFieldsValue(); presetForm.setFieldsValue({ name: '', description: '', ...currentConfig, }); setEditingPreset(null); setIsPresetModalVisible(true); }; const getProviderColor = (provider: string) => { switch (provider) { case 'openai': return 'blue'; // case 'anthropic': // return 'purple'; case 'gemini': return 'green'; default: return 'default'; } }; // ========== 渲染预设列表 ========== const renderPresetsList = () => (
管理你的API配置预设,快速切换不同的配置
{presets.length === 0 ? ( ) : ( { const isActive = preset.id === activePresetId; return ( handlePresetActivate(preset.id, preset.name)} > 激活 ), , , handlePresetDelete(preset.id)} disabled={isActive} okText="确定" cancelText="取消" > , ].filter(Boolean)} > ) } title={ {preset.name} {isActive && 激活中} } description={ {preset.description && (
{preset.description}
)} {preset.config.api_provider.toUpperCase()} {preset.config.llm_model} 温度: {preset.config.temperature} Tokens: {preset.config.max_tokens}
创建于: {new Date(preset.created_at).toLocaleString()}
} />
); }} /> )}
); return ( <> {contextHolder}
{/* 顶部导航卡片 */} {/* 装饰性背景元素 */}
AI API 设置 配置AI接口参数,管理多个API配置预设 {/* 按钮区域预留 */} {/* 主内容卡片 */} {/* 默认配置提示 */} {isDefaultSettings && (

当前显示的是从服务器 .env 文件读取的默认配置。

点击"保存设置"后,配置将保存到数据库并同步更新到 .env 文件。

} type="info" showIcon style={{ marginBottom: isMobile ? 12 : 16 }} /> )} {/* 已保存配置提示 */} {hasSettings && !isDefaultSettings && ( )} {/* 表单 */}
API 提供商 } name="api_provider" rules={[{ required: true, message: '请选择API提供商' }]} > API 密钥 } name="api_key" rules={[{ required: true, message: '请输入API密钥' }]} > API 地址 } name="api_base_url" rules={[ { required: true, message: '请输入API地址' }, { type: 'url', message: '请输入有效的URL' } ]} > 模型名称 } name="llm_model" rules={[{ required: true, message: '请输入或选择模型名称' }]} >