feat: introduce auto routing (#122)

This commit is contained in:
samanhappy
2025-05-25 21:09:47 +08:00
committed by GitHub
parent 27b7e766af
commit 9e5c2b5525
24 changed files with 2864 additions and 26 deletions

BIN
frontend/favicon.ico Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -14,14 +14,14 @@ const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-30 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50 bg-opacity-30 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-3">
{isGroup ? t('groups.confirmDelete') : t('server.confirmDelete')}
</h3>
<p className="text-gray-500 mb-6">
{isGroup
{isGroup
? t('groups.deleteWarning', { name: serverName })
: t('server.deleteWarning', { name: serverName })}
</p>

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import React, { createContext, useContext, useState, ReactNode, useCallback } from 'react';
import Toast, { ToastType } from '@/components/ui/Toast';
interface ToastContextProps {
@@ -32,18 +32,18 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
duration: 3000,
});
const showToast = (message: string, type: ToastType = 'info', duration: number = 3000) => {
const showToast = useCallback((message: string, type: ToastType = 'info', duration: number = 3000) => {
setToast({
message,
type,
visible: true,
duration,
});
};
}, []);
const hideToast = () => {
const hideToast = useCallback(() => {
setToast((prev) => ({ ...prev, visible: false }));
};
}, []);
return (
<ToastContext.Provider value={{ showToast }}>

View File

@@ -16,10 +16,19 @@ interface InstallConfig {
npmRegistry: string;
}
interface SmartRoutingConfig {
enabled: boolean;
dbUrl: string;
openaiApiBaseUrl: string;
openaiApiKey: string;
openaiApiEmbeddingModel: string;
}
interface SystemSettings {
systemConfig?: {
routing?: RoutingConfig;
install?: InstallConfig;
smartRouting?: SmartRoutingConfig;
};
}
@@ -47,6 +56,14 @@ export const useSettingsData = () => {
npmRegistry: '',
});
const [smartRoutingConfig, setSmartRoutingConfig] = useState<SmartRoutingConfig>({
enabled: false,
dbUrl: '',
openaiApiBaseUrl: '',
openaiApiKey: '',
openaiApiEmbeddingModel: '',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
@@ -89,14 +106,25 @@ export const useSettingsData = () => {
npmRegistry: data.data.systemConfig.install.npmRegistry || '',
});
}
if (data.success && data.data?.systemConfig?.smartRouting) {
setSmartRoutingConfig({
enabled: data.data.systemConfig.smartRouting.enabled ?? false,
dbUrl: data.data.systemConfig.smartRouting.dbUrl || '',
openaiApiBaseUrl: data.data.systemConfig.smartRouting.openaiApiBaseUrl || '',
openaiApiKey: data.data.systemConfig.smartRouting.openaiApiKey || '',
openaiApiEmbeddingModel:
data.data.systemConfig.smartRouting.openaiApiEmbeddingModel || '',
});
}
} catch (error) {
console.error('Failed to fetch settings:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch settings');
// 使用一个稳定的 showToast 引用,避免将其加入依赖数组
showToast(t('errors.failedToFetchSettings'));
} finally {
setLoading(false);
}
}, [t, showToast]);
}, [t]); // 移除 showToast 依赖
// Update routing configuration
const updateRoutingConfig = async <T extends keyof RoutingConfig>(
@@ -195,6 +223,107 @@ export const useSettingsData = () => {
}
};
// Update smart routing configuration
const updateSmartRoutingConfig = async <T extends keyof SmartRoutingConfig>(
key: T,
value: SmartRoutingConfig[T],
) => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch('/api/system-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token || '',
},
body: JSON.stringify({
smartRouting: {
[key]: value,
},
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setSmartRoutingConfig({
...smartRoutingConfig,
[key]: value,
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(data.message || t('errors.failedToUpdateSmartRoutingConfig'));
return false;
}
} catch (error) {
console.error('Failed to update smart routing config:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to update smart routing config';
setError(errorMessage);
showToast(errorMessage);
return false;
} finally {
setLoading(false);
}
};
// Update multiple smart routing configuration fields at once
const updateSmartRoutingConfigBatch = async (updates: Partial<SmartRoutingConfig>) => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch('/api/system-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token || '',
},
body: JSON.stringify({
smartRouting: updates,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setSmartRoutingConfig({
...smartRoutingConfig,
...updates,
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(data.message || t('errors.failedToUpdateSmartRoutingConfig'));
return false;
}
} catch (error) {
console.error('Failed to update smart routing config:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to update smart routing config';
setError(errorMessage);
showToast(errorMessage);
return false;
} finally {
setLoading(false);
}
};
// Fetch settings when the component mounts or refreshKey changes
useEffect(() => {
fetchSettings();
@@ -213,6 +342,7 @@ export const useSettingsData = () => {
tempRoutingConfig,
setTempRoutingConfig,
installConfig,
smartRoutingConfig,
loading,
error,
setError,
@@ -220,5 +350,7 @@ export const useSettingsData = () => {
fetchSettings,
updateRoutingConfig,
updateInstallConfig,
updateSmartRoutingConfig,
updateSmartRoutingConfigBatch,
};
};

View File

@@ -125,7 +125,8 @@
"initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch...",
"serverInstall": "Failed to install server",
"failedToFetchSettings": "Failed to fetch settings",
"failedToUpdateRouteConfig": "Failed to update route configuration"
"failedToUpdateRouteConfig": "Failed to update route configuration",
"failedToUpdateSmartRoutingConfig": "Failed to update smart routing configuration"
},
"common": {
"processing": "Processing...",
@@ -170,8 +171,9 @@
"account": "Account Settings",
"password": "Change Password",
"appearance": "Appearance",
"routeConfig": "Security Configuration",
"installConfig": "Installation Configuration"
"routeConfig": "Security",
"installConfig": "Installation",
"smartRouting": "Smart Routing"
},
"market": {
"title": "Server Market - (Data from mcpm.sh)"
@@ -277,7 +279,20 @@
"npmRegistry": "NPM Registry URL",
"npmRegistryDescription": "Set npm_config_registry environment variable for NPM package installation",
"npmRegistryPlaceholder": "e.g. https://registry.npmjs.org/",
"installConfig": "Installation Configuration",
"systemConfigUpdated": "System configuration updated successfully"
"installConfig": "Installation",
"systemConfigUpdated": "System configuration updated successfully",
"enableSmartRouting": "Enable Smart Routing",
"enableSmartRoutingDescription": "Enable smart routing feature to search the most suitable tool based on input (using $smart group name)",
"dbUrl": "PostgreSQL URL (with pgvector support)",
"dbUrlPlaceholder": "e.g. postgresql://user:password@localhost:5432/dbname",
"openaiApiBaseUrl": "OpenAI API Base URL",
"openaiApiBaseUrlPlaceholder": "https://api.openai.com/v1",
"openaiApiKey": "OpenAI API Key",
"openaiApiKeyPlaceholder": "Enter OpenAI API key",
"openaiApiEmbeddingModel": "OpenAI Embedding Model",
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
"smartRoutingConfigUpdated": "Smart routing configuration updated successfully",
"smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing",
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}"
}
}

View File

@@ -126,7 +126,8 @@
"serverInstall": "安装服务器失败",
"failedToFetchSettings": "获取设置失败",
"failedToUpdateSystemConfig": "更新系统配置失败",
"failedToUpdateRouteConfig": "更新路由配置失败"
"failedToUpdateRouteConfig": "更新路由配置失败",
"failedToUpdateSmartRoutingConfig": "更新智能路由配置失败"
},
"common": {
"processing": "处理中...",
@@ -169,7 +170,8 @@
"password": "修改密码",
"appearance": "外观",
"routeConfig": "安全配置",
"installConfig": "安装配置"
"installConfig": "安装",
"smartRouting": "智能路由"
},
"groups": {
"title": "分组管理"
@@ -279,6 +281,20 @@
"npmRegistryDescription": "设置 npm_config_registry 环境变量,用于 NPM 包安装",
"npmRegistryPlaceholder": "例如: https://registry.npmmirror.com/",
"installConfig": "安装配置",
"systemConfigUpdated": "系统配置更新成功"
"systemConfigUpdated": "系统配置更新成功",
"enableSmartRouting": "启用智能路由",
"enableSmartRoutingDescription": "开启智能路由功能,根据输入自动搜索最合适的工具",
"dbUrl": "PostgreSQL 连接地址(支持 pgvector",
"dbUrlPlaceholder": "例如: postgresql://user:password@localhost:5432/dbname",
"openaiApiBaseUrl": "OpenAI API 基础地址",
"openaiApiBaseUrlPlaceholder": "https://api.openai.com/v1",
"openaiApiKey": "OpenAI API 密钥",
"openaiApiKeyDescription": "用于访问 OpenAI API 的密钥",
"openaiApiKeyPlaceholder": "请输入 OpenAI API 密钥",
"openaiApiEmbeddingModel": "OpenAI 嵌入模型",
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
"smartRoutingConfigUpdated": "智能路由配置更新成功",
"smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥",
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}"
}
}

