diff --git a/Dockerfile b/Dockerfile index e2b232c..9233ee1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y curl gnupg \ RUN npm install -g pnpm -ARG REQUEST_TIMEOUT=120000 +ARG REQUEST_TIMEOUT=60000 ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT RUN uv tool install mcp-server-fetch diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2aaf293..1486e0e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,19 +1,99 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Server, ApiResponse } from './types' import ServerCard from './components/ServerCard' import AddServerForm from './components/AddServerForm' import EditServerForm from './components/EditServerForm' +// 配置选项 +const CONFIG = { + // 初始化启动阶段的配置 + startup: { + maxAttempts: 60, // 初始化阶段最大尝试次数 + pollingInterval: 3000 // 初始阶段轮询间隔 (3秒) + }, + // 正常运行阶段的配置 + normal: { + pollingInterval: 10000 // 正常运行时的轮询间隔 (10秒) + } +} + function App() { const { t } = useTranslation() const [servers, setServers] = useState([]) const [error, setError] = useState(null) const [refreshKey, setRefreshKey] = useState(0) const [editingServer, setEditingServer] = useState(null) + const [isInitialLoading, setIsInitialLoading] = useState(true) + const [fetchAttempts, setFetchAttempts] = useState(0) + + // 轮询定时器引用 + const intervalRef = useRef(null) + // 保存当前尝试次数,避免依赖循环 + const attemptsRef = useRef(0) + + // 清理定时器 + const clearTimer = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + } + + // 开始正常轮询 + const startNormalPolling = useCallback(() => { + // 确保没有其他定时器在运行 + clearTimer() + + const fetchServers = async () => { + try { + const response = await fetch('/api/servers') + const data = await response.json() + + if (data && data.success && Array.isArray(data.data)) { + setServers(data.data) + } else if (data && Array.isArray(data)) { + setServers(data) + } else { + console.error('Invalid server data format:', data) + setServers([]) + } + + // 重置错误状态 + setError(null) + } catch (err) { + console.error('Error fetching servers during normal polling:', err) + + // 使用友好的错误消息 + if (!navigator.onLine) { + setError(t('errors.network')) + } else if (err instanceof TypeError && ( + err.message.includes('NetworkError') || + err.message.includes('Failed to fetch') + )) { + setError(t('errors.serverConnection')) + } else { + setError(t('errors.serverFetch')) + } + } + } + + // 立即执行一次 + fetchServers() + + // 设置定期轮询 + intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval) + }, [t]) useEffect(() => { - const fetchServers = async () => { + // 重置尝试计数 + if (refreshKey > 0) { + attemptsRef.current = 0; + setFetchAttempts(0); + } + + // 初始化加载阶段的请求函数 + const fetchInitialData = async () => { try { const response = await fetch('/api/servers') const data = await response.json() @@ -21,25 +101,92 @@ function App() { // 处理API响应中的包装对象,提取data字段 if (data && data.success && Array.isArray(data.data)) { setServers(data.data) + setIsInitialLoading(false) + // 初始化成功,开始正常轮询 + startNormalPolling() + return true } else if (data && Array.isArray(data)) { // 兼容性处理,如果API直接返回数组 setServers(data) + setIsInitialLoading(false) + // 初始化成功,开始正常轮询 + startNormalPolling() + return true } else { // 如果数据格式不符合预期,设置为空数组 console.error('Invalid server data format:', data) setServers([]) + setIsInitialLoading(false) + // 初始化成功但数据为空,开始正常轮询 + startNormalPolling() + return true } } catch (err) { - setError(err instanceof Error ? err.message : String(err)) + // 增加尝试次数计数,使用 ref 避免触发 effect 重新运行 + attemptsRef.current += 1; + console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err) + + // 更新状态用于显示 + setFetchAttempts(attemptsRef.current) + + // 设置适当的错误消息 + if (!navigator.onLine) { + setError(t('errors.network')) + } else { + setError(t('errors.initialStartup')) + } + + // 如果已超过最大尝试次数,放弃初始化并切换到正常轮询 + if (attemptsRef.current >= CONFIG.startup.maxAttempts) { + console.log('Maximum startup attempts reached, switching to normal polling') + setIsInitialLoading(false) + // 清除初始化的轮询 + clearTimer() + // 切换到正常轮询模式 + startNormalPolling() + } + + return false } } - fetchServers() + // 组件挂载时,根据当前状态设置适当的轮询 + if (isInitialLoading) { + // 确保没有其他定时器在运行 + clearTimer() + + // 立即执行一次初始请求 + fetchInitialData() + + // 设置初始阶段的轮询间隔 + intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval) + console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`) + } else { + // 已经初始化完成,开始正常轮询 + startNormalPolling() + } + + // 清理函数 + return () => { + clearTimer() + } + }, [refreshKey, t, isInitialLoading, startNormalPolling]) - // Poll for updates every 5 seconds - const interval = setInterval(fetchServers, 5000) - return () => clearInterval(interval) - }, [refreshKey]) + // 手动触发刷新 + const triggerRefresh = () => { + // 清除当前的定时器 + clearTimer() + + // 如果在初始化阶段,重置初始化状态 + if (isInitialLoading) { + setIsInitialLoading(true) + attemptsRef.current = 0 + setFetchAttempts(0) + } + + // refreshKey 的改变会触发 useEffect 再次运行 + setRefreshKey(prevKey => prevKey + 1) + } const handleServerAdd = () => { setRefreshKey(prevKey => prevKey + 1) @@ -101,28 +248,29 @@ function App() { } } - if (error) { - return ( -
-
-
-

