2025-10-30 16:53:50 +08:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-12-15 15:58:57 +08:00
|
|
|
|
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd';
|
|
|
|
|
|
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
|
2025-10-30 16:53:50 +08:00
|
|
|
|
import { settingsApi } from '../services/api';
|
2025-12-15 15:58:57 +08:00
|
|
|
|
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
|
2025-10-30 16:53:50 +08:00
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
const { Title, Text } = Typography;
|
2025-10-30 16:53:50 +08:00
|
|
|
|
const { Option } = Select;
|
2025-10-31 17:23:25 +08:00
|
|
|
|
const { useBreakpoint } = Grid;
|
2025-12-15 15:58:57 +08:00
|
|
|
|
const { TextArea } = Input;
|
2025-10-30 16:53:50 +08:00
|
|
|
|
|
|
|
|
|
|
export default function SettingsPage() {
|
|
|
|
|
|
const navigate = useNavigate();
|
2025-10-31 17:23:25 +08:00
|
|
|
|
const screens = useBreakpoint();
|
|
|
|
|
|
const isMobile = !screens.md; // md断点是768px
|
2025-10-30 16:53:50 +08:00
|
|
|
|
const [form] = Form.useForm();
|
2025-12-15 15:58:57 +08:00
|
|
|
|
const [modal, contextHolder] = Modal.useModal();
|
2025-10-30 16:53:50 +08:00
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
|
|
|
|
const [hasSettings, setHasSettings] = useState(false);
|
|
|
|
|
|
const [isDefaultSettings, setIsDefaultSettings] = useState(false);
|
|
|
|
|
|
const [modelOptions, setModelOptions] = useState<Array<{ value: string; label: string; description: string }>>([]);
|
|
|
|
|
|
const [fetchingModels, setFetchingModels] = useState(false);
|
|
|
|
|
|
const [modelsFetched, setModelsFetched] = useState(false);
|
2025-11-03 15:28:51 +08:00
|
|
|
|
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);
|
2025-10-30 16:53:50 +08:00
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
// 预设相关状态
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState('current');
|
|
|
|
|
|
const [presets, setPresets] = useState<APIKeyPreset[]>([]);
|
|
|
|
|
|
const [presetsLoading, setPresetsLoading] = useState(false);
|
|
|
|
|
|
const [activePresetId, setActivePresetId] = useState<string | undefined>();
|
|
|
|
|
|
const [editingPreset, setEditingPreset] = useState<APIKeyPreset | null>(null);
|
|
|
|
|
|
const [isPresetModalVisible, setIsPresetModalVisible] = useState(false);
|
|
|
|
|
|
const [testingPresetId, setTestingPresetId] = useState<string | null>(null);
|
|
|
|
|
|
const [presetForm] = Form.useForm();
|
|
|
|
|
|
|
2025-10-30 16:53:50 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadSettings();
|
2025-12-15 15:58:57 +08:00
|
|
|
|
if (activeTab === 'presets') {
|
|
|
|
|
|
loadPresets();
|
|
|
|
|
|
}
|
2025-10-30 16:53:50 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (activeTab === 'presets') {
|
|
|
|
|
|
loadPresets();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeTab]);
|
|
|
|
|
|
|
2025-10-30 16:53:50 +08:00
|
|
|
|
const loadSettings = async () => {
|
|
|
|
|
|
setInitialLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const settings = await settingsApi.getSettings();
|
|
|
|
|
|
form.setFieldsValue(settings);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-10-30 16:53:50 +08:00
|
|
|
|
// 判断是否为默认设置(id='0'表示来自.env的默认配置)
|
|
|
|
|
|
if (settings.id === '0' || !settings.id) {
|
|
|
|
|
|
setIsDefaultSettings(true);
|
|
|
|
|
|
setHasSettings(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setIsDefaultSettings(false);
|
|
|
|
|
|
setHasSettings(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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',
|
2025-11-04 17:27:39 +08:00
|
|
|
|
llm_model: 'gpt-4',
|
2025-10-30 16:53:50 +08:00
|
|
|
|
temperature: 0.7,
|
|
|
|
|
|
max_tokens: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('加载设置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setInitialLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = async (values: SettingsUpdate) => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await settingsApi.saveSettings(values);
|
|
|
|
|
|
message.success('设置已保存');
|
|
|
|
|
|
setHasSettings(true);
|
|
|
|
|
|
setIsDefaultSettings(false);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('保存设置失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
2025-12-30 10:05:34 +08:00
|
|
|
|
modal.confirm({
|
2025-10-30 16:53:50 +08:00
|
|
|
|
title: '重置设置',
|
|
|
|
|
|
content: '确定要重置为默认值吗?',
|
2025-12-30 10:05:34 +08:00
|
|
|
|
centered: true,
|
2025-10-30 16:53:50 +08:00
|
|
|
|
okText: '确定',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: () => {
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
api_provider: 'openai',
|
|
|
|
|
|
api_key: '',
|
|
|
|
|
|
api_base_url: 'https://api.openai.com/v1',
|
2025-11-04 17:27:39 +08:00
|
|
|
|
llm_model: 'gpt-4',
|
2025-10-30 16:53:50 +08:00
|
|
|
|
temperature: 0.7,
|
|
|
|
|
|
max_tokens: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
message.info('已重置为默认值,请点击保存');
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = () => {
|
2025-12-30 10:05:34 +08:00
|
|
|
|
modal.confirm({
|
2025-10-30 16:53:50 +08:00
|
|
|
|
title: '删除设置',
|
|
|
|
|
|
content: '确定要删除所有设置吗?此操作不可恢复。',
|
2025-12-30 10:05:34 +08:00
|
|
|
|
centered: true,
|
2025-10-30 16:53:50 +08:00
|
|
|
|
okText: '确定',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await settingsApi.deleteSettings();
|
|
|
|
|
|
message.success('设置已删除');
|
|
|
|
|
|
setHasSettings(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('删除设置失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const apiProviders = [
|
2025-12-28 19:35:23 +08:00
|
|
|
|
{ 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' },
|
2025-10-30 16:53:50 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
|
});
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-10-30 16:53:50 +08:00
|
|
|
|
setModelOptions(response.models);
|
|
|
|
|
|
setModelsFetched(true);
|
|
|
|
|
|
if (!silent) {
|
|
|
|
|
|
message.success(`成功获取 ${response.count || response.models.length} 个可用模型`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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模式,不显示成功消息
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-03 15:28:51 +08:00
|
|
|
|
const handleTestConnection = async () => {
|
|
|
|
|
|
const apiKey = form.getFieldValue('api_key');
|
|
|
|
|
|
const apiBaseUrl = form.getFieldValue('api_base_url');
|
|
|
|
|
|
const provider = form.getFieldValue('api_provider');
|
2025-11-04 17:27:39 +08:00
|
|
|
|
const modelName = form.getFieldValue('llm_model');
|
2025-11-03 15:28:51 +08:00
|
|
|
|
|
|
|
|
|
|
if (!apiKey || !apiBaseUrl || !provider || !modelName) {
|
|
|
|
|
|
message.warning('请先填写完整的配置信息');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTestingApi(true);
|
|
|
|
|
|
setTestResult(null);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-11-03 15:28:51 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const result = await settingsApi.testApiConnection({
|
|
|
|
|
|
api_key: apiKey,
|
|
|
|
|
|
api_base_url: apiBaseUrl,
|
|
|
|
|
|
provider: provider,
|
2025-11-04 17:27:39 +08:00
|
|
|
|
llm_model: modelName
|
2025-11-03 15:28:51 +08:00
|
|
|
|
});
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-11-03 15:28:51 +08:00
|
|
|
|
setTestResult(result);
|
|
|
|
|
|
setShowTestResult(true);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-11-03 15:28:51 +08:00
|
|
|
|
if (result.success) {
|
|
|
|
|
|
message.success(`测试成功!响应时间: ${result.response_time_ms}ms`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('API 测试失败,请查看详细信息');
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
// ========== 预设管理函数 ==========
|
2025-10-30 16:53:50 +08:00
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-30 16:53:50 +08:00
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
const showPresetModal = (preset?: APIKeyPreset) => {
|
|
|
|
|
|
if (preset) {
|
|
|
|
|
|
setEditingPreset(preset);
|
|
|
|
|
|
presetForm.setFieldsValue({
|
|
|
|
|
|
name: preset.name,
|
|
|
|
|
|
description: preset.description,
|
|
|
|
|
|
...preset.config,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setEditingPreset(null);
|
|
|
|
|
|
presetForm.resetFields();
|
|
|
|
|
|
presetForm.setFieldsValue({
|
|
|
|
|
|
api_provider: 'openai',
|
|
|
|
|
|
temperature: 0.7,
|
|
|
|
|
|
max_tokens: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
setIsPresetModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePresetCancel = () => {
|
|
|
|
|
|
setIsPresetModalVisible(false);
|
|
|
|
|
|
setEditingPreset(null);
|
|
|
|
|
|
presetForm.resetFields();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.response?.data?.detail || '删除失败');
|
|
|
|
|
|
console.error(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePresetActivate = async (presetId: string, presetName: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await settingsApi.activatePreset(presetId);
|
|
|
|
|
|
message.success(`已激活预设: ${presetName}`);
|
|
|
|
|
|
loadPresets();
|
|
|
|
|
|
loadSettings(); // 重新加载当前配置
|
|
|
|
|
|
} 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: (
|
|
|
|
|
|
<div style={{ padding: '8px 0' }}>
|
|
|
|
|
|
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
|
|
|
|
|
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
|
|
|
|
|
|
✓ API 连接正常
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
padding: 16,
|
|
|
|
|
|
background: 'var(--color-bg-layout)',
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
marginBottom: 16
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ marginBottom: 8, fontSize: 14 }}>
|
|
|
|
|
|
<Text type="secondary">提供商:</Text>
|
|
|
|
|
|
<Text strong>{result.provider?.toUpperCase() || 'N/A'}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ marginBottom: 8, fontSize: 14 }}>
|
|
|
|
|
|
<Text type="secondary">模型:</Text>
|
|
|
|
|
|
<Text strong>{result.model || 'N/A'}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{result.response_time_ms !== undefined && (
|
|
|
|
|
|
<div style={{ fontSize: 14 }}>
|
|
|
|
|
|
<Text type="secondary">响应时间:</Text>
|
|
|
|
|
|
<Text strong>{result.response_time_ms}ms</Text>
|
2025-10-30 16:53:50 +08:00
|
|
|
|
</div>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-30 16:53:50 +08:00
|
|
|
|
|
|
|
|
|
|
<Alert
|
2025-12-15 15:58:57 +08:00
|
|
|
|
message="预设配置测试通过,可以正常使用"
|
2025-10-30 16:53:50 +08:00
|
|
|
|
type="success"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
/>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
modal.error({
|
|
|
|
|
|
title: '测试失败',
|
|
|
|
|
|
centered: true,
|
|
|
|
|
|
width: isMobile ? '90%' : 600,
|
|
|
|
|
|
content: (
|
|
|
|
|
|
<div style={{ padding: '8px 0' }}>
|
|
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={result.message || 'API 测试失败'}
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{result.error && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
padding: 16,
|
|
|
|
|
|
background: 'var(--color-error-bg)',
|
|
|
|
|
|
border: '1px solid var(--color-error-border)',
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
marginBottom: 16
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
|
|
|
|
|
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
|
|
|
|
{result.error}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{result.suggestions && result.suggestions.length > 0 && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
padding: 16,
|
|
|
|
|
|
background: 'var(--color-warning-bg)',
|
|
|
|
|
|
border: '1px solid var(--color-warning-border)',
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
marginBottom: 16
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
|
|
|
|
|
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
|
|
|
|
|
|
{result.suggestions.map((s, i) => (
|
|
|
|
|
|
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
|
2025-10-30 16:53:50 +08:00
|
|
|
|
))}
|
2025-12-15 15:58:57 +08:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="预设配置存在问题,请检查后重试"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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';
|
2025-12-28 19:35:23 +08:00
|
|
|
|
// case 'anthropic':
|
|
|
|
|
|
// return 'purple';
|
2025-12-15 15:58:57 +08:00
|
|
|
|
case 'gemini':
|
|
|
|
|
|
return 'green';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 渲染预设列表 ==========
|
|
|
|
|
|
|
|
|
|
|
|
const renderPresetsList = () => (
|
|
|
|
|
|
<Spin spinning={presetsLoading}>
|
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
|
<Text type="secondary">管理你的API配置预设,快速切换不同的配置</Text>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button icon={<CopyOutlined />} onClick={handleCreateFromCurrent}>
|
|
|
|
|
|
从当前创建
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => showPresetModal()}>
|
|
|
|
|
|
新建预设
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{presets.length === 0 ? (
|
|
|
|
|
|
<Empty
|
|
|
|
|
|
description="暂无预设配置"
|
|
|
|
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
|
|
|
|
style={{ margin: '40px 0' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => showPresetModal()}>
|
|
|
|
|
|
创建第一个预设
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Empty>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<List
|
|
|
|
|
|
dataSource={presets}
|
|
|
|
|
|
renderItem={(preset) => {
|
|
|
|
|
|
const isActive = preset.id === activePresetId;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<List.Item
|
|
|
|
|
|
key={preset.id}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: isActive ? '#f0f5ff' : 'transparent',
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
marginBottom: '8px',
|
|
|
|
|
|
border: isActive ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
actions={[
|
|
|
|
|
|
!isActive && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
onClick={() => handlePresetActivate(preset.id, preset.name)}
|
|
|
|
|
|
>
|
|
|
|
|
|
激活
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
<Tooltip title="测试连接">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
icon={<ThunderboltOutlined />}
|
|
|
|
|
|
loading={testingPresetId === preset.id}
|
|
|
|
|
|
onClick={() => handlePresetTest(preset.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
测试
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Tooltip>,
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => showPresetModal(preset)}
|
|
|
|
|
|
>
|
|
|
|
|
|
编辑
|
|
|
|
|
|
</Button>,
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定删除此预设吗?"
|
|
|
|
|
|
onConfirm={() => handlePresetDelete(preset.id)}
|
|
|
|
|
|
disabled={isActive}
|
|
|
|
|
|
okText="确定"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
disabled={isActive}
|
|
|
|
|
|
>
|
|
|
|
|
|
删除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Popconfirm>,
|
|
|
|
|
|
].filter(Boolean)}
|
2025-10-30 16:53:50 +08:00
|
|
|
|
>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<List.Item.Meta
|
|
|
|
|
|
avatar={
|
|
|
|
|
|
isActive && (
|
|
|
|
|
|
<CheckCircleOutlined
|
|
|
|
|
|
style={{ fontSize: '24px', color: '#52c41a' }}
|
|
|
|
|
|
/>
|
2025-10-30 16:53:50 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-15 15:58:57 +08:00
|
|
|
|
title={
|
2025-11-03 15:28:51 +08:00
|
|
|
|
<Space>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<span style={{ fontWeight: 'bold' }}>{preset.name}</span>
|
|
|
|
|
|
{isActive && <Tag color="success">激活中</Tag>}
|
2025-11-03 15:28:51 +08:00
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
|
|
|
|
|
{preset.description && (
|
|
|
|
|
|
<div style={{ color: '#666' }}>{preset.description}</div>
|
2025-11-03 15:28:51 +08:00
|
|
|
|
)}
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<Space wrap>
|
|
|
|
|
|
<Tag color={getProviderColor(preset.config.api_provider)}>
|
|
|
|
|
|
{preset.config.api_provider.toUpperCase()}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
<Tag>{preset.config.llm_model}</Tag>
|
|
|
|
|
|
<Tag>温度: {preset.config.temperature}</Tag>
|
|
|
|
|
|
<Tag>Tokens: {preset.config.max_tokens}</Tag>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: '#999' }}>
|
|
|
|
|
|
创建于: {new Date(preset.created_at).toLocaleString()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
2025-11-03 15:28:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
</List.Item>
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Spin>
|
|
|
|
|
|
);
|
2025-11-03 15:28:51 +08:00
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{contextHolder}
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
|
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
|
|
|
|
|
padding: isMobile ? '20px 16px' : '40px 24px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
maxWidth: 1400,
|
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{/* 顶部导航卡片 */}
|
|
|
|
|
|
<Card
|
|
|
|
|
|
variant="borderless"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
|
|
|
|
|
borderRadius: isMobile ? 16 : 24,
|
|
|
|
|
|
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
|
|
|
|
|
marginBottom: isMobile ? 20 : 24,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
position: 'relative',
|
|
|
|
|
|
overflow: 'hidden'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 装饰性背景元素 */}
|
|
|
|
|
|
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
|
|
|
|
|
|
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
|
|
|
|
|
|
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
|
|
|
|
|
|
|
|
|
|
|
|
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
|
|
|
|
|
<Col xs={24} sm={12}>
|
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
|
|
|
|
|
<SettingOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} />
|
|
|
|
|
|
AI API 设置
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
|
|
|
|
|
配置AI接口参数,管理多个API配置预设
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={12}>
|
|
|
|
|
|
<Space size={12} style={{ display: 'flex', justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ArrowLeftOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.15)',
|
|
|
|
|
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
backdropFilter: 'blur(10px)',
|
|
|
|
|
|
transition: 'all 0.3s ease'
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={(e) => {
|
|
|
|
|
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
|
|
|
|
|
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={(e) => {
|
|
|
|
|
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
|
|
|
|
|
|
e.currentTarget.style.transform = 'none';
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
返回主页
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 主内容卡片 */}
|
|
|
|
|
|
<Card
|
|
|
|
|
|
variant="borderless"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.95)',
|
|
|
|
|
|
borderRadius: isMobile ? 12 : 16,
|
|
|
|
|
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
}}
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
body: {
|
|
|
|
|
|
padding: isMobile ? '16px' : '24px'
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tabs
|
|
|
|
|
|
activeKey={activeTab}
|
|
|
|
|
|
onChange={setActiveTab}
|
|
|
|
|
|
items={[
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'current',
|
|
|
|
|
|
label: '当前配置',
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<Space direction="vertical" size={isMobile ? 'middle' : 'large'} style={{ width: '100%' }}>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 默认配置提示 */}
|
|
|
|
|
|
{isDefaultSettings && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="使用 .env 文件中的默认配置"
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
<p style={{ margin: '8px 0' }}>
|
|
|
|
|
|
当前显示的是从服务器 <code>.env</code> 文件读取的默认配置。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p style={{ margin: '8px 0 0 0' }}>
|
|
|
|
|
|
点击"保存设置"后,配置将保存到数据库并同步更新到 <code>.env</code> 文件。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginBottom: isMobile ? 12 : 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 已保存配置提示 */}
|
|
|
|
|
|
{hasSettings && !isDefaultSettings && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="使用已保存的个人配置"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginBottom: isMobile ? 12 : 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 表单 */}
|
|
|
|
|
|
<Spin spinning={initialLoading}>
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
onFinish={handleSave}
|
|
|
|
|
|
autoComplete="off"
|
2025-10-31 17:23:25 +08:00
|
|
|
|
>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>API 提供商</span>
|
|
|
|
|
|
<Tooltip title="选择你的AI服务提供商">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="api_provider"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择API提供商' }]}
|
2025-10-31 17:23:25 +08:00
|
|
|
|
>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<Select size={isMobile ? 'middle' : 'large'} onChange={handleProviderChange}>
|
|
|
|
|
|
{apiProviders.map(provider => (
|
|
|
|
|
|
<Option key={provider.value} value={provider.value}>
|
|
|
|
|
|
{provider.label}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>API 密钥</span>
|
|
|
|
|
|
<Tooltip title="你的API密钥,将加密存储">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="api_key"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入API密钥' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input.Password
|
|
|
|
|
|
size={isMobile ? 'middle' : 'large'}
|
|
|
|
|
|
placeholder="sk-..."
|
|
|
|
|
|
autoComplete="new-password"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>API 地址</span>
|
|
|
|
|
|
<Tooltip title="API的基础URL地址">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="api_base_url"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入API地址' },
|
|
|
|
|
|
{ type: 'url', message: '请输入有效的URL' }
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
size={isMobile ? 'middle' : 'large'}
|
|
|
|
|
|
placeholder="https://api.openai.com/v1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>模型名称</span>
|
|
|
|
|
|
<Tooltip title="AI模型的名称,如 gpt-4, gpt-3.5-turbo">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="llm_model"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入或选择模型名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
size={isMobile ? 'middle' : 'large'}
|
|
|
|
|
|
showSearch
|
|
|
|
|
|
placeholder={isMobile ? "选择模型" : "输入模型名称或点击获取"}
|
|
|
|
|
|
optionFilterProp="label"
|
|
|
|
|
|
loading={fetchingModels}
|
|
|
|
|
|
onFocus={handleModelSelectFocus}
|
|
|
|
|
|
filterOption={(input, option) =>
|
|
|
|
|
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
|
|
|
|
|
|
(option?.description ?? '').toLowerCase().includes(input.toLowerCase())
|
|
|
|
|
|
}
|
|
|
|
|
|
dropdownRender={(menu) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{menu}
|
|
|
|
|
|
{fetchingModels && (
|
|
|
|
|
|
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
<Spin size="small" /> 正在获取模型列表...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!fetchingModels && modelOptions.length === 0 && modelsFetched && (
|
|
|
|
|
|
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
未能获取到模型列表,请检查 API 配置
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && (
|
|
|
|
|
|
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
点击输入框自动获取模型列表
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
notFoundContent={
|
|
|
|
|
|
fetchingModels ? (
|
|
|
|
|
|
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
<Spin size="small" /> 加载中...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
未找到匹配的模型
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
suffixIcon={
|
|
|
|
|
|
!isMobile ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
if (!fetchingModels) {
|
|
|
|
|
|
setModelsFetched(false);
|
|
|
|
|
|
handleFetchModels(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
cursor: fetchingModels ? 'not-allowed' : 'pointer',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
padding: '0 4px',
|
|
|
|
|
|
height: '100%',
|
|
|
|
|
|
marginRight: -8
|
|
|
|
|
|
}}
|
|
|
|
|
|
title="重新获取模型列表"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
loading={fetchingModels}
|
|
|
|
|
|
style={{ pointerEvents: 'none' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
options={modelOptions.map(model => ({
|
|
|
|
|
|
value: model.value,
|
|
|
|
|
|
label: model.label,
|
|
|
|
|
|
description: model.description
|
|
|
|
|
|
}))}
|
|
|
|
|
|
optionRender={(option) => (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>{option.data.label}</div>
|
|
|
|
|
|
{option.data.description && (
|
|
|
|
|
|
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
|
|
|
|
|
|
{option.data.description}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>温度参数</span>
|
|
|
|
|
|
<Tooltip title="控制输出的随机性,值越高越随机(0.0-2.0)">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="temperature"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Slider
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
max={2}
|
|
|
|
|
|
step={0.1}
|
|
|
|
|
|
marks={{
|
|
|
|
|
|
0: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '0.0' },
|
|
|
|
|
|
0.7: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '0.7' },
|
|
|
|
|
|
1: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '1.0' },
|
|
|
|
|
|
2: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '2.0' }
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>最大 Token 数</span>
|
|
|
|
|
|
<Tooltip title="单次请求的最大token数量">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="max_tokens"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入最大token数' },
|
|
|
|
|
|
{ type: 'number', min: 1, message: '请输入大于0的数字' }
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
size={isMobile ? 'middle' : 'large'}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
placeholder="2000"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2025-12-28 19:35:23 +08:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={
|
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
|
<span>系统提示词</span>
|
|
|
|
|
|
<Tooltip title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
name="system_prompt"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={4}
|
|
|
|
|
|
placeholder="例如:你是一个专业的小说创作助手,请用生动、细腻的文字进行创作..."
|
|
|
|
|
|
maxLength={10000}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
style={{ fontSize: isMobile ? '13px' : '14px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
{/* 测试结果展示 */}
|
|
|
|
|
|
{showTestResult && testResult && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
{testResult.success ? (
|
|
|
|
|
|
<CheckCircleOutlined style={{ color: 'var(--color-success)', fontSize: isMobile ? '16px' : '18px' }} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<CloseCircleOutlined style={{ color: 'var(--color-error)', fontSize: isMobile ? '16px' : '18px' }} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
|
|
|
|
|
|
{testResult.message}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
|
{testResult.success ? (
|
|
|
|
|
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
|
|
|
|
|
{testResult.response_time_ms && (
|
|
|
|
|
|
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
|
|
|
|
|
|
⚡ 响应时间: <strong>{testResult.response_time_ms} ms</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{testResult.response_preview && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
fontSize: isMobile ? '12px' : '13px',
|
|
|
|
|
|
padding: '8px 12px',
|
|
|
|
|
|
background: '#f6ffed',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
border: '1px solid #b7eb8f',
|
|
|
|
|
|
marginTop: '8px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ marginBottom: '4px', fontWeight: 500 }}>AI 响应预览:</div>
|
|
|
|
|
|
<div style={{ color: '#595959' }}>{testResult.response_preview}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div style={{ color: 'var(--color-success)', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
|
|
|
|
|
|
✓ API 配置正确,可以正常使用
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
|
|
|
|
|
{testResult.error && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
fontSize: isMobile ? '12px' : '13px',
|
|
|
|
|
|
padding: '8px 12px',
|
|
|
|
|
|
background: '#fff2e8',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
border: '1px solid #ffbb96',
|
|
|
|
|
|
color: '#d4380d'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<strong>错误信息:</strong> {testResult.error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{testResult.error_type && (
|
|
|
|
|
|
<div style={{ fontSize: isMobile ? '11px' : '12px', color: 'var(--color-text-secondary)' }}>
|
|
|
|
|
|
错误类型: {testResult.error_type}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{testResult.suggestions && testResult.suggestions.length > 0 && (
|
|
|
|
|
|
<div style={{ marginTop: '8px' }}>
|
|
|
|
|
|
<div style={{ fontSize: isMobile ? '12px' : '13px', fontWeight: 500, marginBottom: '4px' }}>
|
|
|
|
|
|
💡 解决建议:
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ul style={{
|
|
|
|
|
|
margin: 0,
|
|
|
|
|
|
paddingLeft: isMobile ? '16px' : '20px',
|
|
|
|
|
|
fontSize: isMobile ? '12px' : '13px',
|
|
|
|
|
|
color: '#595959'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{testResult.suggestions.map((suggestion, index) => (
|
|
|
|
|
|
<li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
type={testResult.success ? 'success' : 'error'}
|
|
|
|
|
|
closable
|
|
|
|
|
|
onClose={() => setShowTestResult(false)}
|
|
|
|
|
|
style={{ marginBottom: isMobile ? 16 : 24 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 操作按钮 */}
|
|
|
|
|
|
<Form.Item style={{ marginBottom: 0, marginTop: isMobile ? 24 : 32 }}>
|
|
|
|
|
|
{isMobile ? (
|
|
|
|
|
|
// 移动端:垂直堆叠布局
|
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<SaveOutlined />}
|
|
|
|
|
|
htmlType="submit"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
block
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--color-primary)',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
height: '44px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
保存设置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<ThunderboltOutlined />}
|
|
|
|
|
|
onClick={handleTestConnection}
|
|
|
|
|
|
loading={testingApi}
|
|
|
|
|
|
block
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderColor: 'var(--color-success)',
|
|
|
|
|
|
color: 'var(--color-success)',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
height: '44px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{testingApi ? '测试中...' : '测试连接'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Space size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
|
style={{ flex: 1, height: '44px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
{hasSettings && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
style={{ flex: 1, height: '44px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
删除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// 桌面端:删除在左边,测试、重置和保存在右边
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '16px',
|
|
|
|
|
|
flexWrap: 'wrap'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{/* 左侧:删除按钮 */}
|
|
|
|
|
|
{hasSettings ? (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
minWidth: '100px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
删除配置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div /> // 占位符,保持右侧按钮位置
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 右侧:测试、重置和保存按钮组 */}
|
|
|
|
|
|
<Space size="middle">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<ThunderboltOutlined />}
|
|
|
|
|
|
onClick={handleTestConnection}
|
|
|
|
|
|
loading={testingApi}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderColor: 'var(--color-success)',
|
|
|
|
|
|
color: 'var(--color-success)',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
minWidth: '100px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{testingApi ? '测试中...' : '测试'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
minWidth: '100px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<SaveOutlined />}
|
|
|
|
|
|
htmlType="submit"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--color-primary)',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
minWidth: '120px',
|
|
|
|
|
|
fontWeight: 500
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Spin>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
</Space>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'presets',
|
|
|
|
|
|
label: '配置预设',
|
|
|
|
|
|
children: renderPresetsList(),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
2025-12-15 15:58:57 +08:00
|
|
|
|
{/* 预设编辑对话框 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={editingPreset ? '编辑预设' : '创建预设'}
|
|
|
|
|
|
open={isPresetModalVisible}
|
|
|
|
|
|
onOk={handlePresetSave}
|
|
|
|
|
|
onCancel={handlePresetCancel}
|
|
|
|
|
|
width={isMobile ? '90%' : 600}
|
|
|
|
|
|
centered
|
|
|
|
|
|
okText="保存"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={presetForm}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
label="预设名称"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入预设名称' },
|
|
|
|
|
|
{ max: 50, message: '名称不能超过50个字符' },
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="例如:工作账号-GPT4" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="description"
|
|
|
|
|
|
label="预设描述"
|
|
|
|
|
|
rules={[{ max: 200, message: '描述不能超过200个字符' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<TextArea rows={2} placeholder="例如:用于日常写作任务(可选)" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="api_provider"
|
|
|
|
|
|
label="API 提供商"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择API提供商' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select>
|
|
|
|
|
|
<Select.Option value="openai">OpenAI</Select.Option>
|
2025-12-28 19:35:23 +08:00
|
|
|
|
{/* <Select.Option value="anthropic">Anthropic (Claude)</Select.Option> */}
|
2025-12-15 15:58:57 +08:00
|
|
|
|
<Select.Option value="gemini">Google Gemini</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="api_key"
|
|
|
|
|
|
label="API Key"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入API Key' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input.Password placeholder="sk-..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="api_base_url" label="API Base URL">
|
|
|
|
|
|
<Input placeholder="https://api.openai.com/v1(可选)" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="llm_model"
|
|
|
|
|
|
label="模型名称"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入模型名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="例如:gpt-4, claude-3-opus-20240229" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="temperature"
|
|
|
|
|
|
label="温度参数"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入温度参数' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
max={2}
|
|
|
|
|
|
step={0.1}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
placeholder="0.7"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="max_tokens"
|
|
|
|
|
|
label="最大 Tokens"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入最大tokens' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={100000}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
placeholder="2000"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
2025-12-28 19:35:23 +08:00
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="system_prompt"
|
|
|
|
|
|
label="系统提示词"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
placeholder="例如:你是一个专业的小说创作助手...(可选)"
|
|
|
|
|
|
maxLength={10000}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
2025-10-30 16:53:50 +08:00
|
|
|
|
</div>
|
2025-12-15 15:58:57 +08:00
|
|
|
|
</>
|
2025-10-30 16:53:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|