View File

@@ -26,14 +26,29 @@ const SettingsPage: React.FC = () => {
npmRegistry: '',
});
const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{
dbUrl: string;
openaiApiBaseUrl: string;
openaiApiKey: string;
openaiApiEmbeddingModel: string;
}>({
dbUrl: '',
openaiApiBaseUrl: '',
openaiApiKey: '',
openaiApiEmbeddingModel: '',
});
const {
routingConfig,
tempRoutingConfig,
setTempRoutingConfig,
installConfig: savedInstallConfig,
smartRoutingConfig,
loading,
updateRoutingConfig,
updateInstallConfig
updateInstallConfig,
updateSmartRoutingConfig,
updateSmartRoutingConfigBatch
} = useSettingsData();
// Update local installConfig when savedInstallConfig changes
@@ -43,13 +58,26 @@ const SettingsPage: React.FC = () => {
}
}, [savedInstallConfig]);
// Update local tempSmartRoutingConfig when smartRoutingConfig changes
useEffect(() => {
if (smartRoutingConfig) {
setTempSmartRoutingConfig({
dbUrl: smartRoutingConfig.dbUrl || '',
openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '',
openaiApiKey: smartRoutingConfig.openaiApiKey || '',
openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '',
});
}
}, [smartRoutingConfig]);
const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false,
installConfig: false,
smartRoutingConfig: false,
password: false
});
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'password') => {
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'password') => {
setSectionsVisible(prev => ({
...prev,
[section]: !prev[section]
@@ -91,6 +119,59 @@ const SettingsPage: React.FC = () => {
await updateInstallConfig(key, installConfig[key]);
};
const handleSmartRoutingConfigChange = (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', value: string) => {
setTempSmartRoutingConfig({
...tempSmartRoutingConfig,
[key]: value
});
};
const saveSmartRoutingConfig = async (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel') => {
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
};
const handleSmartRoutingEnabledChange = async (value: boolean) => {
// If enabling Smart Routing, validate required fields and save any unsaved changes
if (value) {
const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl;
const currentOpenaiApiKey = tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey;
if (!currentDbUrl || !currentOpenaiApiKey) {
const missingFields = [];
if (!currentDbUrl) missingFields.push(t('settings.dbUrl'));
if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'));
showToast(t('settings.smartRoutingValidationError', {
fields: missingFields.join(', ')
}));
return;
}
// Prepare updates object with unsaved changes and enabled status
const updates: any = { enabled: value };
// Check for unsaved changes and include them in the batch update
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
updates.dbUrl = tempSmartRoutingConfig.dbUrl;
}
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl;
}
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey;
}
if (tempSmartRoutingConfig.openaiApiEmbeddingModel !== smartRoutingConfig.openaiApiEmbeddingModel) {
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel;
}
// Save all changes in a single batch update
await updateSmartRoutingConfigBatch(updates);
} else {
// If disabling, just update the enabled status
await updateSmartRoutingConfig('enabled', value);
}
};
const handlePasswordChangeSuccess = () => {
setTimeout(() => {
navigate('/');
@@ -133,6 +214,131 @@ const SettingsPage: React.FC = () => {
</div>
</div>
{/* Smart Routing Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('smartRoutingConfig')}
>
<h2 className="font-semibold text-gray-800">{t('pages.settings.smartRouting')}</h2>
<span className="text-gray-500">
{sectionsVisible.smartRoutingConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.smartRoutingConfig && (
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableSmartRouting')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableSmartRoutingDescription')}</p>
</div>
<Switch
disabled={loading}
checked={smartRoutingConfig.enabled}
onCheckedChange={(checked) => handleSmartRoutingEnabledChange(checked)}
/>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
<span className="text-red-500 px-1">*</span>{t('settings.dbUrl')}
</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.dbUrl}
onChange={(e) => handleSmartRoutingConfigChange('dbUrl', e.target.value)}
placeholder={t('settings.dbUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('dbUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
<span className="text-red-500 px-1">*</span>{t('settings.openaiApiKey')}
</h3>
</div>
<div className="flex items-center gap-3">
<input
type="password"
value={tempSmartRoutingConfig.openaiApiKey}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiKey', e.target.value)}
placeholder={t('settings.openaiApiKeyPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiKey')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.openaiApiBaseUrl')}</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.openaiApiBaseUrl}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)}
placeholder={t('settings.openaiApiBaseUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiBaseUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.openaiApiEmbeddingModel')}</h3>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempSmartRoutingConfig.openaiApiEmbeddingModel}
onChange={(e) => handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)}
placeholder={t('settings.openaiApiEmbeddingModelPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
disabled={loading}
/>
<button
onClick={() => saveSmartRoutingConfig('openaiApiEmbeddingModel')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
</div>
{/* Route Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div
@@ -296,7 +502,7 @@ const SettingsPage: React.FC = () => {
</div>
)}
</div>
</div>
</div >
);
};