{t('app.error')}

-

{error}

- -
-
-
- ) - } - return (
+ {error && ( +
+
+
+

{t('app.error')}

+

{error}

+
+ +
+
+ )} +

{t('app.title')}

diff --git a/frontend/src/components/AddServerForm.tsx b/frontend/src/components/AddServerForm.tsx index 5af373b..9db9764 100644 --- a/frontend/src/components/AddServerForm.tsx +++ b/frontend/src/components/AddServerForm.tsx @@ -9,13 +9,16 @@ interface AddServerFormProps { const AddServerForm = ({ onAdd }: AddServerFormProps) => { const { t } = useTranslation() const [modalVisible, setModalVisible] = useState(false) + const [error, setError] = useState(null) const toggleModal = () => { setModalVisible(!modalVisible) + setError(null) // Clear any previous errors when toggling modal } const handleSubmit = async (payload: any) => { try { + setError(null) const response = await fetch('/api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -25,14 +28,35 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => { const result = await response.json() if (!response.ok) { - alert(result.message || 'Failed to add server') + // Use specific error message from the response if available + if (result && result.message) { + setError(result.message) + } else if (response.status === 400) { + setError(t('server.invalidData')) + } else if (response.status === 409) { + setError(t('server.alreadyExists', { serverName: payload.name })) + } else { + setError(t('server.addError')) + } return } setModalVisible(false) onAdd() } catch (err) { - alert(`Error: ${err instanceof Error ? err.message : String(err)}`) + console.error('Error adding server:', err) + + // Use friendly error messages based on error type + if (!navigator.onLine) { + setError(t('errors.network')) + } else if (err instanceof TypeError && ( + err.message.includes('NetworkError') || + err.message.includes('Failed to fetch') + )) { + setError(t('errors.serverConnection')) + } else { + setError(t('errors.serverAdd')) + } } } @@ -47,7 +71,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => { {modalVisible && (
- +
)}
diff --git a/frontend/src/components/EditServerForm.tsx b/frontend/src/components/EditServerForm.tsx index 038e402..71981d7 100644 --- a/frontend/src/components/EditServerForm.tsx +++ b/frontend/src/components/EditServerForm.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Server } from '@/types' import ServerForm from './ServerForm' @@ -10,9 +11,11 @@ interface EditServerFormProps { const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => { const { t } = useTranslation() + const [error, setError] = useState(null) const handleSubmit = async (payload: any) => { try { + setError(null) const response = await fetch(`/api/servers/${server.name}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -22,13 +25,34 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => { const result = await response.json() if (!response.ok) { - alert(result.message || t('server.updateError', 'Failed to update server')) + // Use specific error message from the response if available + if (result && result.message) { + setError(result.message) + } else if (response.status === 404) { + setError(t('server.notFound', { serverName: server.name })) + } else if (response.status === 400) { + setError(t('server.invalidData')) + } else { + setError(t('server.updateError', { serverName: server.name })) + } return } onEdit() } catch (err) { - alert(`${t('errors.general')}: ${err instanceof Error ? err.message : String(err)}`) + console.error('Error updating server:', err) + + // Use friendly error messages based on error type + if (!navigator.onLine) { + setError(t('errors.network')) + } else if (err instanceof TypeError && ( + err.message.includes('NetworkError') || + err.message.includes('Failed to fetch') + )) { + setError(t('errors.serverConnection')) + } else { + setError(t('errors.serverUpdate', { serverName: server.name })) + } } } @@ -39,6 +63,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => { onCancel={onCancel} initialData={server} modalTitle={t('server.editTitle', {serverName: server.name})} + formError={error} />
) diff --git a/frontend/src/components/ServerForm.tsx b/frontend/src/components/ServerForm.tsx index 6f3fbc3..a935bce 100644 --- a/frontend/src/components/ServerForm.tsx +++ b/frontend/src/components/ServerForm.tsx @@ -7,9 +7,10 @@ interface ServerFormProps { onCancel: () => void initialData?: Server | null modalTitle: string + formError?: string | null } -const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: ServerFormProps) => { +const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => { const { t } = useTranslation() const [serverType, setServerType] = useState<'sse' | 'stdio'>( initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio', @@ -104,7 +105,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
- {error &&
{error}
} + {(error || formError) && ( +
+ {formError || error} +
+ )}
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 3619a3f..d798bbf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -19,6 +19,8 @@ "save": "Save Changes", "cancel": "Cancel", "invalidConfig": "Could not find configuration data for {{serverName}}", + "addError": "Failed to add server", + "editError": "Failed to edit server {{serverName}}", "deleteError": "Failed to delete server {{serverName}}", "updateError": "Failed to update server", "editTitle": "Edit Server: {{serverName}}", @@ -36,6 +38,12 @@ "connecting": "Connecting" }, "errors": { - "general": "Something went wrong" + "general": "Something went wrong", + "network": "Network connection error. Please check your internet connection", + "serverConnection": "Unable to connect to the server. Please check if the server is running", + "serverAdd": "Failed to add server. Please check the server status", + "serverUpdate": "Failed to edit server {{serverName}}. Please check the server status", + "serverFetch": "Failed to retrieve server data. Please try again later", + "initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch..." } } \ No newline at end of file diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 0e6a087..84437a7 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -18,6 +18,8 @@ "apiKey": "API 密钥", "save": "保存更改", "cancel": "取消", + "addError": "添加服务器失败", + "editError": "编辑服务器 {{serverName}} 失败", "invalidConfig": "无法找到 {{serverName}} 的配置数据", "deleteError": "删除服务器 {{serverName}} 失败", "updateError": "更新服务器失败", @@ -36,6 +38,12 @@ "connecting": "连接中" }, "errors": { - "general": "发生错误" + "general": "发生错误", + "network": "网络连接错误,请检查您的互联网连接", + "serverConnection": "无法连接到服务器,请检查服务器是否正在运行", + "serverAdd": "添加服务器失败,请检查服务器状态", + "serverUpdate": "编辑服务器 {{serverName}} 失败,请检查服务器状态", + "serverFetch": "获取服务器数据失败,请稍后重试", + "initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..." } } \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts index cb662c0..cc9f9e0 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -7,7 +7,7 @@ dotenv.config(); const defaultConfig = { port: process.env.PORT || 3000, - timeout: process.env.REQUEST_TIMEOUT || 120000, + timeout: process.env.REQUEST_TIMEOUT || 60000, mcpHubName: 'mcphub', mcpHubVersion: '0.0.1', };