feat(i18n): add internationalization support across multiple components
This commit is contained in:
@@ -4,6 +4,8 @@ import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
|||||||
import { BaseBreadcrumbs, type BreadcrumbItem } from '@/components/Breadcrumbs';
|
import { BaseBreadcrumbs, type BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
import SettingSidebar from './SettingSidebar';
|
import SettingSidebar from './SettingSidebar';
|
||||||
import { Home as HomeIcon } from '@mui/icons-material';
|
import { Home as HomeIcon } from '@mui/icons-material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
|
|
||||||
const LayoutContainer = styled(Box)({
|
const LayoutContainer = styled(Box)({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -21,6 +23,9 @@ const HeaderContainer = styled(Box)(({ theme }) => ({
|
|||||||
padding: theme.spacing(2, 3),
|
padding: theme.spacing(2, 3),
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const MainContent = styled(Box)({
|
const MainContent = styled(Box)({
|
||||||
@@ -29,23 +34,28 @@ const MainContent = styled(Box)({
|
|||||||
padding: '24px',
|
padding: '24px',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置页面路径映射
|
|
||||||
const settingPathMap: Record<string, string> = {
|
|
||||||
'/setting/profile': '个人资料',
|
|
||||||
'/setting/models': '模型配置',
|
|
||||||
'/setting/system': '系统设置',
|
|
||||||
'/setting/teams': '团队管理',
|
|
||||||
'/setting/mcp': 'MCP配置',
|
|
||||||
};
|
|
||||||
|
|
||||||
function SettingHeader() {
|
function SettingHeader() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
|
// 设置页面路径映射
|
||||||
|
const settingPathMap: Record<string, string> = {
|
||||||
|
'/setting/profile': t('setting.profile'),
|
||||||
|
'/setting/models': t('setting.model'),
|
||||||
|
'/setting/system': t('setting.system'),
|
||||||
|
'/setting/teams': t('setting.team'),
|
||||||
|
'/setting/mcp': t('setting.mcp'),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// 生成面包屑导航
|
// 生成面包屑导航
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
{
|
{
|
||||||
label: '首页',
|
label: t('header.home'),
|
||||||
path: '/',
|
path: '/',
|
||||||
onClick: () => navigate('/'),
|
onClick: () => navigate('/'),
|
||||||
},
|
},
|
||||||
@@ -65,6 +75,7 @@ function SettingHeader() {
|
|||||||
items={breadcrumbItems}
|
items={breadcrumbItems}
|
||||||
sx={{ mb: 0 }}
|
sx={{ mb: 0 }}
|
||||||
/>
|
/>
|
||||||
|
<LanguageSwitcher textColor='text.primary' />
|
||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
SmartToy as SmartToyIcon,
|
SmartToy as SmartToyIcon,
|
||||||
Extension as ExtensionIcon,
|
Extension as ExtensionIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
interface SettingMenuItem {
|
interface SettingMenuItem {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -16,40 +18,43 @@ interface SettingMenuItem {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingMenuItems: SettingMenuItem[] = [
|
|
||||||
{
|
|
||||||
key: 'profile',
|
|
||||||
label: '个人资料',
|
|
||||||
icon: PersonIcon,
|
|
||||||
path: '/setting/profile',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'models',
|
|
||||||
label: '模型配置',
|
|
||||||
icon: SmartToyIcon,
|
|
||||||
path: '/setting/models',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'system',
|
|
||||||
label: '系统设置',
|
|
||||||
icon: ComputerIcon,
|
|
||||||
path: '/setting/system',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'teams',
|
|
||||||
label: '团队管理',
|
|
||||||
icon: GroupIcon,
|
|
||||||
path: '/setting/teams',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'mcp',
|
|
||||||
label: 'MCP配置',
|
|
||||||
icon: ExtensionIcon,
|
|
||||||
path: '/setting/mcp',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const SettingSidebar: React.FC = () => {
|
const SettingSidebar: React.FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const settingMenuItems: SettingMenuItem[] = [
|
||||||
|
{
|
||||||
|
key: 'profile',
|
||||||
|
label: t('setting.profile'),
|
||||||
|
icon: PersonIcon,
|
||||||
|
path: '/setting/profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'models',
|
||||||
|
label: t('setting.model'),
|
||||||
|
icon: SmartToyIcon,
|
||||||
|
path: '/setting/models',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system',
|
||||||
|
label: t('setting.system'),
|
||||||
|
icon: ComputerIcon,
|
||||||
|
path: '/setting/system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'teams',
|
||||||
|
label: t('setting.team'),
|
||||||
|
icon: GroupIcon,
|
||||||
|
path: '/setting/teams',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mcp',
|
||||||
|
label: t('setting.mcp'),
|
||||||
|
icon: ExtensionIcon,
|
||||||
|
path: '/setting/mcp',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -79,14 +84,14 @@ const SettingSidebar: React.FC = () => {
|
|||||||
letterSpacing: '0.5px',
|
letterSpacing: '0.5px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
设置
|
{t('header.setting')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{settingMenuItems.map((item) => {
|
{settingMenuItems.map((item) => {
|
||||||
const IconComponent = item.icon;
|
const IconComponent = item.icon;
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -106,19 +111,19 @@ const SettingSidebar: React.FC = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
sx={{
|
sx={{
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
marginRight: '12px',
|
marginRight: '12px',
|
||||||
fontSize: '1.2rem'
|
fontSize: '1.2rem'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ListItemText primary={item.label} />
|
<ListItemText primary={item.label} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
|
|||||||
1001
src/locales/en.ts
1001
src/locales/en.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import {
|
|||||||
Alert
|
Alert
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Visibility, VisibilityOff, Close } from '@mui/icons-material';
|
import { Visibility, VisibilityOff, Close } from '@mui/icons-material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSnackbar } from '@/hooks/useSnackbar';
|
import { useSnackbar } from '@/hooks/useSnackbar';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ interface PasswordFormData {
|
|||||||
* 修改密码对话框
|
* 修改密码对话框
|
||||||
*/
|
*/
|
||||||
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) {
|
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { showMessage } = useSnackbar();
|
const { showMessage } = useSnackbar();
|
||||||
|
|
||||||
const [formData, setFormData] = useState<PasswordFormData>({
|
const [formData, setFormData] = useState<PasswordFormData>({
|
||||||
@@ -100,23 +102,23 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
const newErrors: Partial<PasswordFormData> = {};
|
const newErrors: Partial<PasswordFormData> = {};
|
||||||
|
|
||||||
if (!formData.currentPassword.trim()) {
|
if (!formData.currentPassword.trim()) {
|
||||||
newErrors.currentPassword = '请输入当前密码';
|
newErrors.currentPassword = t('setting.currentPasswordRequired');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.newPassword.trim()) {
|
if (!formData.newPassword.trim()) {
|
||||||
newErrors.newPassword = '请输入新密码';
|
newErrors.newPassword = t('setting.newPasswordRequired');
|
||||||
} else if (formData.newPassword.length < 6) {
|
} else if (formData.newPassword.length < 6) {
|
||||||
newErrors.newPassword = '新密码长度至少6位';
|
newErrors.newPassword = t('setting.passwordMinLength');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.confirmPassword.trim()) {
|
if (!formData.confirmPassword.trim()) {
|
||||||
newErrors.confirmPassword = '请确认新密码';
|
newErrors.confirmPassword = t('setting.confirmPasswordRequired');
|
||||||
} else if (formData.newPassword !== formData.confirmPassword) {
|
} else if (formData.newPassword !== formData.confirmPassword) {
|
||||||
newErrors.confirmPassword = '两次输入的密码不一致';
|
newErrors.confirmPassword = t('setting.passwordMismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.currentPassword === formData.newPassword) {
|
if (formData.currentPassword === formData.newPassword) {
|
||||||
newErrors.newPassword = '新密码不能与当前密码相同';
|
newErrors.newPassword = t('setting.newPasswordSameAsCurrent');
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
@@ -136,10 +138,15 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
new_password: formData.newPassword
|
new_password: formData.newPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
showMessage.success('密码修改成功');
|
showMessage.success(t('setting.passwordChangeSuccess'));
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('修改密码失败:', error);
|
logger.error('修改密码失败:', error);
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
showMessage.error(t('setting.currentPasswordIncorrect'));
|
||||||
|
} else {
|
||||||
|
showMessage.error(t('setting.passwordChangeError'));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -157,7 +164,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
>
|
>
|
||||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Typography variant="h6" component="div">
|
<Typography variant="h6" component="div">
|
||||||
修改密码
|
{t('setting.changePassword')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
@@ -171,13 +178,17 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
|
||||||
<Alert severity="info" sx={{ mb: 1 }}>
|
<Alert severity="info" sx={{ mb: 1 }}>
|
||||||
为了您的账户安全,请设置一个强密码。密码长度至少6位,建议包含字母、数字和特殊字符。
|
{t('setting.passwordSecurityTip')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
{t('setting.passwordUpdateTip')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{/* 当前密码 */}
|
{/* 当前密码 */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="当前密码"
|
label={t('setting.currentPassword')}
|
||||||
type={showPasswords.current ? 'text' : 'password'}
|
type={showPasswords.current ? 'text' : 'password'}
|
||||||
value={formData.currentPassword}
|
value={formData.currentPassword}
|
||||||
onChange={handleInputChange('currentPassword')}
|
onChange={handleInputChange('currentPassword')}
|
||||||
@@ -201,7 +212,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
{/* 新密码 */}
|
{/* 新密码 */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="新密码"
|
label={t('setting.newPassword')}
|
||||||
type={showPasswords.new ? 'text' : 'password'}
|
type={showPasswords.new ? 'text' : 'password'}
|
||||||
value={formData.newPassword}
|
value={formData.newPassword}
|
||||||
onChange={handleInputChange('newPassword')}
|
onChange={handleInputChange('newPassword')}
|
||||||
@@ -225,7 +236,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
{/* 确认新密码 */}
|
{/* 确认新密码 */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="确认新密码"
|
label={t('setting.confirmNewPassword')}
|
||||||
type={showPasswords.confirm ? 'text' : 'password'}
|
type={showPasswords.confirm ? 'text' : 'password'}
|
||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={handleInputChange('confirmPassword')}
|
onChange={handleInputChange('confirmPassword')}
|
||||||
@@ -254,7 +265,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
取消
|
{t('setting.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
@@ -262,7 +273,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
sx={{ minWidth: 100 }}
|
sx={{ minWidth: 100 }}
|
||||||
>
|
>
|
||||||
{loading ? '修改中...' : '确认修改'}
|
{loading ? t('setting.changing') : t('setting.confirmChange')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// 表单数据接口
|
// 表单数据接口
|
||||||
export interface ApiKeyFormData {
|
export interface ApiKeyFormData {
|
||||||
@@ -45,6 +46,7 @@ function ApiKeyDialog({
|
|||||||
initialData,
|
initialData,
|
||||||
editMode = false,
|
editMode = false,
|
||||||
}: ApiKeyDialogProps) {
|
}: ApiKeyDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [showApiKey, setShowApiKey] = React.useState(false);
|
const [showApiKey, setShowApiKey] = React.useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -81,14 +83,14 @@ function ApiKeyDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editMode ? '编辑' : '配置'} {factoryName} API Key
|
{editMode ? t('common.edit') : t('common.configure')} {factoryName} API Key
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box component="form" sx={{ mt: 2 }}>
|
<Box component="form" sx={{ mt: 2 }}>
|
||||||
<Controller
|
<Controller
|
||||||
name="api_key"
|
name="api_key"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: 'API Key 是必填项' }}
|
rules={{ required: t('setting.apiKeyRequired') }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -126,7 +128,7 @@ function ApiKeyDialog({
|
|||||||
label="Base URL"
|
label="Base URL"
|
||||||
placeholder="https://api.openai.com/v1"
|
placeholder="https://api.openai.com/v1"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
helperText="可选,自定义 API 端点"
|
helperText={t('setting.baseUrlOptional')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +144,7 @@ function ApiKeyDialog({
|
|||||||
fullWidth
|
fullWidth
|
||||||
label="Group ID"
|
label="Group ID"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
helperText="Minimax 专用的 Group ID"
|
helperText={t('setting.minimaxGroupId')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -151,7 +153,7 @@ function ApiKeyDialog({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} disabled={loading}>
|
<Button onClick={onClose} disabled={loading}>
|
||||||
取消
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit(handleFormSubmit)}
|
onClick={handleSubmit(handleFormSubmit)}
|
||||||
@@ -159,7 +161,7 @@ function ApiKeyDialog({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||||
>
|
>
|
||||||
{editMode ? '更新' : '保存'}
|
{editMode ? t('common.update') : t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Button,
|
|
||||||
TextField,
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
IconButton,
|
|
||||||
InputAdornment,
|
|
||||||
CircularProgress,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
FormHelperText,
|
|
||||||
Link,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
|
||||||
|
|
||||||
// 模型类型选项
|
|
||||||
const MODEL_TYPE_OPTIONS = [
|
|
||||||
{ value: 'chat', label: 'Chat' },
|
|
||||||
{ value: 'embedding', label: 'Embedding' },
|
|
||||||
{ value: 'image2text', label: 'Image2Text' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 表单数据接口
|
|
||||||
export interface AzureOpenAIFormData extends IAddLlmRequestBody {
|
|
||||||
api_version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对话框 Props 接口
|
|
||||||
export interface AzureOpenAIDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (data: AzureOpenAIFormData) => void;
|
|
||||||
loading: boolean;
|
|
||||||
initialData?: AzureOpenAIFormData;
|
|
||||||
editMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Azure OpenAI 配置对话框
|
|
||||||
*/
|
|
||||||
function AzureOpenAIDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
loading,
|
|
||||||
initialData,
|
|
||||||
editMode = false,
|
|
||||||
}: AzureOpenAIDialogProps) {
|
|
||||||
const [showApiKey, setShowApiKey] = React.useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<AzureOpenAIFormData>({
|
|
||||||
defaultValues: {
|
|
||||||
model_type: 'embedding',
|
|
||||||
llm_name: 'gpt-3.5-turbo',
|
|
||||||
api_base: '',
|
|
||||||
api_key: '',
|
|
||||||
api_version: '2024-02-01',
|
|
||||||
max_tokens: 4096,
|
|
||||||
llm_factory: 'Azure-OpenAI',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当对话框打开或初始数据变化时重置表单
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
reset({
|
|
||||||
model_type: 'embedding',
|
|
||||||
llm_name: 'gpt-3.5-turbo',
|
|
||||||
api_base: '',
|
|
||||||
api_key: '',
|
|
||||||
api_version: '2024-02-01',
|
|
||||||
max_tokens: 4096,
|
|
||||||
llm_factory: initialData?.llm_factory || 'Azure-OpenAI',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [open, initialData, reset]);
|
|
||||||
|
|
||||||
const handleFormSubmit = (data: AzureOpenAIFormData) => {
|
|
||||||
onSubmit(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleShowApiKey = () => {
|
|
||||||
setShowApiKey(!showApiKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
|
||||||
<DialogTitle>
|
|
||||||
{editMode ? '编辑' : '配置'} Azure OpenAI
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Box component="form" sx={{ mt: 2 }}>
|
|
||||||
{/* 模型类型选择 */}
|
|
||||||
<Controller
|
|
||||||
name="model_type"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: '模型类型是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
|
||||||
<InputLabel>模型类型</InputLabel>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
label="模型类型"
|
|
||||||
>
|
|
||||||
{MODEL_TYPE_OPTIONS.map((option) => (
|
|
||||||
<MenuItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{errors.model_type && (
|
|
||||||
<FormHelperText>{errors.model_type.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 模型名称 */}
|
|
||||||
<Controller
|
|
||||||
name="llm_name"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: '模型名称是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="模型名称"
|
|
||||||
margin="normal"
|
|
||||||
error={!!errors.llm_name}
|
|
||||||
helperText={errors.llm_name?.message || '请输入模型名称'}
|
|
||||||
placeholder="gpt-3.5-turbo"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 基础 URL */}
|
|
||||||
<Controller
|
|
||||||
name="api_base"
|
|
||||||
control={control}
|
|
||||||
rules={{
|
|
||||||
required: '基础 URL 是必填项',
|
|
||||||
pattern: {
|
|
||||||
value: /^https?:\/\/.+/,
|
|
||||||
message: '基础 URL 必须是有效的 URL'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="基础 Url"
|
|
||||||
margin="normal"
|
|
||||||
error={!!errors.api_base}
|
|
||||||
helperText={errors.api_base?.message || 'Azure OpenAI 服务的端点 URL'}
|
|
||||||
placeholder="https://your-resource.openai.azure.com/"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* API Key */}
|
|
||||||
<Controller
|
|
||||||
name="api_key"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="API-Key"
|
|
||||||
type={showApiKey ? 'text' : 'password'}
|
|
||||||
margin="normal"
|
|
||||||
error={!!errors.api_key}
|
|
||||||
helperText={errors.api_key?.message || '输入api key(如果是本地部署的模型,请忽略)'}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
aria-label="toggle api key visibility"
|
|
||||||
onClick={toggleShowApiKey}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showApiKey ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* API Version */}
|
|
||||||
<Controller
|
|
||||||
name="api_version"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: 'API Version 是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="API Version"
|
|
||||||
margin="normal"
|
|
||||||
error={!!errors.api_version}
|
|
||||||
helperText={errors.api_version?.message || 'Azure OpenAI API 版本'}
|
|
||||||
placeholder="2024-02-01"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 最大token数 */}
|
|
||||||
<Controller
|
|
||||||
name="max_tokens"
|
|
||||||
control={control}
|
|
||||||
rules={{
|
|
||||||
required: '最大token数是必填项',
|
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' }
|
|
||||||
}}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="最大token数"
|
|
||||||
type="number"
|
|
||||||
margin="normal"
|
|
||||||
error={!!errors.max_tokens}
|
|
||||||
helperText={errors.max_tokens?.message || '设置了模型输出的最大长度,以token(单词片段)的数量表示'}
|
|
||||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
{/* 右侧按钮组 */}
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<Button onClick={onClose} disabled={loading}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit(handleFormSubmit)}
|
|
||||||
variant="contained"
|
|
||||||
disabled={loading}
|
|
||||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AzureOpenAIDialog;
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Button,
|
|
||||||
TextField,
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
IconButton,
|
|
||||||
InputAdornment,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
CircularProgress,
|
|
||||||
FormHelperText,
|
|
||||||
Link,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
|
||||||
|
|
||||||
// AWS Bedrock 支持的区域列表
|
|
||||||
export const BEDROCK_REGIONS = [
|
|
||||||
'us-east-2',
|
|
||||||
'us-east-1',
|
|
||||||
'us-west-1',
|
|
||||||
'us-west-2',
|
|
||||||
'af-south-1',
|
|
||||||
'ap-east-1',
|
|
||||||
'ap-south-2',
|
|
||||||
'ap-southeast-3',
|
|
||||||
'ap-southeast-5',
|
|
||||||
'ap-southeast-4',
|
|
||||||
'ap-south-1',
|
|
||||||
'ap-northeast-3',
|
|
||||||
'ap-northeast-2',
|
|
||||||
'ap-southeast-1',
|
|
||||||
'ap-southeast-2',
|
|
||||||
'ap-east-2',
|
|
||||||
'ap-southeast-7',
|
|
||||||
'ap-northeast-1',
|
|
||||||
'ca-central-1',
|
|
||||||
'ca-west-1',
|
|
||||||
'eu-central-1',
|
|
||||||
'eu-west-1',
|
|
||||||
'eu-west-2',
|
|
||||||
'eu-south-1',
|
|
||||||
'eu-west-3',
|
|
||||||
'eu-south-2',
|
|
||||||
'eu-north-1',
|
|
||||||
'eu-central-2',
|
|
||||||
'il-central-1',
|
|
||||||
'mx-central-1',
|
|
||||||
'me-south-1',
|
|
||||||
'me-central-1',
|
|
||||||
'sa-east-1',
|
|
||||||
'us-gov-east-1',
|
|
||||||
'us-gov-west-1',
|
|
||||||
];
|
|
||||||
|
|
||||||
// 模型类型选项
|
|
||||||
const MODEL_TYPE_OPTIONS = [
|
|
||||||
{ value: 'chat', label: 'Chat' },
|
|
||||||
{ value: 'embedding', label: 'Embedding' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 表单数据接口
|
|
||||||
export interface BedrockFormData extends IAddLlmRequestBody {
|
|
||||||
bedrock_ak: string;
|
|
||||||
bedrock_sk: string;
|
|
||||||
bedrock_region: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对话框 Props 接口
|
|
||||||
export interface BedrockDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (data: BedrockFormData) => void;
|
|
||||||
loading: boolean;
|
|
||||||
initialData?: BedrockFormData;
|
|
||||||
editMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AWS Bedrock 配置对话框
|
|
||||||
*/
|
|
||||||
function BedrockDialog ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
loading,
|
|
||||||
initialData,
|
|
||||||
editMode = false,
|
|
||||||
}: BedrockDialogProps) {
|
|
||||||
const [showAccessKey, setShowAccessKey] = React.useState(false);
|
|
||||||
const [showSecretKey, setShowSecretKey] = React.useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<BedrockFormData>({
|
|
||||||
defaultValues: {
|
|
||||||
model_type: 'chat',
|
|
||||||
llm_name: '',
|
|
||||||
bedrock_ak: '',
|
|
||||||
bedrock_sk: '',
|
|
||||||
bedrock_region: 'us-east-1',
|
|
||||||
max_tokens: 4096,
|
|
||||||
llm_factory: 'Bedrock',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当对话框打开或初始数据变化时重置表单
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
reset({
|
|
||||||
model_type: 'chat',
|
|
||||||
llm_name: '',
|
|
||||||
bedrock_ak: '',
|
|
||||||
bedrock_sk: '',
|
|
||||||
bedrock_region: 'us-east-1',
|
|
||||||
max_tokens: 4096,
|
|
||||||
llm_factory: initialData?.llm_factory || 'Bedrock',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [open, initialData, reset]);
|
|
||||||
|
|
||||||
const handleFormSubmit = (data: BedrockFormData) => {
|
|
||||||
onSubmit(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleShowAccessKey = () => {
|
|
||||||
setShowAccessKey(!showAccessKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleShowSecretKey = () => {
|
|
||||||
setShowSecretKey(!showSecretKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const docInfo = {
|
|
||||||
url: 'https://console.aws.amazon.com/',
|
|
||||||
text: '如何集成 Bedrock',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
|
||||||
<DialogTitle>
|
|
||||||
{editMode ? '编辑' : '添加'} LLM
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Box component="form" sx={{ mt: 2 }}>
|
|
||||||
{/* 模型类型 */}
|
|
||||||
<Controller
|
|
||||||
name="model_type"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: '模型类型是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
|
||||||
<InputLabel>* 模型类型</InputLabel>
|
|
||||||
<Select {...field} label="* 模型类型">
|
|
||||||
{MODEL_TYPE_OPTIONS.map((option) => (
|
|
||||||
<MenuItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{errors.model_type && (
|
|
||||||
<FormHelperText>{errors.model_type.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 模型名称 */}
|
|
||||||
<Controller
|
|
||||||
name="llm_name"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: '模型名称是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="* 模型名称"
|
|
||||||
margin="normal"
|
|
||||||
placeholder="请输入模型名称"
|
|
||||||
error={!!errors.llm_name}
|
|
||||||
helperText={errors.llm_name?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* ACCESS KEY */}
|
|
||||||
<Controller
|
|
||||||
name="bedrock_ak"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: 'ACCESS KEY 是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="* ACCESS KEY"
|
|
||||||
type={showAccessKey ? 'text' : 'password'}
|
|
||||||
margin="normal"
|
|
||||||
placeholder="请输入 ACCESS KEY"
|
|
||||||
error={!!errors.bedrock_ak}
|
|
||||||
helperText={errors.bedrock_ak?.message}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
aria-label="toggle access key visibility"
|
|
||||||
onClick={toggleShowAccessKey}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showAccessKey ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* SECRET KEY */}
|
|
||||||
<Controller
|
|
||||||
name="bedrock_sk"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: 'SECRET KEY 是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="* SECRET KEY"
|
|
||||||
type={showSecretKey ? 'text' : 'password'}
|
|
||||||
margin="normal"
|
|
||||||
placeholder="请输入 SECRET KEY"
|
|
||||||
error={!!errors.bedrock_sk}
|
|
||||||
helperText={errors.bedrock_sk?.message}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
aria-label="toggle secret key visibility"
|
|
||||||
onClick={toggleShowSecretKey}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showSecretKey ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* AWS Region */}
|
|
||||||
<Controller
|
|
||||||
name="bedrock_region"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: 'AWS Region 是必填项' }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl fullWidth margin="normal" error={!!errors.bedrock_region}>
|
|
||||||
<InputLabel>* AWS Region</InputLabel>
|
|
||||||
<Select {...field} label="* AWS Region">
|
|
||||||
{BEDROCK_REGIONS.map((region) => (
|
|
||||||
<MenuItem key={region} value={region}>
|
|
||||||
{region}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{errors.bedrock_region && (
|
|
||||||
<FormHelperText>{errors.bedrock_region.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 最大token数 */}
|
|
||||||
<Controller
|
|
||||||
name="max_tokens"
|
|
||||||
control={control}
|
|
||||||
rules={{
|
|
||||||
required: '最大token数是必填项',
|
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
|
||||||
}}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label="* 最大token数"
|
|
||||||
type="number"
|
|
||||||
margin="normal"
|
|
||||||
placeholder="这设置了模型输出的最大长度,以token(单词或词片段)的数量来衡量"
|
|
||||||
error={!!errors.max_tokens}
|
|
||||||
helperText={errors.max_tokens?.message}
|
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
||||||
<Link
|
|
||||||
href={docInfo.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
sx={{ alignSelf: 'center', textDecoration: 'none', ml:2 }}
|
|
||||||
>
|
|
||||||
{docInfo.text}
|
|
||||||
</Link>
|
|
||||||
<Box>
|
|
||||||
<Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit(handleFormSubmit)}
|
|
||||||
variant="contained"
|
|
||||||
disabled={loading}
|
|
||||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BedrockDialog;
|
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
||||||
|
|
||||||
// 表单项配置接口
|
// 表单项配置接口
|
||||||
@@ -79,6 +80,7 @@ function ConfigurationDialog({
|
|||||||
docLink,
|
docLink,
|
||||||
editMode = false,
|
editMode = false,
|
||||||
}: ConfigurationDialogProps) {
|
}: ConfigurationDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [passwordVisibility, setPasswordVisibility] = React.useState<Record<string, boolean>>({});
|
const [passwordVisibility, setPasswordVisibility] = React.useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// 构建默认值
|
// 构建默认值
|
||||||
@@ -138,7 +140,7 @@ function ConfigurationDialog({
|
|||||||
// 构建验证规则
|
// 构建验证规则
|
||||||
const rules: any = {};
|
const rules: any = {};
|
||||||
if (item.required) {
|
if (item.required) {
|
||||||
rules.required = `${item.label}是必填项`;
|
rules.required = t('setting.fieldRequired', { field: item.label });
|
||||||
}
|
}
|
||||||
if (item.validation) {
|
if (item.validation) {
|
||||||
Object.assign(rules, item.validation);
|
Object.assign(rules, item.validation);
|
||||||
@@ -266,7 +268,7 @@ function ConfigurationDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editMode ? '编辑' : ''} {title}
|
{editMode ? t('setting.edit') : ''} {title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box component="form" sx={{ mt: 2 }}>
|
<Box component="form" sx={{ mt: 2 }}>
|
||||||
@@ -290,7 +292,7 @@ function ConfigurationDialog({
|
|||||||
{/* 右侧按钮组 */}
|
{/* 右侧按钮组 */}
|
||||||
<Box sx={{ ml: 'auto' }}>
|
<Box sx={{ ml: 'auto' }}>
|
||||||
<Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}>
|
<Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}>
|
||||||
取消
|
{t('setting.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit(handleFormSubmit)}
|
onClick={handleSubmit(handleFormSubmit)}
|
||||||
@@ -298,7 +300,7 @@ function ConfigurationDialog({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||||
>
|
>
|
||||||
确定
|
{t('setting.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { LLM_FACTORY_LIST, type LLMFactory } from '@/constants/llm';
|
import { LLM_FACTORY_LIST, type LLMFactory } from '@/constants/llm';
|
||||||
|
import i18n from '@/locales';
|
||||||
|
|
||||||
// 表单数据接口
|
// 表单数据接口
|
||||||
export interface OllamaFormData {
|
export interface OllamaFormData {
|
||||||
@@ -64,7 +66,7 @@ const llmFactoryToUrlMap: { [x: string]: string } = {
|
|||||||
function getURLByFactory(factory: LLMFactory) {
|
function getURLByFactory(factory: LLMFactory) {
|
||||||
const url = llmFactoryToUrlMap[factory];
|
const url = llmFactoryToUrlMap[factory];
|
||||||
return {
|
return {
|
||||||
textTip: `如何集成 ${factory}`,
|
textTip: `${i18n.t('setting.howToIntegrate')} ${factory}`,
|
||||||
url,
|
url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,7 @@ function OllamaDialog({
|
|||||||
initialData,
|
initialData,
|
||||||
editMode = false,
|
editMode = false,
|
||||||
}: OllamaDialogProps) {
|
}: OllamaDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -163,7 +166,7 @@ function OllamaDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editMode ? `编辑 ${initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}` : `配置 ${initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}`}
|
{editMode ? t('setting.edit') : t('setting.configure')} {initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box component="form" sx={{ mt: 2 }}>
|
<Box component="form" sx={{ mt: 2 }}>
|
||||||
@@ -171,13 +174,13 @@ function OllamaDialog({
|
|||||||
<Controller
|
<Controller
|
||||||
name="model_type"
|
name="model_type"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: '模型类型是必填项' }}
|
rules={{ required: t('setting.modelTypeRequired') }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
||||||
<InputLabel>模型类型 *</InputLabel>
|
<InputLabel>{t('setting.modelType')} *</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
label="模型类型 *"
|
label={`${t('setting.modelType')} *`}
|
||||||
>
|
>
|
||||||
{modelTypeOptions.map((option) => (
|
{modelTypeOptions.map((option) => (
|
||||||
<MenuItem key={option.value} value={option.value}>
|
<MenuItem key={option.value} value={option.value}>
|
||||||
@@ -196,17 +199,17 @@ function OllamaDialog({
|
|||||||
<Controller
|
<Controller
|
||||||
name="llm_name"
|
name="llm_name"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: '模型名称是必填项' }}
|
rules={{ required: t('setting.modelNameRequired') }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
label="模型名称"
|
label={t('setting.modelName')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
required
|
required
|
||||||
error={!!errors.llm_name}
|
error={!!errors.llm_name}
|
||||||
helperText={errors.llm_name?.message || '请输入模型名称'}
|
helperText={errors.llm_name?.message || t('setting.modelNamePlaceholder')}
|
||||||
placeholder="例如: llama2, mistral"
|
placeholder={t('setting.modelNameExample')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -216,21 +219,21 @@ function OllamaDialog({
|
|||||||
name="api_base"
|
name="api_base"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: '基础 URL 是必填项',
|
required: t('setting.baseUrlRequired'),
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^https?:\/\/.+/,
|
value: /^https?:\/\/.+/,
|
||||||
message: '基础 URL 必须是有效的 URL'
|
message: t('setting.baseUrlInvalid')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
label="基础 URL"
|
label={t('setting.baseUrl')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
required
|
required
|
||||||
error={!!errors.api_base}
|
error={!!errors.api_base}
|
||||||
helperText={errors.api_base?.message || '基础 URL'}
|
helperText={errors.api_base?.message || t('setting.baseUrl')}
|
||||||
placeholder="http://localhost:8888"
|
placeholder="http://localhost:8888"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -247,8 +250,8 @@ function OllamaDialog({
|
|||||||
label="API Key"
|
label="API Key"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
error={!!errors.api_key}
|
error={!!errors.api_key}
|
||||||
helperText={errors.api_key?.message || 'API Key (可选)'}
|
helperText={errors.api_key?.message || t('setting.apiKeyTip')}
|
||||||
placeholder="如果需要认证,请输入 API Key"
|
placeholder={t('setting.apiKeyPlaceholder')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -258,26 +261,26 @@ function OllamaDialog({
|
|||||||
name="max_tokens"
|
name="max_tokens"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: '最大 Token 数是必填项',
|
required: t('setting.maxTokensRequired'),
|
||||||
min: {
|
min: {
|
||||||
value: 1,
|
value: 1,
|
||||||
message: '最大 Token 数必须大于 0'
|
message: t('setting.maxTokensMin')
|
||||||
},
|
},
|
||||||
max: {
|
max: {
|
||||||
value: 100000,
|
value: 100000,
|
||||||
message: '最大 Token 数不能超过 100000'
|
message: t('setting.maxTokensMax')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
label="最大 Token 数"
|
label={t('setting.maxTokens')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
error={!!errors.max_tokens}
|
error={!!errors.max_tokens}
|
||||||
helperText={errors.max_tokens?.message || '模型支持的最大 Token 数'}
|
helperText={errors.max_tokens?.message || t('setting.maxTokensValidation')}
|
||||||
placeholder="4096"
|
placeholder="4096"
|
||||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||||
/>
|
/>
|
||||||
@@ -307,7 +310,7 @@ function OllamaDialog({
|
|||||||
{/* 右侧按钮组 */}
|
{/* 右侧按钮组 */}
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button onClick={onClose} disabled={loading}>
|
<Button onClick={onClose} disabled={loading}>
|
||||||
取消
|
{t('setting.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit(handleFormSubmit)}
|
onClick={handleSubmit(handleFormSubmit)}
|
||||||
@@ -315,7 +318,7 @@ function OllamaDialog({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||||
>
|
>
|
||||||
确定
|
{t('setting.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ListSubheader,
|
ListSubheader,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LlmSvgIcon } from '@/components/AppSvgIcon';
|
import { LlmSvgIcon } from '@/components/AppSvgIcon';
|
||||||
import { IconMap, type LLMFactory } from '@/constants/llm';
|
import { IconMap, type LLMFactory } from '@/constants/llm';
|
||||||
import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
||||||
@@ -69,6 +70,7 @@ function SystemModelDialog({
|
|||||||
editMode = false,
|
editMode = false,
|
||||||
allModelOptions
|
allModelOptions
|
||||||
}: SystemModelDialogProps) {
|
}: SystemModelDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({
|
const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({
|
||||||
defaultValues: {}
|
defaultValues: {}
|
||||||
});
|
});
|
||||||
@@ -106,25 +108,25 @@ function SystemModelDialog({
|
|||||||
await onSubmit(data);
|
await onSubmit(data);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error);
|
console.error(t('setting.submitFailed'), error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
设置默认模型
|
{t('setting.setDefaultModel')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box component="form" sx={{ mt: 2 }}>
|
<Box component="form" sx={{ mt: 2 }}>
|
||||||
<Controller
|
<Controller
|
||||||
name="llm_id"
|
name="llm_id"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: '聊天模型是必填项' }}
|
rules={{ required: t('setting.chatModelRequired') }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal" error={!!errors.llm_id}>
|
<FormControl fullWidth margin="normal" error={!!errors.llm_id}>
|
||||||
<InputLabel>聊天模型</InputLabel>
|
<InputLabel>{t('setting.chatModel')}</InputLabel>
|
||||||
<Select {...field} label="聊天模型">
|
<Select {...field} label={t('setting.chatModel')}>
|
||||||
{llmOptions.map((group) => [
|
{llmOptions.map((group) => [
|
||||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
...group.options.map((option) => (
|
...group.options.map((option) => (
|
||||||
@@ -152,11 +154,11 @@ function SystemModelDialog({
|
|||||||
<Controller
|
<Controller
|
||||||
name="embd_id"
|
name="embd_id"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: '嵌入模型是必填项' }}
|
rules={{ required: t('setting.embeddingModelRequired') }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal" error={!!errors.embd_id}>
|
<FormControl fullWidth margin="normal" error={!!errors.embd_id}>
|
||||||
<InputLabel>嵌入模型</InputLabel>
|
<InputLabel>{t('setting.embeddingModel')}</InputLabel>
|
||||||
<Select {...field} label="嵌入模型">
|
<Select {...field} label={t('setting.embeddingModel')}>
|
||||||
{embdOptions.map((group) => [
|
{embdOptions.map((group) => [
|
||||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
...group.options.map((option) => (
|
...group.options.map((option) => (
|
||||||
@@ -186,8 +188,8 @@ function SystemModelDialog({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal">
|
<FormControl fullWidth margin="normal">
|
||||||
<InputLabel>Img2txt模型</InputLabel>
|
<InputLabel>{t('setting.img2txtModel')}</InputLabel>
|
||||||
<Select {...field} label="Img2txt模型">
|
<Select {...field} label={t('setting.img2txtModel')}>
|
||||||
{img2txtOptions.map((group) => [
|
{img2txtOptions.map((group) => [
|
||||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
...group.options.map((option) => (
|
...group.options.map((option) => (
|
||||||
@@ -212,8 +214,8 @@ function SystemModelDialog({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal">
|
<FormControl fullWidth margin="normal">
|
||||||
<InputLabel>Speech2txt模型</InputLabel>
|
<InputLabel>{t('setting.speech2txtModel')}</InputLabel>
|
||||||
<Select {...field} label="Speech2txt模型">
|
<Select {...field} label={t('setting.speech2txtModel')}>
|
||||||
{asrOptions.map((group) => [
|
{asrOptions.map((group) => [
|
||||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
...group.options.map((option) => (
|
...group.options.map((option) => (
|
||||||
@@ -238,8 +240,8 @@ function SystemModelDialog({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal">
|
<FormControl fullWidth margin="normal">
|
||||||
<InputLabel>Rerank模型</InputLabel>
|
<InputLabel>{t('setting.rerankModel')}</InputLabel>
|
||||||
<Select {...field} label="Rerank模型">
|
<Select {...field} label={t('setting.rerankModel')}>
|
||||||
{rerankOptions.map((group) => [
|
{rerankOptions.map((group) => [
|
||||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
...group.options.map((option) => (
|
...group.options.map((option) => (
|
||||||
@@ -264,8 +266,8 @@ function SystemModelDialog({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth margin="normal">
|
<FormControl fullWidth margin="normal">
|
||||||
<InputLabel>TTS模型</InputLabel>
|
<InputLabel>{t('setting.ttsModel')}</InputLabel>
|
||||||
<Select {...field} label="TTS模型">
|
<Select {...field} label={t('setting.ttsModel')}>
|
||||||
{ttsOptions.map((group) => [
|
{ttsOptions.map((group) => [
|
||||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
...group.options.map((option) => (
|
...group.options.map((option) => (
|
||||||
@@ -288,7 +290,7 @@ function SystemModelDialog({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} disabled={loading}>
|
<Button onClick={onClose} disabled={loading}>
|
||||||
取消
|
{t('setting.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit(handleFormSubmit)}
|
onClick={handleSubmit(handleFormSubmit)}
|
||||||
@@ -296,7 +298,7 @@ function SystemModelDialog({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||||
>
|
>
|
||||||
确定
|
{t('setting.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ConfigFormItem, DocLinkConfig } from './ConfigurationDialog';
|
import type { ConfigFormItem, DocLinkConfig } from './ConfigurationDialog';
|
||||||
import { LLM_FACTORY_LIST } from '@/constants/llm';
|
import { LLM_FACTORY_LIST } from '@/constants/llm';
|
||||||
|
import i18n from '@/locales';
|
||||||
|
|
||||||
// AWS Bedrock 支持的区域列表
|
// AWS Bedrock 支持的区域列表
|
||||||
export const BEDROCK_REGIONS = [
|
export const BEDROCK_REGIONS = [
|
||||||
@@ -54,71 +55,71 @@ export const MODEL_TYPE_OPTIONS = [
|
|||||||
export const DOC_LINKS: Record<string, DocLinkConfig> = {
|
export const DOC_LINKS: Record<string, DocLinkConfig> = {
|
||||||
[LLM_FACTORY_LIST.AzureOpenAI]: {
|
[LLM_FACTORY_LIST.AzureOpenAI]: {
|
||||||
url: 'https://azure.microsoft.com/en-us/products/ai-services/openai-service',
|
url: 'https://azure.microsoft.com/en-us/products/ai-services/openai-service',
|
||||||
text: '如何集成 Azure OpenAI',
|
text: `${i18n.t('setting.howToIntegrate')} Azure OpenAI`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.Bedrock]: {
|
[LLM_FACTORY_LIST.Bedrock]: {
|
||||||
url: 'https://console.aws.amazon.com/',
|
url: 'https://console.aws.amazon.com/',
|
||||||
text: '如何集成 Bedrock',
|
text: `${i18n.t('setting.howToIntegrate')} Bedrock`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.Ollama]: {
|
[LLM_FACTORY_LIST.Ollama]: {
|
||||||
url: 'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx',
|
url: 'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx',
|
||||||
text: '如何集成 Ollama',
|
text: `${i18n.t('setting.howToIntegrate')} Ollama`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.Xinference]: {
|
[LLM_FACTORY_LIST.Xinference]: {
|
||||||
url: 'https://inference.readthedocs.io/en/latest/user_guide',
|
url: 'https://inference.readthedocs.io/en/latest/user_guide',
|
||||||
text: '如何集成 Xinference',
|
text: `${i18n.t('setting.howToIntegrate')} Xinference`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.ModelScope]: {
|
[LLM_FACTORY_LIST.ModelScope]: {
|
||||||
url: 'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
|
url: 'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
|
||||||
text: '如何集成 ModelScope',
|
text: `${i18n.t('setting.howToIntegrate')} ModelScope`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.LocalAI]: {
|
[LLM_FACTORY_LIST.LocalAI]: {
|
||||||
url: 'https://localai.io/docs/getting-started/models/',
|
url: 'https://localai.io/docs/getting-started/models/',
|
||||||
text: '如何集成 LocalAI',
|
text: `${i18n.t('setting.howToIntegrate')} LocalAI`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.LMStudio]: {
|
[LLM_FACTORY_LIST.LMStudio]: {
|
||||||
url: 'https://lmstudio.ai/docs/basics',
|
url: 'https://lmstudio.ai/docs/basics',
|
||||||
text: '如何集成 LMStudio',
|
text: `${i18n.t('setting.howToIntegrate')} LMStudio`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.OpenAiAPICompatible]: {
|
[LLM_FACTORY_LIST.OpenAiAPICompatible]: {
|
||||||
url: 'https://platform.openai.com/docs/models/gpt-4',
|
url: 'https://platform.openai.com/docs/models/gpt-4',
|
||||||
text: '如何集成 OpenAI API Compatible',
|
text: `${i18n.t('setting.howToIntegrate')} OpenAI API Compatible`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.TogetherAI]: {
|
[LLM_FACTORY_LIST.TogetherAI]: {
|
||||||
url: 'https://docs.together.ai/docs/deployment-options',
|
url: 'https://docs.together.ai/docs/deployment-options',
|
||||||
text: '如何集成 TogetherAI',
|
text: `${i18n.t('setting.howToIntegrate')} TogetherAI`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.Replicate]: {
|
[LLM_FACTORY_LIST.Replicate]: {
|
||||||
url: 'https://replicate.com/docs/topics/deployments',
|
url: 'https://replicate.com/docs/topics/deployments',
|
||||||
text: '如何集成 Replicate',
|
text: `${i18n.t('setting.howToIntegrate')} Replicate`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.OpenRouter]: {
|
[LLM_FACTORY_LIST.OpenRouter]: {
|
||||||
url: 'https://openrouter.ai/docs',
|
url: 'https://openrouter.ai/docs',
|
||||||
text: '如何集成 OpenRouter',
|
text: `${i18n.t('setting.howToIntegrate')} OpenRouter`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.HuggingFace]: {
|
[LLM_FACTORY_LIST.HuggingFace]: {
|
||||||
url: 'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
|
url: 'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
|
||||||
text: '如何集成 HuggingFace',
|
text: `${i18n.t('setting.howToIntegrate')} HuggingFace`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.GPUStack]: {
|
[LLM_FACTORY_LIST.GPUStack]: {
|
||||||
url: 'https://docs.gpustack.ai/latest/quickstart',
|
url: 'https://docs.gpustack.ai/latest/quickstart',
|
||||||
text: '如何集成 GPUStack',
|
text: `${i18n.t('setting.howToIntegrate')} GPUStack`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.VLLM]: {
|
[LLM_FACTORY_LIST.VLLM]: {
|
||||||
url: 'https://docs.vllm.ai/en/latest/',
|
url: 'https://docs.vllm.ai/en/latest/',
|
||||||
text: '如何集成 VLLM',
|
text: `${i18n.t('setting.howToIntegrate')} VLLM`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.FishAudio]: {
|
[LLM_FACTORY_LIST.FishAudio]: {
|
||||||
url: 'https://www.fish.audio/',
|
url: 'https://www.fish.audio/',
|
||||||
text: '如何集成 Fish Audio',
|
text: `${i18n.t('setting.howToIntegrate')} Fish Audio`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.TencentCloud]: {
|
[LLM_FACTORY_LIST.TencentCloud]: {
|
||||||
url: 'https://cloud.tencent.com/document/api/1093/37823',
|
url: 'https://cloud.tencent.com/document/api/1093/37823',
|
||||||
text: '如何集成 腾讯云语音识别',
|
text: `${i18n.t('setting.howToIntegrate')} 腾讯云语音识别`,
|
||||||
},
|
},
|
||||||
[LLM_FACTORY_LIST.VolcEngine]: {
|
[LLM_FACTORY_LIST.VolcEngine]: {
|
||||||
url: 'https://www.volcengine.com/docs/82379/1302008',
|
url: 'https://www.volcengine.com/docs/82379/1302008',
|
||||||
text: '如何集成 VolcEngine',
|
text: `${i18n.t('setting.howToIntegrate')} VolcEngine`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ export const DOC_LINKS: Record<string, DocLinkConfig> = {
|
|||||||
export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [
|
options: [
|
||||||
@@ -138,24 +139,24 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'gpt-3.5-turbo',
|
placeholder: 'gpt-3.5-turbo',
|
||||||
helperText: '请输入模型名称',
|
helperText: i18n.t('setting.modelNameHelperText'),
|
||||||
defaultValue: 'gpt-3.5-turbo',
|
defaultValue: 'gpt-3.5-turbo',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'api_base',
|
name: 'api_base',
|
||||||
label: '基础 Url',
|
label: i18n.t('setting.baseUrl'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'https://your-resource.openai.azure.com/',
|
placeholder: 'https://your-resource.openai.azure.com/',
|
||||||
helperText: 'Azure OpenAI 服务的端点 URL',
|
helperText: i18n.t('setting.azureOpenAIEndpointHelperText'),
|
||||||
validation: {
|
validation: {
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^https?:\/\/.+/,
|
value: /^https?:\/\/.+/,
|
||||||
message: '基础 URL 必须是有效的 URL',
|
message: i18n.t('setting.baseUrlValidationMessage'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -163,7 +164,7 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
|||||||
name: 'api_key',
|
name: 'api_key',
|
||||||
label: 'API-Key',
|
label: 'API-Key',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
helperText: '输入api key(如果是本地部署的模型,请忽略)',
|
helperText: i18n.t('setting.apiKeyHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'api_version',
|
name: 'api_version',
|
||||||
@@ -171,20 +172,20 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '2024-02-01',
|
placeholder: '2024-02-01',
|
||||||
helperText: 'Azure OpenAI API 版本',
|
helperText: i18n.t('setting.azureAPIVersionHelperText'),
|
||||||
defaultValue: '2024-02-01',
|
defaultValue: '2024-02-01',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -193,7 +194,7 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
|||||||
export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: MODEL_TYPE_OPTIONS.slice(0, 2), // 只支持 chat 和 embedding
|
options: MODEL_TYPE_OPTIONS.slice(0, 2), // 只支持 chat 和 embedding
|
||||||
@@ -201,24 +202,24 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入模型名称',
|
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'bedrock_ak',
|
name: 'bedrock_ak',
|
||||||
label: 'ACCESS KEY',
|
label: 'ACCESS KEY',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 ACCESS KEY',
|
placeholder: i18n.t('setting.accessKeyPlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'bedrock_sk',
|
name: 'bedrock_sk',
|
||||||
label: 'SECRET KEY',
|
label: 'SECRET KEY',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 SECRET KEY',
|
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'bedrock_region',
|
name: 'bedrock_region',
|
||||||
@@ -230,14 +231,14 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '这设置了模型输出的最大长度,以token(单词或词片段)的数量来衡量',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '这设置了模型输出的最大长度,以token(单词或词片段)的数量来衡量',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -246,7 +247,7 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
|||||||
export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: MODEL_TYPE_OPTIONS,
|
options: MODEL_TYPE_OPTIONS,
|
||||||
@@ -254,24 +255,24 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '例如: llama2, mistral',
|
placeholder: i18n.t('setting.ollamaModelNamePlaceholder'),
|
||||||
helperText: '请输入模型名称',
|
helperText: i18n.t('setting.modelNameHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'api_base',
|
name: 'api_base',
|
||||||
label: '基础 URL',
|
label: i18n.t('setting.baseUrl'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'http://localhost:8888',
|
placeholder: 'http://localhost:8888',
|
||||||
helperText: '基础 URL',
|
helperText: i18n.t('setting.baseUrlHelperText'),
|
||||||
defaultValue: 'http://localhost:11434',
|
defaultValue: 'http://localhost:11434',
|
||||||
validation: {
|
validation: {
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^https?:\/\/.+/,
|
value: /^https?:\/\/.+/,
|
||||||
message: '基础 URL 必须是有效的 URL',
|
message: i18n.t('setting.baseUrlValidationMessage'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -279,20 +280,20 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
|||||||
name: 'api_key',
|
name: 'api_key',
|
||||||
label: 'API Key',
|
label: 'API Key',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: '如果需要认证,请输入 API Key',
|
placeholder: i18n.t('setting.apiKeyOptionalPlaceholder'),
|
||||||
helperText: 'API Key (可选)',
|
helperText: i18n.t('setting.apiKeyOptional'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大 Token 数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '4096',
|
placeholder: '4096',
|
||||||
helperText: '模型支持的最大 Token 数',
|
helperText: i18n.t('setting.maxTokensSupportedHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大 Token 数必须大于 0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大 Token 数不能超过 100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -300,7 +301,7 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
|||||||
export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: MODEL_TYPE_OPTIONS.slice(0, 3),
|
options: MODEL_TYPE_OPTIONS.slice(0, 3),
|
||||||
@@ -308,38 +309,38 @@ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入模型名称',
|
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yiyan_ak',
|
name: 'yiyan_ak',
|
||||||
label: '一言 API KEY',
|
label: i18n.t('setting.baiduYiYanAPIKey'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 API KEY',
|
placeholder: i18n.t('setting.apiKeyPlaceholder'),
|
||||||
helperText: 'Baidu YiYan API KEY',
|
helperText: 'Baidu YiYan API KEY',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yiyan_sk',
|
name: 'yiyan_sk',
|
||||||
label: '一言 Secret KEY',
|
label: i18n.t('setting.baiduYiYanSecretKey'),
|
||||||
type: 'password',
|
type: 'password',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Secret KEY',
|
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||||
helperText: 'Baidu YiYan Secret KEY',
|
helperText: 'Baidu YiYan Secret KEY',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -347,7 +348,7 @@ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
|||||||
export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'tts', label: 'TTS' },],
|
options: [{ value: 'tts', label: 'TTS' },],
|
||||||
@@ -355,17 +356,17 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入模型名称',
|
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fish_audio_ak',
|
name: 'fish_audio_ak',
|
||||||
label: 'Fish Audio API KEY',
|
label: 'Fish Audio API KEY',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 API KEY',
|
placeholder: i18n.t('setting.apiKeyPlaceholder'),
|
||||||
helperText: 'Fish Audio API KEY',
|
helperText: 'Fish Audio API KEY',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -373,20 +374,20 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
|||||||
label: 'FishAudio Refrence ID',
|
label: 'FishAudio Refrence ID',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Refrence ID',
|
placeholder: i18n.t('setting.fishAudioRefIdPlaceholder'),
|
||||||
helperText: 'Fish Audio Refrence ID',
|
helperText: 'Fish Audio Refrence ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -394,7 +395,7 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
|||||||
export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'chat', label: 'Chat' }, { value: 'image2text', label: 'Image2Text' }],
|
options: [{ value: 'chat', label: 'Chat' }, { value: 'image2text', label: 'Image2Text' }],
|
||||||
@@ -402,46 +403,46 @@ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入模型名称',
|
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'google_project_id',
|
name: 'google_project_id',
|
||||||
label: 'Project ID',
|
label: 'Project ID',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Project ID',
|
placeholder: i18n.t('setting.googleProjectIdPlaceholder'),
|
||||||
helperText: 'Google Cloud Project ID',
|
helperText: 'Google Cloud Project ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'google_region',
|
name: 'google_region',
|
||||||
label: 'Google Cloud 区域',
|
label: i18n.t('setting.googleCloudRegion'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Google Cloud 区域',
|
placeholder: i18n.t('setting.googleCloudRegionPlaceholder'),
|
||||||
helperText: 'Google Cloud 区域',
|
helperText: i18n.t('setting.googleCloudRegionHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'google_service_account_key',
|
name: 'google_service_account_key',
|
||||||
label: 'Google Cloud Service Account Key',
|
label: 'Google Cloud Service Account Key',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Google Cloud Service Account Key',
|
placeholder: i18n.t('setting.googleServiceAccountKeyPlaceholder'),
|
||||||
helperText: 'Google Cloud Service Account Key',
|
helperText: 'Google Cloud Service Account Key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -449,7 +450,7 @@ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
|||||||
export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'speech2text', label: 'Speech2Text' }],
|
options: [{ value: 'speech2text', label: 'Speech2Text' }],
|
||||||
@@ -457,7 +458,7 @@ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [
|
options: [
|
||||||
@@ -469,38 +470,38 @@ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tencent_ak',
|
name: 'tencent_ak',
|
||||||
label: '腾讯云 Secret ID',
|
label: i18n.t('setting.tencentSecretId'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Secret ID',
|
placeholder: i18n.t('setting.secretIdPlaceholder'),
|
||||||
helperText: '腾讯云 Secret ID',
|
helperText: i18n.t('setting.tencentSecretIdHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tencent_sk',
|
name: 'tencent_sk',
|
||||||
label: '腾讯云 Secret KEY',
|
label: i18n.t('setting.tencentSecretKey'),
|
||||||
type: 'password',
|
type: 'password',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Secret KEY',
|
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||||
helperText: '腾讯云 Secret KEY',
|
helperText: i18n.t('setting.tencentSecretKeyHelperText'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
|
export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'hunyuan_sid',
|
name: 'hunyuan_sid',
|
||||||
label: '混元 Secret ID',
|
label: i18n.t('setting.hunyuanSecretId'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Secret ID',
|
placeholder: i18n.t('setting.secretIdPlaceholder'),
|
||||||
helperText: '混元 Secret ID',
|
helperText: i18n.t('setting.hunyuanSecretIdHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'hunyuan_sk',
|
name: 'hunyuan_sk',
|
||||||
label: '混元 Secret KEY',
|
label: i18n.t('setting.hunyuanSecretKey'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 Secret KEY',
|
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||||
helperText: '混元 Secret KEY',
|
helperText: i18n.t('setting.hunyuanSecretKeyHelperText'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -508,7 +509,7 @@ export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
|
|||||||
export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'chat', label: 'Chat' }, { value: 'tts', label: 'TTS' }],
|
options: [{ value: 'chat', label: 'Chat' }, { value: 'tts', label: 'TTS' }],
|
||||||
@@ -516,30 +517,30 @@ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入模型名称',
|
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'xunfei_spark_password',
|
name: 'xunfei_spark_password',
|
||||||
label: '讯飞星火 API Password',
|
label: i18n.t('setting.xunfeiSparkAPIPassword'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 API Password',
|
placeholder: i18n.t('setting.apiPasswordPlaceholder'),
|
||||||
helperText: '讯飞星火 API Password',
|
helperText: i18n.t('setting.xunfeiSparkAPIPasswordHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -548,7 +549,7 @@ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
|||||||
export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
|
export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
|
||||||
{
|
{
|
||||||
name: 'model_type',
|
name: 'model_type',
|
||||||
label: '模型类型',
|
label: i18n.t('setting.modelType'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'chat', label: 'Chat' }, { value: 'embedding', label: 'Embedding' }],
|
options: [{ value: 'chat', label: 'Chat' }, { value: 'embedding', label: 'Embedding' }],
|
||||||
@@ -556,38 +557,38 @@ export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'llm_name',
|
name: 'llm_name',
|
||||||
label: '模型名称',
|
label: i18n.t('setting.modelName'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入模型名称',
|
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'endpoint_id',
|
name: 'endpoint_id',
|
||||||
label: '模型 EndpointID',
|
label: i18n.t('setting.modelEndpointId'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 EndpointID',
|
placeholder: i18n.t('setting.endpointIdPlaceholder'),
|
||||||
helperText: '模型 EndpointID',
|
helperText: i18n.t('setting.modelEndpointIdHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ark_api_key',
|
name: 'ark_api_key',
|
||||||
label: '火山 ARK_API_KEY',
|
label: i18n.t('setting.volcEngineARKAPIKey'),
|
||||||
type: 'password',
|
type: 'password',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '请输入 ARK_API_KEY',
|
placeholder: i18n.t('setting.arkApiKeyPlaceholder'),
|
||||||
helperText: '模型 ARK_API_KEY',
|
helperText: i18n.t('setting.modelARKAPIKeyHelperText'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
label: '最大token数',
|
label: i18n.t('setting.maxTokens'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||||
defaultValue: 4096,
|
defaultValue: 4096,
|
||||||
validation: {
|
validation: {
|
||||||
min: { value: 1, message: '最大token数必须大于0' },
|
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { LlmSvgIcon } from "@/components/AppSvgIcon";
|
|||||||
import { IconMap, type LLMFactory } from "@/constants/llm";
|
import { IconMap, type LLMFactory } from "@/constants/llm";
|
||||||
import type { IFactory } from "@/interfaces/database/llm";
|
import type { IFactory } from "@/interfaces/database/llm";
|
||||||
import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material";
|
import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// 模型类型标签颜色映射
|
// 模型类型标签颜色映射
|
||||||
export const MODEL_TYPE_COLORS: Record<string, string> = {
|
export const MODEL_TYPE_COLORS: Record<string, string> = {
|
||||||
@@ -27,6 +28,7 @@ const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
|
|||||||
factory,
|
factory,
|
||||||
onConfigure,
|
onConfigure,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 获取工厂图标名称
|
// 获取工厂图标名称
|
||||||
const getFactoryIconName = (factoryName: LLMFactory) => {
|
const getFactoryIconName = (factoryName: LLMFactory) => {
|
||||||
@@ -89,7 +91,7 @@ const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
|
|||||||
width: 'fit-content'
|
width: 'fit-content'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
添加模型
|
{t('setting.addModel')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ const McpDialog: React.FC<McpDialogProps> = ({
|
|||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
|
||||||
logger.debug('McpDialog 组件加载', initialData);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(async (data: ICreateMcpServerRequestBody) => {
|
const handleSubmit = useCallback(async (data: ICreateMcpServerRequestBody) => {
|
||||||
setSubmitLoading(true);
|
setSubmitLoading(true);
|
||||||
setSubmitError(null);
|
setSubmitError(null);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||||
import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||||
import { Mode } from '@mui/icons-material';
|
import { Mode } from '@mui/icons-material';
|
||||||
@@ -74,6 +75,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
loading = false,
|
loading = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [formData, setFormData] = useState<McpFormData>({
|
const [formData, setFormData] = useState<McpFormData>({
|
||||||
name: initialData?.name || '',
|
name: initialData?.name || '',
|
||||||
url: initialData?.url || '',
|
url: initialData?.url || '',
|
||||||
@@ -131,17 +133,17 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
const errors: Partial<McpFormData> = {};
|
const errors: Partial<McpFormData> = {};
|
||||||
|
|
||||||
if (!formData.name?.trim()) {
|
if (!formData.name?.trim()) {
|
||||||
errors.name = '名称不能为空';
|
errors.name = t('setting.nameRequired');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.url.trim()) {
|
if (!formData.url.trim()) {
|
||||||
errors.url = 'URL不能为空';
|
errors.url = t('setting.urlRequired');
|
||||||
} else if (!/^https?:\/\/.+/.test(formData.url)) {
|
} else if (!/^https?:\/\/.+/.test(formData.url)) {
|
||||||
errors.url = 'URL格式不正确';
|
errors.url = t('setting.urlFormatInvalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.server_type) {
|
if (!formData.server_type) {
|
||||||
errors.server_type = '请选择服务器类型';
|
errors.server_type = t('setting.serverTypeRequired');
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormErrors(errors);
|
setFormErrors(errors);
|
||||||
@@ -166,7 +168,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
if (!formData.url.trim()) {
|
if (!formData.url.trim()) {
|
||||||
setTestResult({
|
setTestResult({
|
||||||
success: false,
|
success: false,
|
||||||
error: '请先填写 URL',
|
error: t('setting.fillUrlFirst'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -205,13 +207,13 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setTestResult({
|
setTestResult({
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error || '测试失败',
|
error: result.error || t('setting.testFailed'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setTestResult({
|
setTestResult({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || '测试连接时发生错误',
|
error: error.message || t('setting.testConnectionError'),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setTestLoading(false);
|
setTestLoading(false);
|
||||||
@@ -234,7 +236,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
return (
|
return (
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<TextField
|
<TextField
|
||||||
label="名称"
|
label={t('common.name')}
|
||||||
value={formData.name || ''}
|
value={formData.name || ''}
|
||||||
onChange={handleInputChange('name')}
|
onChange={handleInputChange('name')}
|
||||||
error={!!formErrors.name}
|
error={!!formErrors.name}
|
||||||
@@ -257,12 +259,12 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl fullWidth required error={!!formErrors.server_type}>
|
<FormControl fullWidth required error={!!formErrors.server_type}>
|
||||||
<InputLabel>Server Type</InputLabel>
|
<InputLabel>{t('mcp.serverType')}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={formData.server_type}
|
value={formData.server_type}
|
||||||
onChange={handleInputChange('server_type')}
|
onChange={handleInputChange('server_type')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label="Server Type"
|
label={t('mcp.serverType')}
|
||||||
>
|
>
|
||||||
<MenuItem value="sse">SSE</MenuItem>
|
<MenuItem value="sse">SSE</MenuItem>
|
||||||
<MenuItem value="streamable-http">Streamable HTTP</MenuItem>
|
<MenuItem value="streamable-http">Streamable HTTP</MenuItem>
|
||||||
@@ -276,21 +278,21 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder="e.g. eyJhbGciOiJIUzI1Ni..."
|
placeholder="e.g. eyJhbGciOiJIUzI1Ni..."
|
||||||
helperText="可选:用于身份验证的令牌"
|
helperText={t('setting.authTokenOptional')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 测试连接部分 */}
|
{/* 测试连接部分 */}
|
||||||
{onTest && (
|
{onTest && (
|
||||||
<TestSection>
|
<TestSection>
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||||
<Typography variant="h6">测试连接</Typography>
|
<Typography variant="h6">{t('setting.testConnection')}</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleTest}
|
onClick={handleTest}
|
||||||
disabled={testLoading || disabled}
|
disabled={testLoading || disabled}
|
||||||
startIcon={testLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
|
startIcon={testLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
|
||||||
>
|
>
|
||||||
{testLoading ? '测试中...' : '测试连接'}
|
{testLoading ? t('setting.testing') : t('setting.testConnection')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -299,16 +301,16 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
{testResult.success ? (
|
{testResult.success ? (
|
||||||
<Box>
|
<Box>
|
||||||
<Alert severity="success" sx={{ mb: 2 }}>
|
<Alert severity="success" sx={{ mb: 2 }}>
|
||||||
连接成功!发现 {testResult.tools?.length || 0} 个工具
|
{t('setting.connectionSuccess', { count: testResult.tools?.length || 0 })}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{testResult.tools && testResult.tools.length > 0 && (
|
{testResult.tools && testResult.tools.length > 0 && (
|
||||||
<Accordion defaultExpanded>
|
<Accordion defaultExpanded>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Typography variant="subtitle1">可用工具</Typography>
|
<Typography variant="subtitle1">{t('setting.availableTools')}</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
label={`${testResult.tools.length} tools available`}
|
label={`${testResult.tools.length} ${t('mcp.toolsAvailable')}`}
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
@@ -344,7 +346,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
{testResult.error || '连接失败'}
|
{testResult.error || t('setting.connectionFailed')}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -360,7 +362,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
|||||||
disabled={loading || disabled || !testResult?.success}
|
disabled={loading || disabled || !testResult?.success}
|
||||||
startIcon={loading ? <CircularProgress size={16} /> : undefined}
|
startIcon={loading ? <CircularProgress size={16} /> : undefined}
|
||||||
>
|
>
|
||||||
{loading ? '保存中...' : '保存'}
|
{loading ? t('setting.saving') : t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import React from 'react';
|
|||||||
|
|
||||||
// 导入独立的对话框组件
|
// 导入独立的对话框组件
|
||||||
import ApiKeyDialog, { type ApiKeyFormData, type ApiKeyDialogProps } from './Dialog/ApiKeyDialog';
|
import ApiKeyDialog, { type ApiKeyFormData, type ApiKeyDialogProps } from './Dialog/ApiKeyDialog';
|
||||||
import AzureOpenAIDialog, { type AzureOpenAIFormData, type AzureOpenAIDialogProps } from './Dialog/AzureOpenAIDialog';
|
|
||||||
import BedrockDialog, { type BedrockFormData, type BedrockDialogProps, BEDROCK_REGIONS } from './Dialog/BedrockDialog';
|
|
||||||
import OllamaDialog, { type OllamaFormData, type OllamaDialogProps } from './Dialog/OllamaDialog';
|
import OllamaDialog, { type OllamaFormData, type OllamaDialogProps } from './Dialog/OllamaDialog';
|
||||||
import SystemModelDialog, { type SystemModelFormData, type SystemModelDialogProps, type ModelOption, type ModelGroup } from './Dialog/SystemModelDialog';
|
import SystemModelDialog, { type SystemModelFormData, type SystemModelDialogProps, type ModelOption, type ModelGroup } from './Dialog/SystemModelDialog';
|
||||||
import ConfigurationDialog, { type ConfigurationFormData, type ConfigurationDialogProps, type ConfigFormItem, type DocLinkConfig } from './Dialog/ConfigurationDialog';
|
import ConfigurationDialog, { type ConfigurationFormData, type ConfigurationDialogProps, type ConfigFormItem, type DocLinkConfig } from './Dialog/ConfigurationDialog';
|
||||||
@@ -19,14 +17,13 @@ export interface BaseDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出所有表单数据接口
|
// 导出所有表单数据接口
|
||||||
export type { ApiKeyFormData, AzureOpenAIFormData, BedrockFormData, OllamaFormData, SystemModelFormData, ConfigurationFormData };
|
export type { ApiKeyFormData, OllamaFormData, SystemModelFormData, ConfigurationFormData };
|
||||||
|
|
||||||
// 导出所有对话框 Props 接口
|
// 导出所有对话框 Props 接口
|
||||||
export type { ApiKeyDialogProps, AzureOpenAIDialogProps, BedrockDialogProps, OllamaDialogProps, SystemModelDialogProps, ConfigurationDialogProps };
|
export type { ApiKeyDialogProps, OllamaDialogProps, SystemModelDialogProps, ConfigurationDialogProps };
|
||||||
|
|
||||||
// 导出其他相关接口和常量
|
// 导出其他相关接口和常量
|
||||||
export type { ModelOption, ModelGroup };
|
export type { ModelOption, ModelGroup };
|
||||||
export { BEDROCK_REGIONS };
|
|
||||||
|
|
||||||
// 模型对话框整合组件的 Props 接口
|
// 模型对话框整合组件的 Props 接口
|
||||||
export interface ModelDialogsProps {
|
export interface ModelDialogsProps {
|
||||||
@@ -39,22 +36,6 @@ export interface ModelDialogsProps {
|
|||||||
initialData?: ApiKeyFormData;
|
initialData?: ApiKeyFormData;
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
};
|
};
|
||||||
azureDialog: {
|
|
||||||
open: boolean;
|
|
||||||
closeDialog: () => void;
|
|
||||||
submitAzureOpenAI: (data: AzureOpenAIFormData) => void;
|
|
||||||
loading: boolean;
|
|
||||||
initialData?: AzureOpenAIFormData;
|
|
||||||
editMode?: boolean;
|
|
||||||
};
|
|
||||||
bedrockDialog: {
|
|
||||||
open: boolean;
|
|
||||||
closeDialog: () => void;
|
|
||||||
submitBedrock: (data: BedrockFormData) => void;
|
|
||||||
loading: boolean;
|
|
||||||
initialData?: BedrockFormData;
|
|
||||||
editMode?: boolean;
|
|
||||||
};
|
|
||||||
ollamaDialog: {
|
ollamaDialog: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
@@ -93,8 +74,6 @@ export interface ModelDialogsProps {
|
|||||||
*/
|
*/
|
||||||
export const ModelDialogs: React.FC<ModelDialogsProps> = ({
|
export const ModelDialogs: React.FC<ModelDialogsProps> = ({
|
||||||
apiKeyDialog,
|
apiKeyDialog,
|
||||||
azureDialog,
|
|
||||||
bedrockDialog,
|
|
||||||
ollamaDialog,
|
ollamaDialog,
|
||||||
configurationDialog,
|
configurationDialog,
|
||||||
systemDialog,
|
systemDialog,
|
||||||
@@ -112,26 +91,6 @@ export const ModelDialogs: React.FC<ModelDialogsProps> = ({
|
|||||||
editMode={apiKeyDialog!.editMode}
|
editMode={apiKeyDialog!.editMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Azure OpenAI 对话框 */}
|
|
||||||
<AzureOpenAIDialog
|
|
||||||
open={azureDialog.open}
|
|
||||||
onClose={azureDialog.closeDialog}
|
|
||||||
onSubmit={azureDialog.submitAzureOpenAI}
|
|
||||||
loading={azureDialog.loading}
|
|
||||||
initialData={azureDialog.initialData}
|
|
||||||
editMode={azureDialog.editMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* AWS Bedrock 对话框 */}
|
|
||||||
<BedrockDialog
|
|
||||||
open={bedrockDialog.open}
|
|
||||||
onClose={bedrockDialog.closeDialog}
|
|
||||||
onSubmit={bedrockDialog.submitBedrock}
|
|
||||||
loading={bedrockDialog.loading}
|
|
||||||
initialData={bedrockDialog.initialData}
|
|
||||||
editMode={bedrockDialog.editMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Ollama 对话框 */}
|
{/* Ollama 对话框 */}
|
||||||
<OllamaDialog
|
<OllamaDialog
|
||||||
open={ollamaDialog.open}
|
open={ollamaDialog.open}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { PhotoCamera, Edit } from '@mui/icons-material';
|
import { PhotoCamera, Edit } from '@mui/icons-material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useProfileSetting } from '@/hooks/setting-hooks';
|
import { useProfileSetting } from '@/hooks/setting-hooks';
|
||||||
import { useMessage } from '@/hooks/useSnackbar';
|
import { useMessage } from '@/hooks/useSnackbar';
|
||||||
import type { IUserInfo } from '@/interfaces/database/user-setting';
|
import type { IUserInfo } from '@/interfaces/database/user-setting';
|
||||||
@@ -45,6 +46,7 @@ interface ProfileFormProps {
|
|||||||
* 个人信息表单
|
* 个人信息表单
|
||||||
*/
|
*/
|
||||||
function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const showMessage = useMessage();
|
const showMessage = useMessage();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -91,13 +93,13 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
if (file) {
|
if (file) {
|
||||||
// 检查文件类型
|
// 检查文件类型
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
showMessage.error('请选择图片文件');
|
showMessage.error(t('setting.pleaseSelectImageFile'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件大小 (限制为2MB)
|
// 检查文件大小 (限制为2MB)
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
showMessage.error('图片大小不能超过2MB');
|
showMessage.error(t('setting.imageSizeLimit'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +125,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
if (!formData.nickname?.trim()) {
|
if (!formData.nickname?.trim()) {
|
||||||
showMessage.error('用户名不能为空');
|
showMessage.error(t('setting.usernameRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,17 +138,17 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(updateData);
|
await onSubmit(updateData);
|
||||||
showMessage.success('个人信息更新成功');
|
showMessage.success(t('setting.profileUpdateSuccess'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新用户信息失败:', error);
|
console.error('更新用户信息失败:', error);
|
||||||
showMessage.error('更新失败,请重试');
|
showMessage.error(t('setting.updateFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
|
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
|
||||||
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
|
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
|
||||||
个人资料
|
{t('setting.personalProfile')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
@@ -166,9 +168,9 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
头像
|
{t('setting.avatar')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title="上传头像">
|
<Tooltip title={t('setting.uploadAvatar')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={triggerFileSelect}
|
onClick={triggerFileSelect}
|
||||||
@@ -192,7 +194,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
|
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
支持 JPG、PNG 格式,文件大小不超过 2MB
|
{t('setting.avatarFormatTip')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -202,7 +204,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="用户名"
|
label={t('setting.username')}
|
||||||
value={formData.nickname}
|
value={formData.nickname}
|
||||||
onChange={handleInputChange('nickname')}
|
onChange={handleInputChange('nickname')}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -214,21 +216,21 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="邮箱"
|
label={t('setting.email')}
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
disabled
|
disabled
|
||||||
helperText="邮箱地址不可修改"
|
helperText={t('setting.emailNotEditable')}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* 语言 */}
|
{/* 语言 */}
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>语言</InputLabel>
|
<InputLabel>{t('setting.language')}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={formData.language}
|
value={formData.language}
|
||||||
label="语言"
|
label={t('setting.language')}
|
||||||
onChange={handleSelectChange('language')}
|
onChange={handleSelectChange('language')}
|
||||||
>
|
>
|
||||||
{languageOptions.map((option) => (
|
{languageOptions.map((option) => (
|
||||||
@@ -243,10 +245,10 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
{/* 时区 */}
|
{/* 时区 */}
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>时区</InputLabel>
|
<InputLabel>{t('setting.timezone')}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={formData.timezone}
|
value={formData.timezone}
|
||||||
label="时区"
|
label={t('setting.timezone')}
|
||||||
onChange={handleSelectChange('timezone')}
|
onChange={handleSelectChange('timezone')}
|
||||||
>
|
>
|
||||||
{timezoneOptions.map((option) => (
|
{timezoneOptions.map((option) => (
|
||||||
@@ -266,7 +268,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
sx={{ minWidth: 120 }}
|
sx={{ minWidth: 120 }}
|
||||||
>
|
>
|
||||||
保存
|
{t('setting.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import McpDialog from '@/pages/setting/components/McpDialog';
|
|||||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||||
import type { IImportMcpServersRequestBody, ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
import type { IImportMcpServersRequestBody, ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||||
import { useMessage } from '@/hooks/useSnackbar';
|
import { useMessage } from '@/hooks/useSnackbar';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const PageContainer = styled(Box)(({ theme }) => ({
|
const PageContainer = styled(Box)(({ theme }) => ({
|
||||||
@@ -69,6 +70,7 @@ const SearchContainer = styled(Box)(({ theme }) => ({
|
|||||||
|
|
||||||
|
|
||||||
export default function McpSettingPage() {
|
export default function McpSettingPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleRefreshServer = async (initial?: boolean) => {
|
const handleRefreshServer = async (initial?: boolean) => {
|
||||||
if (initial) {
|
if (initial) {
|
||||||
@@ -187,9 +189,9 @@ export default function McpSettingPage() {
|
|||||||
if (selectedServerId) {
|
if (selectedServerId) {
|
||||||
const result = await deleteMcpServer(selectedServerId);
|
const result = await deleteMcpServer(selectedServerId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage.success('删除成功');
|
showMessage.success(t('mcp.deleteSuccess'));
|
||||||
} else {
|
} else {
|
||||||
showMessage.error(result.error || '删除失败');
|
showMessage.error(t('mcp.deleteFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
@@ -198,19 +200,19 @@ export default function McpSettingPage() {
|
|||||||
const handleBulkDelete = async () => {
|
const handleBulkDelete = async () => {
|
||||||
const result = await batchDeleteMcpServers(selectedServers);
|
const result = await batchDeleteMcpServers(selectedServers);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage.success('批量删除成功');
|
showMessage.success(t('mcp.batchDeleteSuccess'));
|
||||||
setSelectedServers([]);
|
setSelectedServers([]);
|
||||||
} else {
|
} else {
|
||||||
showMessage.error(result.error || '批量删除失败');
|
showMessage.error(t('mcp.batchDeleteFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
const result = await exportMcpServers(selectedServers);
|
const result = await exportMcpServers(selectedServers);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage.success('导出成功');
|
showMessage.success(t('mcp.exportSuccess'));
|
||||||
} else {
|
} else {
|
||||||
showMessage.error(result.error || '导出失败');
|
showMessage.error(t('mcp.exportFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -218,12 +220,12 @@ export default function McpSettingPage() {
|
|||||||
try {
|
try {
|
||||||
if (editingServer) {
|
if (editingServer) {
|
||||||
if (!editingServer.id) {
|
if (!editingServer.id) {
|
||||||
showMessage.error('server id 不能为空');
|
showMessage.error(t('mcp.serverIdRequired'));
|
||||||
return { success: false, error: 'server id 不能为空' };
|
return { success: false, error: 'server id 不能为空' };
|
||||||
}
|
}
|
||||||
const result = await updateMcpServer({ ...data, mcp_id: editingServer.id ?? '' });
|
const result = await updateMcpServer({ ...data, mcp_id: editingServer.id ?? '' });
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage.success('MCP 服务器更新成功');
|
showMessage.success(t('mcp.mcpServerUpdateSuccess'));
|
||||||
setMcpDialogOpen(false);
|
setMcpDialogOpen(false);
|
||||||
setEditingServer(null);
|
setEditingServer(null);
|
||||||
return result;
|
return result;
|
||||||
@@ -233,7 +235,7 @@ export default function McpSettingPage() {
|
|||||||
} else {
|
} else {
|
||||||
const result = await createMcpServer(data);
|
const result = await createMcpServer(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage.success('MCP 服务器创建成功');
|
showMessage.success(t('mcp.mcpServerCreateSuccess'));
|
||||||
setMcpDialogOpen(false);
|
setMcpDialogOpen(false);
|
||||||
setEditingServer(null);
|
setEditingServer(null);
|
||||||
return result;
|
return result;
|
||||||
@@ -251,7 +253,7 @@ export default function McpSettingPage() {
|
|||||||
try {
|
try {
|
||||||
const result = await testMcpServer(data);
|
const result = await testMcpServer(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage.success('测试成功');
|
showMessage.success(t('mcp.testSuccess'));
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
return result;
|
return result;
|
||||||
@@ -283,7 +285,7 @@ export default function McpSettingPage() {
|
|||||||
setImportJson('');
|
setImportJson('');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showMessage.error('JSON 格式错误');
|
showMessage.error(t('mcp.jsonFormatError'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage.error('JSON 格式错误');
|
showMessage.error('JSON 格式错误');
|
||||||
@@ -294,11 +296,11 @@ export default function McpSettingPage() {
|
|||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" fontWeight={600} color="#333">
|
<Typography variant="h5" component="h1" fontWeight="bold">
|
||||||
MCP Servers
|
{t('mcp.mcpServers')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
Customize the list of MCP servers
|
{t('mcp.customizeTheListOfMcpServers')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" gap={2}>
|
<Box display="flex" gap={2}>
|
||||||
@@ -307,14 +309,14 @@ export default function McpSettingPage() {
|
|||||||
startIcon={<ImportIcon />}
|
startIcon={<ImportIcon />}
|
||||||
onClick={() => setImportDialogOpen(true)}
|
onClick={() => setImportDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Import
|
{t('mcp.import')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
>
|
>
|
||||||
Add MCP
|
{t('mcp.addMCP')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
@@ -323,7 +325,7 @@ export default function McpSettingPage() {
|
|||||||
<Box>
|
<Box>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="Search MCP servers..."
|
placeholder={t('mcp.searchPlaceholder')}
|
||||||
value={searchString}
|
value={searchString}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@@ -338,16 +340,17 @@ export default function McpSettingPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
startIcon={<DeleteIcon />}
|
startIcon={<DeleteIcon />}
|
||||||
onClick={handleBulkDelete}
|
onClick={handleBulkDelete}
|
||||||
color="error"
|
disabled={selectedServers.length === 0}
|
||||||
>
|
>
|
||||||
Delete ({selectedServers.length})
|
{t('mcp.deleteSelected')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<ExportIcon />}
|
startIcon={<ExportIcon />}
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
|
disabled={selectedServers.length === 0}
|
||||||
>
|
>
|
||||||
Export ({selectedServers.length})
|
{t('mcp.exportSelected')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -389,10 +392,10 @@ export default function McpSettingPage() {
|
|||||||
{server.name}
|
{server.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
类型: {server.server_type}
|
{t('mcp.type')}: {server.server_type}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
更新时间: {dayjs(server.update_date).format('YYYY-MM-DD HH:mm:ss')}
|
{t('mcp.updateTime')}: {dayjs(server.update_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -409,7 +412,7 @@ export default function McpSettingPage() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" sx={{ ml: 2 }}>
|
<Typography variant="body2" sx={{ ml: 2 }}>
|
||||||
共 {total} 条
|
{t('mcp.totalItems', { count: total })}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
@@ -423,11 +426,11 @@ export default function McpSettingPage() {
|
|||||||
>
|
>
|
||||||
<MenuItem onClick={handleEdit}>
|
<MenuItem onClick={handleEdit}>
|
||||||
<EditIcon sx={{ mr: 1 }} fontSize="small" />
|
<EditIcon sx={{ mr: 1 }} fontSize="small" />
|
||||||
Edit
|
{t('common.edit')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
|
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
|
||||||
<DeleteIcon sx={{ mr: 1 }} fontSize="small" />
|
<DeleteIcon sx={{ mr: 1 }} fontSize="small" />
|
||||||
Delete
|
{t('common.delete')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
@@ -446,16 +449,16 @@ export default function McpSettingPage() {
|
|||||||
|
|
||||||
{/* 导入对话框 */}
|
{/* 导入对话框 */}
|
||||||
<Dialog open={importDialogOpen} onClose={() => setImportDialogOpen(false)} maxWidth="md" fullWidth>
|
<Dialog open={importDialogOpen} onClose={() => setImportDialogOpen(false)} maxWidth="md" fullWidth>
|
||||||
<DialogTitle>Import MCP Servers</DialogTitle>
|
<DialogTitle>{t('mcp.importTitle')}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
Paste your MCP servers JSON configuration below. The format should match the Mock.json structure.
|
{t('mcp.importDescription')}
|
||||||
</Alert>
|
</Alert>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
rows={10}
|
rows={10}
|
||||||
label="JSON Configuration"
|
label={t('mcp.jsonConfiguration')}
|
||||||
value={importJson}
|
value={importJson}
|
||||||
onChange={(e) => setImportJson(e.target.value)}
|
onChange={(e) => setImportJson(e.target.value)}
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
@@ -472,9 +475,9 @@ export default function McpSettingPage() {
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setImportDialogOpen(false)}>Cancel</Button>
|
<Button onClick={() => setImportDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||||
<Button onClick={handleImport} variant="contained">
|
<Button onClick={handleImport} variant="contained">
|
||||||
Import
|
{t('mcp.import')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
Delete as DeleteIcon,
|
Delete as DeleteIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLlmModelSetting } from '@/hooks/setting-hooks';
|
import { useLlmModelSetting } from '@/hooks/setting-hooks';
|
||||||
import { useModelDialogs } from './hooks/useModelDialogs';
|
import { useModelDialogs } from './hooks/useModelDialogs';
|
||||||
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
|
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
|
||||||
@@ -66,6 +67,7 @@ function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model:
|
|||||||
|
|
||||||
// 主页面组件
|
// 主页面组件
|
||||||
function ModelsPage() {
|
function ModelsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
|
const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
|
||||||
const modelDialogs = useModelDialogs(refreshLlmModel);
|
const modelDialogs = useModelDialogs(refreshLlmModel);
|
||||||
|
|
||||||
@@ -106,12 +108,15 @@ function ModelsPage() {
|
|||||||
LLM_FACTORY_LIST.VolcEngine,
|
LLM_FACTORY_LIST.VolcEngine,
|
||||||
]
|
]
|
||||||
if (LocalLlmFactories.includes(factoryName)) {
|
if (LocalLlmFactories.includes(factoryName)) {
|
||||||
|
// local llm
|
||||||
modelDialogs.ollamaDialog.openDialog({
|
modelDialogs.ollamaDialog.openDialog({
|
||||||
llm_factory: factory.name,
|
llm_factory: factory.name,
|
||||||
});
|
});
|
||||||
} else if (configurationFactories.includes(factoryName)) {
|
} else if (configurationFactories.includes(factoryName)) {
|
||||||
|
// custom configuration llm
|
||||||
modelDialogs.configurationDialog.openConfigurationDialog(factory.name);
|
modelDialogs.configurationDialog.openConfigurationDialog(factory.name);
|
||||||
} else {
|
} else {
|
||||||
|
// llm set api
|
||||||
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
|
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
|
||||||
}
|
}
|
||||||
logger.debug('handleConfigureFactory', factory);
|
logger.debug('handleConfigureFactory', factory);
|
||||||
@@ -129,26 +134,26 @@ function ModelsPage() {
|
|||||||
// 处理删除单个模型
|
// 处理删除单个模型
|
||||||
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
|
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
|
||||||
dialog.confirm({
|
dialog.confirm({
|
||||||
title: '确认删除',
|
title: t('setting.confirmDelete'),
|
||||||
content: `是否确认删除模型 ${modelName}?`,
|
content: t('setting.confirmDeleteModel', { modelName }),
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
|
await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [dialog, modelDialogs.deleteOps]);
|
}, [dialog, modelDialogs.deleteOps, t]);
|
||||||
|
|
||||||
// 处理删除模型工厂
|
// 处理删除模型工厂
|
||||||
const handleDeleteFactory = useCallback(async (factoryName: string) => {
|
const handleDeleteFactory = useCallback(async (factoryName: string) => {
|
||||||
dialog.confirm({
|
dialog.confirm({
|
||||||
title: '确认删除',
|
title: t('setting.confirmDelete'),
|
||||||
content: `是否确认删除模型工厂 ${factoryName}?`,
|
content: t('setting.confirmDeleteFactory', { factoryName }),
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await modelDialogs.deleteOps.deleteFactory(factoryName);
|
await modelDialogs.deleteOps.deleteFactory(factoryName);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [dialog, modelDialogs.deleteOps]);
|
}, [dialog, modelDialogs.deleteOps, t]);
|
||||||
|
|
||||||
if (!llmFactory || !myLlm) {
|
if (!llmFactory || !myLlm) {
|
||||||
return (
|
return (
|
||||||
@@ -168,28 +173,28 @@ function ModelsPage() {
|
|||||||
}}>
|
}}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
模型设置
|
{t('setting.modelSettings')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" color="text.secondary" gutterBottom>
|
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||||
管理您的 LLM 模型工厂和个人模型配置
|
{t('setting.modelSettingsDescription')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{/* 设置默认模型 */}
|
{/* 设置默认模型 */}
|
||||||
<Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}>
|
<Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}>
|
||||||
设置默认模型
|
{t('setting.setDefaultModel')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{/* My LLM 部分 */}
|
{/* My LLM 部分 */}
|
||||||
<Box mb={4} mt={2}>
|
<Box mb={4} mt={2}>
|
||||||
{!myLlm || Object.keys(myLlm).length === 0 ? (
|
{!myLlm || Object.keys(myLlm).length === 0 ? (
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
您还没有配置任何 LLM 模型。请在下方的模型工厂中进行配置。
|
{t('setting.noModelsConfigured')}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Accordion defaultExpanded>
|
<Accordion defaultExpanded>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
我的 LLM 模型
|
{t('setting.myLlmModels')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
@@ -242,13 +247,13 @@ function ModelsPage() {
|
|||||||
variant='contained' color='primary' startIcon={<EditIcon />}
|
variant='contained' color='primary' startIcon={<EditIcon />}
|
||||||
onClick={() => handleEditLlmFactory(factoryName)}
|
onClick={() => handleEditLlmFactory(factoryName)}
|
||||||
>
|
>
|
||||||
编辑
|
{t('setting.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined' color='primary' startIcon={<DeleteIcon />}
|
variant='outlined' color='primary' startIcon={<DeleteIcon />}
|
||||||
onClick={() => handleDeleteFactory(factoryName)}
|
onClick={() => handleDeleteFactory(factoryName)}
|
||||||
>
|
>
|
||||||
删除
|
{t('setting.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -281,7 +286,7 @@ function ModelsPage() {
|
|||||||
<Accordion defaultExpanded>
|
<Accordion defaultExpanded>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
LLM 模型工厂
|
{t('setting.llmModelFactories')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Box, Button, Divider, Typography } from "@mui/material";
|
import { Box, Button, Divider, Typography } from "@mui/material";
|
||||||
import { Lock } from "@mui/icons-material";
|
import { Lock } from "@mui/icons-material";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import ProfileForm from "./components/ProfileForm";
|
import ProfileForm from "./components/ProfileForm";
|
||||||
import ChangePasswordDialog from "./components/ChangePasswordDialog";
|
import ChangePasswordDialog from "./components/ChangePasswordDialog";
|
||||||
import { useProfileSetting } from "@/hooks/setting-hooks";
|
import { useProfileSetting } from "@/hooks/setting-hooks";
|
||||||
import logger from "@/utils/logger";
|
import logger from "@/utils/logger";
|
||||||
|
|
||||||
function ProfileSetting() {
|
function ProfileSetting() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
|
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
|
||||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
|
|
||||||
@@ -31,10 +33,10 @@ function ProfileSetting() {
|
|||||||
{/* 密码修改部分 */}
|
{/* 密码修改部分 */}
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Typography variant="h6" gutterBottom sx={{ mb: 2, fontWeight: 600 }}>
|
<Typography variant="h6" gutterBottom sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
账户安全
|
{t('setting.accountSecurity')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
定期更新密码有助于保护您的账户安全
|
{t('setting.passwordUpdateTip')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -51,7 +53,7 @@ function ProfileSetting() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
修改密码
|
{t('setting.changePassword')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Paper,
|
Paper,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Storage as StorageIcon,
|
Storage as StorageIcon,
|
||||||
Memory as RedisIcon,
|
Memory as RedisIcon,
|
||||||
@@ -39,6 +40,7 @@ const TITLE_MAP = {
|
|||||||
task_executor_heartbeat: 'Task Executor',
|
task_executor_heartbeat: 'Task Executor',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 图标映射
|
// 图标映射
|
||||||
const ICON_MAP = {
|
const ICON_MAP = {
|
||||||
es: SearchIcon,
|
es: SearchIcon,
|
||||||
@@ -49,7 +51,16 @@ const ICON_MAP = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function SystemSetting() {
|
function SystemSetting() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { systemStatus, loading, error, fetchSystemStatus } = useSystemStatus();
|
const { systemStatus, loading, error, fetchSystemStatus } = useSystemStatus();
|
||||||
|
const componentTitleMap: Record<string, string> = {
|
||||||
|
'doc_engine': t('setting.docEngine'),
|
||||||
|
'object_storage': t('setting.objectStorage'),
|
||||||
|
'redis': t('setting.redis'),
|
||||||
|
'database': t('setting.database'),
|
||||||
|
'elasticsearch': t('setting.elasticsearch'),
|
||||||
|
'task_executor': t('setting.taskExecutor'),
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSystemStatus();
|
fetchSystemStatus();
|
||||||
@@ -60,7 +71,7 @@ function SystemSetting() {
|
|||||||
if (key.startsWith('task_executor_heartbeat')) {
|
if (key.startsWith('task_executor_heartbeat')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconComponent = ICON_MAP[key as keyof typeof ICON_MAP] || DefaultIcon;
|
const IconComponent = ICON_MAP[key as keyof typeof ICON_MAP] || DefaultIcon;
|
||||||
const title = TITLE_MAP[key as keyof typeof TITLE_MAP] || key;
|
const title = TITLE_MAP[key as keyof typeof TITLE_MAP] || key;
|
||||||
const status = info?.status || 'unknown';
|
const status = info?.status || 'unknown';
|
||||||
@@ -68,7 +79,7 @@ function SystemSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={12} key={key}>
|
<Grid size={12} key={key}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
@@ -104,44 +115,44 @@ function SystemSetting() {
|
|||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{Object.keys(info)
|
{Object.keys(info)
|
||||||
.filter((x) => x !== 'status')
|
.filter((x) => x !== 'status')
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<Box
|
<Box
|
||||||
key={x}
|
key={x}
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
mb={1}
|
mb={1}
|
||||||
sx={{
|
sx={{
|
||||||
p: 1,
|
p: 1,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'grey.50',
|
backgroundColor: 'grey.50',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary" fontWeight="medium">
|
||||||
|
{x.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={x === 'error' ? 'error' : 'text.primary'}
|
||||||
|
fontWeight="bold"
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary" fontWeight="medium">
|
{typeof info[x] === 'number'
|
||||||
{x.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
|
? info[x].toFixed(2)
|
||||||
</Typography>
|
: (info[x] != null ? String(info[x]) : 'N/A')
|
||||||
<Typography
|
}
|
||||||
variant="body2"
|
{x === 'elapsed' && ' ms'}
|
||||||
color={x === 'error' ? 'error' : 'text.primary'}
|
</Typography>
|
||||||
fontWeight="bold"
|
</Box>
|
||||||
>
|
))}
|
||||||
{typeof info[x] === 'number'
|
</Box>
|
||||||
? info[x].toFixed(2)
|
|
||||||
: (info[x] != null ? String(info[x]) : 'N/A')
|
|
||||||
}
|
|
||||||
{x === 'elapsed' && ' ms'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -170,23 +181,23 @@ function SystemSetting() {
|
|||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||||
系统状态
|
{t('setting.systemStatus')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" color="text.secondary" paragraph>
|
<Typography variant="body1" color="text.secondary" paragraph>
|
||||||
查看系统各个组件的运行状态和性能指标
|
{t('setting.systemStatusDescription')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{systemStatus ? (
|
{systemStatus ? (
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{Object.keys(systemStatus).map((key) =>
|
{Object.keys(systemStatus).map((key) =>
|
||||||
renderSystemInfo(key, systemStatus[key as keyof ISystemStatus])
|
renderSystemInfo(key, systemStatus[key as keyof ISystemStatus])
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Alert severity="info" sx={{ mt: 2 }}>
|
<Alert severity="info" sx={{ mt: 2 }}>
|
||||||
暂无系统状态数据
|
{t('setting.noSystemStatusData')}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ function TeamsSetting() {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={inviteEmail}
|
value={inviteEmail}
|
||||||
onChange={(e) => setInviteEmail(e.target.value)}
|
onChange={(e) => setInviteEmail(e.target.value)}
|
||||||
placeholder={t('setting.emailPlaceholder') || '请输入邀请用户的邮箱地址'}
|
placeholder={t('setting.emailPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -282,7 +282,7 @@ function TeamsSetting() {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={loading || !inviteEmail.trim()}
|
disabled={loading || !inviteEmail.trim()}
|
||||||
>
|
>
|
||||||
{loading ? t('setting.inviting', 'inviting') : t('setting.invite')}
|
{loading ? t('setting.inviting') : t('setting.invite')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -139,8 +139,7 @@ request.interceptors.response.use(
|
|||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
} else if (data?.code !== 0) {
|
} else if (data?.code !== 0) {
|
||||||
// 处理其他业务错误
|
// 处理其他业务错误
|
||||||
logger.info('请求出现错误:', data?.message);
|
const error = new CustomError(data?.message || i18n.t('message.requestError'));
|
||||||
const error = new CustomError(data?.message || '请求出现错误');
|
|
||||||
error.code = data?.code || -1;
|
error.code = data?.code || -1;
|
||||||
error.response = data;
|
error.response = data;
|
||||||
snackbar.warning(error.message);
|
snackbar.warning(error.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user