mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
Add Chinese localization support and i18n middleware (#253)
This commit is contained in:
@@ -50,9 +50,8 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
}
|
||||
|
||||
const result = await createGroup(formData.name, formData.description, formData.servers)
|
||||
|
||||
if (!result) {
|
||||
setError(t('groups.createError'))
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.createError'))
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ServerForm from './ServerForm'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import { apiPost } from '../utils/fetchInterceptor'
|
||||
import { detectVariables } from '../utils/variableDetection'
|
||||
|
||||
interface AddServerFormProps {
|
||||
@@ -34,26 +34,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
const submitServer = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || ''
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const result = await apiPost('/servers', payload)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (!result.success) {
|
||||
// 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'))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { apiPost, apiGet, apiPut, fetchWithInterceptors } from '@/utils/fetchInterceptor';
|
||||
import { getApiUrl } from '@/utils/runtime';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
|
||||
@@ -81,12 +82,8 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
||||
const formData = new FormData();
|
||||
formData.append('dxtFile', selectedFile);
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/dxt/upload'), {
|
||||
const response = await fetchWithInterceptors(getApiUrl('/dxt/upload'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
@@ -119,19 +116,11 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
||||
// Convert DXT manifest to MCPHub stdio server configuration
|
||||
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
|
||||
// First, check if server exists
|
||||
if (!forceOverride) {
|
||||
const checkResponse = await fetch(getApiUrl('/servers'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const checkResult = await apiGet('/servers');
|
||||
|
||||
if (checkResponse.ok) {
|
||||
const checkResult = await checkResponse.json();
|
||||
if (checkResult.success) {
|
||||
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
|
||||
|
||||
if (existingServer) {
|
||||
@@ -145,25 +134,17 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
||||
}
|
||||
|
||||
// Install or override the server
|
||||
const method = forceOverride ? 'PUT' : 'POST';
|
||||
const url = forceOverride ? getApiUrl(`/servers/${encodeURIComponent(serverName)}`) : getApiUrl('/servers');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
let result;
|
||||
if (forceOverride) {
|
||||
result = await apiPut(`/servers/${encodeURIComponent(serverName)}`, {
|
||||
name: serverName,
|
||||
config: serverConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
||||
});
|
||||
} else {
|
||||
result = await apiPost('/servers', {
|
||||
name: serverName,
|
||||
config: serverConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -56,8 +56,8 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
servers: formData.servers
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
setError(t('groups.updateError'))
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.updateError'))
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import { apiPut } from '../utils/fetchInterceptor'
|
||||
import ServerForm from './ServerForm'
|
||||
|
||||
interface EditServerFormProps {
|
||||
@@ -17,26 +17,12 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || ''
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const result = await apiPut(`/servers/${server.name}`, payload)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (!result.success) {
|
||||
// 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 }))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Group, ApiResponse, IGroupServerConfig } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../utils/fetchInterceptor';
|
||||
|
||||
export const useGroupData = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,18 +13,7 @@ export const useGroupData = () => {
|
||||
const fetchGroups = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/groups'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<Group[]> = await response.json();
|
||||
const data: ApiResponse<Group[]> = await apiGet('/groups');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setGroups(data.data);
|
||||
@@ -55,25 +44,16 @@ export const useGroupData = () => {
|
||||
servers: string[] | IGroupServerConfig[] = [],
|
||||
) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/groups'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ name, description, servers }),
|
||||
});
|
||||
const result: ApiResponse<Group> = await apiPost('/groups', { name, description, servers });
|
||||
console.log('Group created successfully:', result);
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.createError'));
|
||||
return null;
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.createError'));
|
||||
return result;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
return result.data || null;
|
||||
return result || null;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create group');
|
||||
return null;
|
||||
@@ -86,25 +66,14 @@ export const useGroupData = () => {
|
||||
data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] },
|
||||
) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.updateError'));
|
||||
return null;
|
||||
const result: ApiResponse<Group> = await apiPut(`/groups/${id}`, data);
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.updateError'));
|
||||
return result;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
return result.data || null;
|
||||
return result || null;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update group');
|
||||
return null;
|
||||
@@ -114,20 +83,12 @@ export const useGroupData = () => {
|
||||
// Update servers in a group (for batch updates)
|
||||
const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ servers }),
|
||||
const result: ApiResponse<Group> = await apiPut(`/groups/${groupId}/servers/batch`, {
|
||||
servers,
|
||||
});
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.updateError'));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.updateError'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -142,46 +103,29 @@ export const useGroupData = () => {
|
||||
// Delete a group
|
||||
const deleteGroup = async (id: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.deleteError'));
|
||||
return false;
|
||||
const result = await apiDelete(`/groups/${id}`);
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.deleteError'));
|
||||
return result;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
return true;
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete group');
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Add server to a group
|
||||
const addServerToGroup = async (groupId: string, serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ serverName }),
|
||||
const result: ApiResponse<Group> = await apiPost(`/groups/${groupId}/servers`, {
|
||||
serverName,
|
||||
});
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.serverAddError'));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.serverAddError'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -196,18 +140,12 @@ export const useGroupData = () => {
|
||||
// Remove server from group
|
||||
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/${serverName}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const result: ApiResponse<Group> = await apiDelete(
|
||||
`/groups/${groupId}/servers/${serverName}`,
|
||||
);
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.serverRemoveError'));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.serverRemoveError'));
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MarketServer, ApiResponse, ServerConfig } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPost } from '../utils/fetchInterceptor';
|
||||
|
||||
export const useMarketData = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,18 +26,7 @@ export const useMarketData = () => {
|
||||
const fetchMarketServers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/market/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet('/market/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
@@ -87,18 +76,7 @@ export const useMarketData = () => {
|
||||
// Fetch all categories
|
||||
const fetchCategories = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/market/categories'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<string[]> = await response.json();
|
||||
const data: ApiResponse<string[]> = await apiGet('/market/categories');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setCategories(data.data);
|
||||
@@ -113,18 +91,7 @@ export const useMarketData = () => {
|
||||
// Fetch all tags
|
||||
const fetchTags = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/market/tags'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<string[]> = await response.json();
|
||||
const data: ApiResponse<string[]> = await apiGet('/market/tags');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setTags(data.data);
|
||||
@@ -141,18 +108,7 @@ export const useMarketData = () => {
|
||||
async (name: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/market/servers/${name}`), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer> = await response.json();
|
||||
const data: ApiResponse<MarketServer> = await apiGet(`/market/servers/${name}`);
|
||||
|
||||
if (data && data.success && data.data) {
|
||||
setCurrentServer(data.data);
|
||||
@@ -186,22 +142,10 @@ export const useMarketData = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(
|
||||
getApiUrl(`/market/servers/search?query=${encodeURIComponent(query)}`),
|
||||
{
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
},
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||
`/market/servers/search?query=${encodeURIComponent(query)}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
@@ -233,22 +177,10 @@ export const useMarketData = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(
|
||||
getApiUrl(`/market/categories/${encodeURIComponent(category)}`),
|
||||
{
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
},
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||
`/market/categories/${encodeURIComponent(category)}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
@@ -280,18 +212,9 @@ export const useMarketData = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/market/tags/${encodeURIComponent(tag)}`), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||
`/market/tags/${encodeURIComponent(tag)}`,
|
||||
);
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
@@ -314,18 +237,7 @@ export const useMarketData = () => {
|
||||
// Fetch installed servers
|
||||
const fetchInstalledServers = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await apiGet<{ success: boolean; data: any[] }>('/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
// Extract server names
|
||||
@@ -365,27 +277,24 @@ export const useMarketData = () => {
|
||||
// Prepare server configuration, merging with customConfig
|
||||
const serverConfig = {
|
||||
name: server.name,
|
||||
config: customConfig.type === 'stdio' ? {
|
||||
command: customConfig.command || installation.command || '',
|
||||
args: customConfig.args || installation.args || [],
|
||||
env: { ...installation.env, ...customConfig.env },
|
||||
} : customConfig
|
||||
config:
|
||||
customConfig.type === 'stdio'
|
||||
? {
|
||||
command: customConfig.command || installation.command || '',
|
||||
args: customConfig.args || installation.args || [],
|
||||
env: { ...installation.env, ...customConfig.env },
|
||||
}
|
||||
: customConfig,
|
||||
};
|
||||
|
||||
// Call the createServer API
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify(serverConfig),
|
||||
});
|
||||
const result = await apiPost<{ success: boolean; message?: string }>(
|
||||
'/servers',
|
||||
serverConfig,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Status: ${response.status}`);
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Failed to install server');
|
||||
}
|
||||
|
||||
// Update installed servers list after successful installation
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server, ApiResponse } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPost, apiDelete } from '../utils/fetchInterceptor';
|
||||
|
||||
// Configuration options
|
||||
const CONFIG = {
|
||||
@@ -44,13 +44,7 @@ export const useServerData = () => {
|
||||
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await apiGet('/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setServers(data.data);
|
||||
@@ -97,13 +91,7 @@ export const useServerData = () => {
|
||||
// Initialization phase request function
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await apiGet('/servers');
|
||||
|
||||
// Handle API response wrapper object, extract data field
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
@@ -203,14 +191,8 @@ export const useServerData = () => {
|
||||
const handleServerEdit = async (server: Server) => {
|
||||
try {
|
||||
// Fetch settings to get the full server config before editing
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();
|
||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> =
|
||||
await apiGet('/settings');
|
||||
|
||||
if (
|
||||
settingsData &&
|
||||
@@ -240,17 +222,10 @@ export const useServerData = () => {
|
||||
|
||||
const handleServerRemove = async (serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/servers/${serverName}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
const result = await apiDelete(`/servers/${serverName}`);
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('server.deleteError', { serverName }));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('server.deleteError', { serverName }));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -264,21 +239,11 @@ export const useServerData = () => {
|
||||
|
||||
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
const result = await apiPost(`/servers/${server.name}/toggle`, { enabled });
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (!result || !result.success) {
|
||||
console.error('Failed to toggle server:', result);
|
||||
setError(t('server.toggleError', { serverName: server.name }));
|
||||
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApiResponse } from '@/types';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPut } from '../utils/fetchInterceptor';
|
||||
|
||||
// Define types for the settings data
|
||||
interface RoutingConfig {
|
||||
@@ -84,18 +84,7 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<SystemSettings> = await response.json();
|
||||
const data: ApiResponse<SystemSettings> = await apiGet('/settings');
|
||||
|
||||
if (data.success && data.data?.systemConfig?.routing) {
|
||||
setRoutingConfig({
|
||||
@@ -134,34 +123,17 @@ export const useSettingsData = () => {
|
||||
}, [t]); // 移除 showToast 依赖
|
||||
|
||||
// Update routing configuration
|
||||
const updateRoutingConfig = async <T extends keyof RoutingConfig>(
|
||||
key: T,
|
||||
value: RoutingConfig[T],
|
||||
) => {
|
||||
const updateRoutingConfig = async (key: keyof RoutingConfig, value: any) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
const data = await apiPut('/system-config', {
|
||||
routing: {
|
||||
[key]: value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
routing: {
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setRoutingConfig({
|
||||
...routingConfig,
|
||||
@@ -170,7 +142,7 @@ export const useSettingsData = () => {
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateRouteConfig'));
|
||||
showToast(data.message || t('errors.failedToUpdateRouteConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -189,26 +161,12 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
const data = await apiPut('/system-config', {
|
||||
install: {
|
||||
[key]: value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
install: {
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setInstallConfig({
|
||||
...installConfig,
|
||||
@@ -217,7 +175,7 @@ export const useSettingsData = () => {
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateSystemConfig'));
|
||||
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -239,27 +197,12 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
const data = await apiPut('/system-config', {
|
||||
smartRouting: {
|
||||
[key]: value,
|
||||
},
|
||||
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,
|
||||
@@ -289,25 +232,10 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
smartRouting: updates,
|
||||
}),
|
||||
const data = await apiPut('/system-config', {
|
||||
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,
|
||||
@@ -337,24 +265,10 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
routing: updates,
|
||||
}),
|
||||
const data = await apiPut('/system-config', {
|
||||
routing: updates,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setRoutingConfig({
|
||||
...routingConfig,
|
||||
@@ -363,7 +277,7 @@ export const useSettingsData = () => {
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateRouteConfig'));
|
||||
showToast(data.message || t('errors.failedToUpdateRouteConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,9 +2,9 @@ import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// Import translations
|
||||
import enTranslation from './locales/en.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
// Import shared translations from root locales directory
|
||||
import enTranslation from '../../locales/en.json';
|
||||
import zhTranslation from '../../locales/zh.json';
|
||||
|
||||
i18n
|
||||
// Detect user language
|
||||
@@ -15,18 +15,18 @@ i18n
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation
|
||||
translation: enTranslation,
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation
|
||||
}
|
||||
translation: zhTranslation,
|
||||
},
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
|
||||
// Common namespace used for all translations
|
||||
defaultNS: 'translation',
|
||||
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // React already safe from XSS
|
||||
},
|
||||
@@ -36,7 +36,7 @@ i18n
|
||||
order: ['localStorage', 'cookie', 'htmlTag', 'navigator'],
|
||||
// Cache the language in localStorage
|
||||
caches: ['localStorage', 'cookie'],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "MCP Hub Dashboard",
|
||||
"error": "Error",
|
||||
"closeButton": "Close",
|
||||
"noServers": "No MCP servers available",
|
||||
"loading": "Loading...",
|
||||
"logout": "Logout",
|
||||
"profile": "Profile",
|
||||
"changePassword": "Change Password",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"welcomeUser": "Welcome, {{username}}",
|
||||
"name": "MCP Hub"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"versionInfo": "MCP Hub Version: {{version}}",
|
||||
"newVersion": "New version available!",
|
||||
"currentVersion": "Current version",
|
||||
"newVersionAvailable": "New version {{version}} is available",
|
||||
"viewOnGitHub": "View on GitHub",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
"checking": "Checking for updates..."
|
||||
},
|
||||
"profile": {
|
||||
"viewProfile": "View profile",
|
||||
"userCenter": "User Center"
|
||||
},
|
||||
"sponsor": {
|
||||
"label": "Sponsor",
|
||||
"title": "Support the Project",
|
||||
"rewardAlt": "Reward QR Code",
|
||||
"supportMessage": "Support the development of MCP Hub by buying me a coffee!",
|
||||
"supportButton": "Support on Ko-fi"
|
||||
},
|
||||
"wechat": {
|
||||
"label": "WeChat",
|
||||
"title": "Connect via WeChat",
|
||||
"qrCodeAlt": "WeChat QR Code",
|
||||
"scanMessage": "Scan this QR code to connect with us on WeChat"
|
||||
},
|
||||
"discord": {
|
||||
"label": "Discord",
|
||||
"title": "Join our Discord server",
|
||||
"community": "Join our growing community on Discord for support, discussions, and updates!"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"loginTitle": "Login to MCP Hub",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"loggingIn": "Logging in...",
|
||||
"emptyFields": "Username and password cannot be empty",
|
||||
"loginFailed": "Login failed, please check your username and password",
|
||||
"loginError": "An error occurred during login",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"passwordsNotMatch": "New password and confirmation do not match",
|
||||
"changePasswordSuccess": "Password changed successfully",
|
||||
"changePasswordError": "Failed to change password",
|
||||
"changePassword": "Change Password",
|
||||
"passwordChanged": "Password changed successfully",
|
||||
"passwordChangeError": "Failed to change password"
|
||||
},
|
||||
"server": {
|
||||
"addServer": "Add Server",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"confirmDelete": "Are you sure you want to delete this server?",
|
||||
"deleteWarning": "Deleting server '{{name}}' will remove it and all its data. This action cannot be undone.",
|
||||
"status": "Status",
|
||||
"tools": "Tools",
|
||||
"name": "Server Name",
|
||||
"url": "Server URL",
|
||||
"apiKey": "API Key",
|
||||
"save": "Save",
|
||||
"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}}",
|
||||
"type": "Server Type",
|
||||
"command": "Command",
|
||||
"arguments": "Arguments",
|
||||
"envVars": "Environment Variables",
|
||||
"headers": "HTTP Headers",
|
||||
"key": "key",
|
||||
"value": "value",
|
||||
"enabled": "Enabled",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"requestOptions": "Configuration",
|
||||
"timeout": "Request Timeout",
|
||||
"timeoutDescription": "Timeout for requests to the MCP server (ms)",
|
||||
"maxTotalTimeout": "Maximum Total Timeout",
|
||||
"maxTotalTimeoutDescription": "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||||
"resetTimeoutOnProgress": "Reset Timeout on Progress",
|
||||
"resetTimeoutOnProgressDescription": "Reset timeout on progress notifications",
|
||||
"remove": "Remove",
|
||||
"toggleError": "Failed to toggle server {{serverName}}",
|
||||
"alreadyExists": "Server {{serverName}} already exists",
|
||||
"invalidData": "Invalid server data provided",
|
||||
"notFound": "Server {{serverName}} not found",
|
||||
"namePlaceholder": "Enter server name",
|
||||
"urlPlaceholder": "Enter server URL",
|
||||
"commandPlaceholder": "Enter command",
|
||||
"argumentsPlaceholder": "Enter arguments",
|
||||
"errorDetails": "Error Details",
|
||||
"viewErrorDetails": "View error details",
|
||||
"confirmVariables": "Confirm Variable Configuration",
|
||||
"variablesDetected": "Variables detected in configuration. Please confirm these variables are properly configured:",
|
||||
"detectedVariables": "Detected Variables",
|
||||
"confirmVariablesMessage": "Please ensure these variables are properly defined in your runtime environment. Continue adding server?",
|
||||
"confirmAndAdd": "Confirm and Add",
|
||||
"openapi": {
|
||||
"inputMode": "Input Mode",
|
||||
"inputModeUrl": "Specification URL",
|
||||
"inputModeSchema": "JSON Schema",
|
||||
"specUrl": "OpenAPI Specification URL",
|
||||
"schema": "OpenAPI JSON Schema",
|
||||
"schemaHelp": "Paste your complete OpenAPI JSON schema here",
|
||||
"security": "Security Type",
|
||||
"securityNone": "None",
|
||||
"securityApiKey": "API Key",
|
||||
"securityHttp": "HTTP Authentication",
|
||||
"securityOAuth2": "OAuth 2.0",
|
||||
"securityOpenIdConnect": "OpenID Connect",
|
||||
"apiKeyConfig": "API Key Configuration",
|
||||
"apiKeyName": "Header/Parameter Name",
|
||||
"apiKeyIn": "Location",
|
||||
"apiKeyValue": "API Key Value",
|
||||
"httpAuthConfig": "HTTP Authentication Configuration",
|
||||
"httpScheme": "Authentication Scheme",
|
||||
"httpCredentials": "Credentials",
|
||||
"oauth2Config": "OAuth 2.0 Configuration",
|
||||
"oauth2Token": "Access Token",
|
||||
"openIdConnectConfig": "OpenID Connect Configuration",
|
||||
"openIdConnectUrl": "Discovery URL",
|
||||
"openIdConnectToken": "ID Token"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"connecting": "Connecting"
|
||||
},
|
||||
"errors": {
|
||||
"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...",
|
||||
"serverInstall": "Failed to install server",
|
||||
"failedToFetchSettings": "Failed to fetch settings",
|
||||
"failedToUpdateRouteConfig": "Failed to update route configuration",
|
||||
"failedToUpdateSmartRoutingConfig": "Failed to update smart routing configuration"
|
||||
},
|
||||
"common": {
|
||||
"processing": "Processing...",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"refresh": "Refresh",
|
||||
"create": "Create",
|
||||
"creating": "Creating...",
|
||||
"update": "Update",
|
||||
"updating": "Updating...",
|
||||
"submitting": "Submitting...",
|
||||
"delete": "Delete",
|
||||
"remove": "Remove",
|
||||
"copy": "Copy",
|
||||
"copyId": "Copy ID",
|
||||
"copyUrl": "Copy URL",
|
||||
"copyJson": "Copy JSON",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"copyFailed": "Copy failed",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"language": "Language"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"servers": "Servers",
|
||||
"groups": "Groups",
|
||||
"users": "Users",
|
||||
"settings": "Settings",
|
||||
"changePassword": "Change Password",
|
||||
"market": "Market",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"totalServers": "Total",
|
||||
"onlineServers": "Online",
|
||||
"offlineServers": "Offline",
|
||||
"connectingServers": "Connecting",
|
||||
"recentServers": "Recent Servers"
|
||||
},
|
||||
"servers": {
|
||||
"title": "Servers Management"
|
||||
},
|
||||
"groups": {
|
||||
"title": "Group Management"
|
||||
},
|
||||
"users": {
|
||||
"title": "User Management"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"language": "Language",
|
||||
"account": "Account Settings",
|
||||
"password": "Change Password",
|
||||
"appearance": "Appearance",
|
||||
"routeConfig": "Security",
|
||||
"installConfig": "Installation",
|
||||
"smartRouting": "Smart Routing"
|
||||
},
|
||||
"market": {
|
||||
"title": "Server Market - (Data from mcpm.sh)"
|
||||
},
|
||||
"logs": {
|
||||
"title": "System Logs"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"filters": "Filters",
|
||||
"search": "Search logs...",
|
||||
"autoScroll": "Auto-scroll",
|
||||
"clearLogs": "Clear logs",
|
||||
"loading": "Loading logs...",
|
||||
"noLogs": "No logs available.",
|
||||
"noMatch": "No logs match the current filters.",
|
||||
"mainProcess": "Main Process",
|
||||
"childProcess": "Child Process",
|
||||
"main": "Main",
|
||||
"child": "Child"
|
||||
},
|
||||
"groups": {
|
||||
"add": "Add",
|
||||
"addNew": "Add New Group",
|
||||
"edit": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"confirmDelete": "Are you sure you want to delete this group?",
|
||||
"deleteWarning": "Deleting group '{{name}}' will remove it and all its server associations. This action cannot be undone.",
|
||||
"name": "Group Name",
|
||||
"namePlaceholder": "Enter group name",
|
||||
"nameRequired": "Group name is required",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Enter group description (optional)",
|
||||
"createError": "Failed to create group",
|
||||
"updateError": "Failed to update group",
|
||||
"deleteError": "Failed to delete group",
|
||||
"serverAddError": "Failed to add server to group",
|
||||
"serverRemoveError": "Failed to remove server from group",
|
||||
"addServer": "Add Server to Group",
|
||||
"selectServer": "Select a server to add",
|
||||
"servers": "Servers in Group",
|
||||
"remove": "Remove",
|
||||
"noGroups": "No groups available. Create a new group to get started.",
|
||||
"noServers": "No servers in this group.",
|
||||
"noServerOptions": "No servers available",
|
||||
"serverCount": "{{count}} Servers",
|
||||
"toolSelection": "Tool Selection",
|
||||
"toolsSelected": "Selected",
|
||||
"allTools": "All",
|
||||
"selectedTools": "Selected tools",
|
||||
"selectAll": "Select All",
|
||||
"selectNone": "Select None",
|
||||
"configureTools": "Configure Tools"
|
||||
},
|
||||
"market": {
|
||||
"title": "Server Market",
|
||||
"official": "Official",
|
||||
"by": "By",
|
||||
"unknown": "Unknown",
|
||||
"tools": "tools",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Search for servers by name, category, or tags",
|
||||
"clearFilters": "Clear",
|
||||
"clearCategoryFilter": "",
|
||||
"clearTagFilter": "",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"showTags": "Show tags",
|
||||
"hideTags": "Hide tags",
|
||||
"moreTags": "",
|
||||
"noServers": "No servers found matching your search",
|
||||
"backToList": "Back to list",
|
||||
"install": "Install",
|
||||
"installing": "Installing...",
|
||||
"installed": "Installed",
|
||||
"installServer": "Install Server: {{name}}",
|
||||
"installSuccess": "Server {{serverName}} installed successfully",
|
||||
"author": "Author",
|
||||
"license": "License",
|
||||
"repository": "Repository",
|
||||
"examples": "Examples",
|
||||
"arguments": "Arguments",
|
||||
"argumentName": "Name",
|
||||
"description": "Description",
|
||||
"required": "Required",
|
||||
"example": "Example",
|
||||
"viewSchema": "View schema",
|
||||
"fetchError": "Error fetching market servers",
|
||||
"serverNotFound": "Server not found",
|
||||
"searchError": "Error searching servers",
|
||||
"filterError": "Error filtering servers by category",
|
||||
"tagFilterError": "Error filtering servers by tag",
|
||||
"noInstallationMethod": "No installation method available for this server",
|
||||
"showing": "Showing {{from}}-{{to}} of {{total}} servers",
|
||||
"perPage": "Per page",
|
||||
"confirmVariablesMessage": "Please ensure these variables are properly defined in your runtime environment. Continue installing server?",
|
||||
"confirmAndInstall": "Confirm and Install"
|
||||
},
|
||||
"tool": {
|
||||
"run": "Run",
|
||||
"running": "Running...",
|
||||
"runTool": "Run Tool",
|
||||
"cancel": "Cancel",
|
||||
"noDescription": "No description available",
|
||||
"inputSchema": "Input Schema:",
|
||||
"runToolWithName": "Run Tool: {{name}}",
|
||||
"execution": "Tool Execution",
|
||||
"successful": "Successful",
|
||||
"failed": "Failed",
|
||||
"result": "Result:",
|
||||
"error": "Error",
|
||||
"errorDetails": "Error Details:",
|
||||
"noContent": "Tool executed successfully but returned no content.",
|
||||
"unknownError": "Unknown error occurred",
|
||||
"jsonResponse": "JSON Response:",
|
||||
"toolResult": "Tool result",
|
||||
"noParameters": "This tool does not require any parameters.",
|
||||
"selectOption": "Select an option",
|
||||
"enterValue": "Enter {{type}} value",
|
||||
"enabled": "Enabled",
|
||||
"enableSuccess": "Tool {{name}} enabled successfully",
|
||||
"disableSuccess": "Tool {{name}} disabled successfully",
|
||||
"toggleFailed": "Failed to toggle tool status",
|
||||
"parameters": "Tool Parameters",
|
||||
"formMode": "Form Mode",
|
||||
"jsonMode": "JSON Mode",
|
||||
"jsonConfiguration": "JSON Configuration",
|
||||
"invalidJsonFormat": "Invalid JSON format",
|
||||
"fixJsonBeforeSwitching": "Please fix JSON format before switching to form mode",
|
||||
"item": "Item {{index}}",
|
||||
"addItem": "Add {{key}} item",
|
||||
"enterKey": "Enter {{key}}"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "Enable Global Route",
|
||||
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
|
||||
"enableGroupNameRoute": "Enable Group Name Route",
|
||||
"enableGroupNameRouteDescription": "Allow connections to /sse endpoint using group names instead of just group IDs",
|
||||
"enableBearerAuth": "Enable Bearer Authentication",
|
||||
"enableBearerAuthDescription": "Require bearer token authentication for MCP requests",
|
||||
"bearerAuthKey": "Bearer Authentication Key",
|
||||
"bearerAuthKeyDescription": "The authentication key that will be required in the Bearer token",
|
||||
"bearerAuthKeyPlaceholder": "Enter bearer authentication key",
|
||||
"skipAuth": "Skip Authentication",
|
||||
"skipAuthDescription": "Bypass login requirement for frontend and API access (DEFAULT OFF for security)",
|
||||
"pythonIndexUrl": "Python Package Repository URL",
|
||||
"pythonIndexUrlDescription": "Set UV_DEFAULT_INDEX environment variable for Python package installation",
|
||||
"pythonIndexUrlPlaceholder": "e.g. https://pypi.org/simple",
|
||||
"npmRegistry": "NPM Registry URL",
|
||||
"npmRegistryDescription": "Set npm_config_registry environment variable for NPM package installation",
|
||||
"npmRegistryPlaceholder": "e.g. https://registry.npmjs.org/",
|
||||
"baseUrl": "Base URL",
|
||||
"baseUrlDescription": "Base URL for MCP requests",
|
||||
"baseUrlPlaceholder": "e.g. http://localhost:3000",
|
||||
"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 (requires 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}}"
|
||||
},
|
||||
"dxt": {
|
||||
"upload": "Upload",
|
||||
"uploadTitle": "Upload DXT Extension",
|
||||
"dropFileHere": "Drop your .dxt file here",
|
||||
"orClickToSelect": "or click to select from your computer",
|
||||
"invalidFileType": "Please select a valid .dxt file",
|
||||
"noFileSelected": "Please select a .dxt file to upload",
|
||||
"uploading": "Uploading...",
|
||||
"uploadFailed": "Failed to upload DXT file",
|
||||
"installServer": "Install MCP Server from DXT",
|
||||
"extensionInfo": "Extension Information",
|
||||
"name": "Name",
|
||||
"version": "Version",
|
||||
"description": "Description",
|
||||
"author": "Author",
|
||||
"tools": "Tools",
|
||||
"serverName": "Server Name",
|
||||
"serverNamePlaceholder": "Enter a name for this server",
|
||||
"install": "Install",
|
||||
"installing": "Installing...",
|
||||
"installFailed": "Failed to install server from DXT",
|
||||
"serverExistsTitle": "Server Already Exists",
|
||||
"serverExistsConfirm": "Server '{{serverName}}' already exists. Do you want to override it with the new version?",
|
||||
"override": "Override"
|
||||
},
|
||||
"users": {
|
||||
"add": "Add User",
|
||||
"addNew": "Add New User",
|
||||
"edit": "Edit User",
|
||||
"delete": "Delete User",
|
||||
"create": "Create User",
|
||||
"update": "Update User",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"adminRole": "Administrator",
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
"permissions": "Permissions",
|
||||
"adminPermissions": "Full system access",
|
||||
"userPermissions": "Limited access",
|
||||
"currentUser": "You",
|
||||
"noUsers": "No users found",
|
||||
"adminRequired": "Administrator access required to manage users",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordTooShort": "Password must be at least 6 characters long",
|
||||
"passwordMismatch": "Passwords do not match",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"newPasswordPlaceholder": "Leave empty to keep current password",
|
||||
"confirmPasswordPlaceholder": "Confirm new password",
|
||||
"createError": "Failed to create user",
|
||||
"updateError": "Failed to update user",
|
||||
"deleteError": "Failed to delete user",
|
||||
"statsError": "Failed to fetch user statistics",
|
||||
"deleteConfirmation": "Are you sure you want to delete user '{{username}}'? This action cannot be undone.",
|
||||
"confirmDelete": "Delete User",
|
||||
"deleteWarning": "Are you sure you want to delete user '{{username}}'? This action cannot be undone."
|
||||
}
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "MCP Hub 控制面板",
|
||||
"error": "错误",
|
||||
"closeButton": "关闭",
|
||||
"noServers": "没有可用的 MCP 服务器",
|
||||
"loading": "加载中...",
|
||||
"logout": "退出登录",
|
||||
"profile": "个人资料",
|
||||
"changePassword": "修改密码",
|
||||
"toggleSidebar": "切换侧边栏",
|
||||
"welcomeUser": "欢迎, {{username}}",
|
||||
"name": "MCP Hub"
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"versionInfo": "MCP Hub 版本: {{version}}",
|
||||
"newVersion": "有新版本可用!",
|
||||
"currentVersion": "当前版本",
|
||||
"newVersionAvailable": "新版本 {{version}} 已发布",
|
||||
"viewOnGitHub": "在 GitHub 上查看",
|
||||
"checkForUpdates": "检查更新",
|
||||
"checking": "检查更新中..."
|
||||
},
|
||||
"profile": {
|
||||
"viewProfile": "查看个人中心",
|
||||
"userCenter": "个人中心"
|
||||
},
|
||||
"sponsor": {
|
||||
"label": "赞助",
|
||||
"title": "支持项目",
|
||||
"rewardAlt": "赞赏码",
|
||||
"supportMessage": "通过捐赠支持 MCP Hub 的开发!",
|
||||
"supportButton": "在 Ko-fi 上支持"
|
||||
},
|
||||
"wechat": {
|
||||
"label": "微信",
|
||||
"title": "微信联系",
|
||||
"qrCodeAlt": "微信二维码",
|
||||
"scanMessage": "扫描二维码添加微信"
|
||||
},
|
||||
"discord": {
|
||||
"label": "Discord",
|
||||
"title": "加入我们的 Discord 服务器",
|
||||
"community": "加入我们不断壮大的 Discord 社区,获取支持、参与讨论并了解最新动态!"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "系统"
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"loginTitle": "登录 MCP Hub",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"loggingIn": "登录中...",
|
||||
"emptyFields": "用户名和密码不能为空",
|
||||
"loginFailed": "登录失败,请检查用户名和密码",
|
||||
"loginError": "登录过程中出现错误",
|
||||
"currentPassword": "当前密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"passwordsNotMatch": "新密码与确认密码不一致",
|
||||
"changePasswordSuccess": "密码修改成功",
|
||||
"changePasswordError": "修改密码失败",
|
||||
"changePassword": "修改密码",
|
||||
"passwordChanged": "密码修改成功",
|
||||
"passwordChangeError": "修改密码失败"
|
||||
},
|
||||
"server": {
|
||||
"addServer": "添加服务器",
|
||||
"add": "添加",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"confirmDelete": "您确定要删除此服务器吗?",
|
||||
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
|
||||
"status": "状态",
|
||||
"tools": "工具",
|
||||
"name": "服务器名称",
|
||||
"url": "服务器 URL",
|
||||
"apiKey": "API 密钥",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"addError": "添加服务器失败",
|
||||
"editError": "编辑服务器 {{serverName}} 失败",
|
||||
"invalidConfig": "无法找到 {{serverName}} 的配置数据",
|
||||
"deleteError": "删除服务器 {{serverName}} 失败",
|
||||
"updateError": "更新服务器失败",
|
||||
"editTitle": "编辑服务器: {{serverName}}",
|
||||
"type": "服务器类型",
|
||||
"command": "命令",
|
||||
"arguments": "参数",
|
||||
"envVars": "环境变量",
|
||||
"headers": "HTTP 请求头",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"enabled": "已启用",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"requestOptions": "配置",
|
||||
"timeout": "请求超时",
|
||||
"timeoutDescription": "请求超时时间(毫秒)",
|
||||
"maxTotalTimeout": "最大总超时",
|
||||
"maxTotalTimeoutDescription": "无论是否有进度通知的最大总超时时间(毫秒)",
|
||||
"resetTimeoutOnProgress": "收到进度通知时重置超时",
|
||||
"resetTimeoutOnProgressDescription": "适用于发送周期性进度更新的长时间运行操作",
|
||||
"remove": "移除",
|
||||
"toggleError": "切换服务器 {{serverName}} 状态失败",
|
||||
"alreadyExists": "服务器 {{serverName}} 已经存在",
|
||||
"invalidData": "提供的服务器数据无效",
|
||||
"notFound": "找不到服务器 {{serverName}}",
|
||||
"namePlaceholder": "请输入服务器名称",
|
||||
"urlPlaceholder": "请输入服务器URL",
|
||||
"commandPlaceholder": "请输入命令",
|
||||
"argumentsPlaceholder": "请输入参数",
|
||||
"errorDetails": "错误详情",
|
||||
"viewErrorDetails": "查看错误详情",
|
||||
"confirmVariables": "确认变量配置",
|
||||
"variablesDetected": "检测到配置中包含变量,请确认这些变量是否已正确配置:",
|
||||
"detectedVariables": "检测到的变量",
|
||||
"confirmVariablesMessage": "请确保这些变量在运行环境中已正确定义。是否继续添加服务器?",
|
||||
"confirmAndAdd": "确认并添加",
|
||||
"openapi": {
|
||||
"inputMode": "输入模式",
|
||||
"inputModeUrl": "规范 URL",
|
||||
"inputModeSchema": "JSON 模式",
|
||||
"specUrl": "OpenAPI 规范 URL",
|
||||
"schema": "OpenAPI JSON 模式",
|
||||
"schemaHelp": "请在此处粘贴完整的 OpenAPI JSON 模式",
|
||||
"security": "安全类型",
|
||||
"securityNone": "无",
|
||||
"securityApiKey": "API 密钥",
|
||||
"securityHttp": "HTTP 认证",
|
||||
"securityOAuth2": "OAuth 2.0",
|
||||
"securityOpenIdConnect": "OpenID Connect",
|
||||
"apiKeyConfig": "API 密钥配置",
|
||||
"apiKeyName": "请求头/参数名称",
|
||||
"apiKeyIn": "位置",
|
||||
"apiKeyValue": "API 密钥值",
|
||||
"httpAuthConfig": "HTTP 认证配置",
|
||||
"httpScheme": "认证方案",
|
||||
"httpCredentials": "凭据",
|
||||
"oauth2Config": "OAuth 2.0 配置",
|
||||
"oauth2Token": "访问令牌",
|
||||
"openIdConnectConfig": "OpenID Connect 配置",
|
||||
"openIdConnectUrl": "发现 URL",
|
||||
"openIdConnectToken": "ID 令牌"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"connecting": "连接中"
|
||||
},
|
||||
"errors": {
|
||||
"general": "发生错误",
|
||||
"network": "网络连接错误,请检查您的互联网连接",
|
||||
"serverConnection": "无法连接到服务器,请检查服务器是否正在运行",
|
||||
"serverAdd": "添加服务器失败,请检查服务器状态",
|
||||
"serverUpdate": "编辑服务器 {{serverName}} 失败,请检查服务器状态",
|
||||
"serverFetch": "获取服务器数据失败,请稍后重试",
|
||||
"initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候...",
|
||||
"serverInstall": "安装服务器失败",
|
||||
"failedToFetchSettings": "获取设置失败",
|
||||
"failedToUpdateSystemConfig": "更新系统配置失败",
|
||||
"failedToUpdateRouteConfig": "更新路由配置失败",
|
||||
"failedToUpdateSmartRoutingConfig": "更新智能路由配置失败"
|
||||
},
|
||||
"common": {
|
||||
"processing": "处理中...",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"refresh": "刷新",
|
||||
"create": "创建",
|
||||
"creating": "创建中...",
|
||||
"update": "更新",
|
||||
"updating": "更新中...",
|
||||
"submitting": "提交中...",
|
||||
"delete": "删除",
|
||||
"remove": "移除",
|
||||
"copy": "复制",
|
||||
"copyId": "复制ID",
|
||||
"copyUrl": "复制URL",
|
||||
"copyJson": "复制JSON",
|
||||
"copySuccess": "已复制到剪贴板",
|
||||
"copyFailed": "复制失败",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"language": "语言"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "仪表盘",
|
||||
"servers": "服务器",
|
||||
"settings": "设置",
|
||||
"changePassword": "修改密码",
|
||||
"groups": "分组",
|
||||
"users": "用户",
|
||||
"market": "市场",
|
||||
"logs": "日志"
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"title": "仪表盘",
|
||||
"totalServers": "总数",
|
||||
"onlineServers": "在线",
|
||||
"offlineServers": "离线",
|
||||
"connectingServers": "连接中",
|
||||
"recentServers": "最近的服务器"
|
||||
},
|
||||
"servers": {
|
||||
"title": "服务器管理"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"language": "语言",
|
||||
"account": "账户设置",
|
||||
"password": "修改密码",
|
||||
"appearance": "外观",
|
||||
"routeConfig": "安全配置",
|
||||
"installConfig": "安装",
|
||||
"smartRouting": "智能路由"
|
||||
},
|
||||
"groups": {
|
||||
"title": "分组管理"
|
||||
},
|
||||
"users": {
|
||||
"title": "用户管理"
|
||||
},
|
||||
"market": {
|
||||
"title": "服务器市场 - (数据来源于 mcpm.sh)"
|
||||
},
|
||||
"logs": {
|
||||
"title": "系统日志"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"filters": "筛选",
|
||||
"search": "搜索日志...",
|
||||
"autoScroll": "自动滚动",
|
||||
"clearLogs": "清除日志",
|
||||
"loading": "加载日志中...",
|
||||
"noLogs": "暂无日志。",
|
||||
"noMatch": "没有匹配当前筛选条件的日志。",
|
||||
"mainProcess": "主进程",
|
||||
"childProcess": "子进程",
|
||||
"main": "主",
|
||||
"child": "子"
|
||||
},
|
||||
"groups": {
|
||||
"add": "添加",
|
||||
"addNew": "添加新分组",
|
||||
"edit": "编辑分组",
|
||||
"delete": "删除",
|
||||
"confirmDelete": "您确定要删除此分组吗?",
|
||||
"deleteWarning": "删除分组 '{{name}}' 将会移除该分组及其所有服务器关联。此操作无法撤销。",
|
||||
"name": "分组名称",
|
||||
"namePlaceholder": "请输入分组名称",
|
||||
"nameRequired": "分组名称不能为空",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "请输入分组描述(可选)",
|
||||
"createError": "创建分组失败",
|
||||
"updateError": "更新分组失败",
|
||||
"deleteError": "删除分组失败",
|
||||
"serverAddError": "向分组添加服务器失败",
|
||||
"serverRemoveError": "从分组移除服务器失败",
|
||||
"addServer": "添加服务器到分组",
|
||||
"selectServer": "选择要添加的服务器",
|
||||
"servers": "分组中的服务器",
|
||||
"remove": "移除",
|
||||
"noGroups": "暂无可用分组。创建一个新分组以开始使用。",
|
||||
"noServers": "此分组中没有服务器。",
|
||||
"noServerOptions": "没有可用的服务器",
|
||||
"serverCount": "{{count}} 台服务器",
|
||||
"toolSelection": "工具选择",
|
||||
"toolsSelected": "选择",
|
||||
"allTools": "全部",
|
||||
"selectedTools": "选中的工具",
|
||||
"selectAll": "全选",
|
||||
"selectNone": "全不选",
|
||||
"configureTools": "配置工具"
|
||||
},
|
||||
"market": {
|
||||
"title": "服务器市场",
|
||||
"official": "官方",
|
||||
"by": "作者",
|
||||
"unknown": "未知",
|
||||
"tools": "工具",
|
||||
"search": "搜索",
|
||||
"searchPlaceholder": "搜索服务器名称、分类或标签",
|
||||
"clearFilters": "清除",
|
||||
"clearCategoryFilter": "",
|
||||
"clearTagFilter": "",
|
||||
"categories": "分类",
|
||||
"tags": "标签",
|
||||
"showTags": "显示标签",
|
||||
"hideTags": "隐藏标签",
|
||||
"moreTags": "",
|
||||
"noServers": "未找到匹配的服务器",
|
||||
"backToList": "返回列表",
|
||||
"install": "安装",
|
||||
"installing": "安装中...",
|
||||
"installed": "已安装",
|
||||
"installServer": "安装服务器: {{name}}",
|
||||
"installSuccess": "服务器 {{serverName}} 安装成功",
|
||||
"author": "作者",
|
||||
"license": "许可证",
|
||||
"repository": "代码仓库",
|
||||
"examples": "示例",
|
||||
"arguments": "参数",
|
||||
"argumentName": "名称",
|
||||
"description": "描述",
|
||||
"required": "必填",
|
||||
"example": "示例",
|
||||
"viewSchema": "查看结构",
|
||||
"fetchError": "获取服务器市场数据失败",
|
||||
"serverNotFound": "未找到服务器",
|
||||
"searchError": "搜索服务器失败",
|
||||
"filterError": "按分类筛选服务器失败",
|
||||
"tagFilterError": "按标签筛选服务器失败",
|
||||
"noInstallationMethod": "该服务器没有可用的安装方法",
|
||||
"showing": "显示 {{from}}-{{to}}/{{total}} 个服务器",
|
||||
"perPage": "每页显示",
|
||||
"confirmVariablesMessage": "请确保这些变量在运行环境中已正确定义。是否继续安装服务器?",
|
||||
"confirmAndInstall": "确认并安装"
|
||||
},
|
||||
"tool": {
|
||||
"run": "运行",
|
||||
"running": "运行中...",
|
||||
"runTool": "运行",
|
||||
"cancel": "取消",
|
||||
"noDescription": "无描述信息",
|
||||
"inputSchema": "输入模式:",
|
||||
"runToolWithName": "运行工具:{{name}}",
|
||||
"execution": "工具执行",
|
||||
"successful": "成功",
|
||||
"failed": "失败",
|
||||
"result": "结果:",
|
||||
"error": "错误",
|
||||
"errorDetails": "错误详情:",
|
||||
"noContent": "工具执行成功但未返回内容。",
|
||||
"unknownError": "发生未知错误",
|
||||
"jsonResponse": "JSON 响应:",
|
||||
"toolResult": "工具结果",
|
||||
"noParameters": "此工具不需要任何参数。",
|
||||
"selectOption": "选择一个选项",
|
||||
"enterValue": "输入{{type}}值",
|
||||
"enabled": "已启用",
|
||||
"enableSuccess": "工具 {{name}} 启用成功",
|
||||
"disableSuccess": "工具 {{name}} 禁用成功",
|
||||
"toggleFailed": "切换工具状态失败",
|
||||
"parameters": "工具参数",
|
||||
"formMode": "表单模式",
|
||||
"jsonMode": "JSON 模式",
|
||||
"jsonConfiguration": "JSON 配置",
|
||||
"invalidJsonFormat": "无效的 JSON 格式",
|
||||
"fixJsonBeforeSwitching": "请修复 JSON 格式后再切换到表单模式",
|
||||
"item": "项目 {{index}}",
|
||||
"addItem": "添加 {{key}} 项目",
|
||||
"enterKey": "输入 {{key}}"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "启用全局路由",
|
||||
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
|
||||
"enableGroupNameRoute": "启用组名路由",
|
||||
"enableGroupNameRouteDescription": "允许使用组名而不仅仅是组 ID 连接到 /sse 端点",
|
||||
"enableBearerAuth": "启用 Bearer 认证",
|
||||
"enableBearerAuthDescription": "对 MCP 请求启用 Bearer 令牌认证",
|
||||
"bearerAuthKey": "Bearer 认证密钥",
|
||||
"bearerAuthKeyDescription": "Bearer 令牌中需要携带的认证密钥",
|
||||
"bearerAuthKeyPlaceholder": "请输入 Bearer 认证密钥",
|
||||
"skipAuth": "免登录开关",
|
||||
"skipAuthDescription": "跳过前端和 API 访问的登录要求(默认关闭确保安全性)",
|
||||
"pythonIndexUrl": "Python 包仓库地址",
|
||||
"pythonIndexUrlDescription": "设置 UV_DEFAULT_INDEX 环境变量,用于 Python 包安装",
|
||||
"pythonIndexUrlPlaceholder": "例如: https://mirrors.aliyun.com/pypi/simple",
|
||||
"npmRegistry": "NPM 仓库地址",
|
||||
"npmRegistryDescription": "设置 npm_config_registry 环境变量,用于 NPM 包安装",
|
||||
"npmRegistryPlaceholder": "例如: https://registry.npmmirror.com/",
|
||||
"baseUrl": "基础地址",
|
||||
"baseUrlDescription": "用于 MCP 请求的基础地址",
|
||||
"baseUrlPlaceholder": "例如: http://localhost:3000",
|
||||
"installConfig": "安装配置",
|
||||
"systemConfigUpdated": "系统配置更新成功",
|
||||
"enableSmartRouting": "启用智能路由",
|
||||
"enableSmartRoutingDescription": "开启智能路由功能,根据输入自动搜索最合适的工具(使用 $smart 分组)",
|
||||
"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}}"
|
||||
},
|
||||
"dxt": {
|
||||
"upload": "上传",
|
||||
"uploadTitle": "上传 DXT 扩展",
|
||||
"dropFileHere": "将 .dxt 文件拖拽到此处",
|
||||
"orClickToSelect": "或点击从计算机选择",
|
||||
"invalidFileType": "请选择有效的 .dxt 文件",
|
||||
"noFileSelected": "请选择要上传的 .dxt 文件",
|
||||
"uploading": "上传中...",
|
||||
"uploadFailed": "上传 DXT 文件失败",
|
||||
"installServer": "从 DXT 安装 MCP 服务器",
|
||||
"extensionInfo": "扩展信息",
|
||||
"name": "名称",
|
||||
"version": "版本",
|
||||
"description": "描述",
|
||||
"author": "作者",
|
||||
"tools": "工具",
|
||||
"serverName": "服务器名称",
|
||||
"serverNamePlaceholder": "为此服务器输入名称",
|
||||
"install": "安装",
|
||||
"installing": "安装中...",
|
||||
"installFailed": "从 DXT 安装服务器失败",
|
||||
"serverExistsTitle": "服务器已存在",
|
||||
"serverExistsConfirm": "服务器 '{{serverName}}' 已存在。是否要用新版本覆盖它?",
|
||||
"override": "覆盖"
|
||||
},
|
||||
"users": {
|
||||
"add": "添加",
|
||||
"addNew": "添加新用户",
|
||||
"edit": "编辑用户",
|
||||
"delete": "删除用户",
|
||||
"create": "创建",
|
||||
"update": "用户",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"adminRole": "管理员",
|
||||
"admin": "管理员",
|
||||
"user": "用户",
|
||||
"permissions": "权限",
|
||||
"adminPermissions": "完全系统访问权限",
|
||||
"userPermissions": "受限访问权限",
|
||||
"currentUser": "当前用户",
|
||||
"noUsers": "没有找到用户",
|
||||
"adminRequired": "需要管理员权限才能管理用户",
|
||||
"usernameRequired": "用户名是必需的",
|
||||
"passwordRequired": "密码是必需的",
|
||||
"passwordTooShort": "密码至少需要6个字符",
|
||||
"passwordMismatch": "密码不匹配",
|
||||
"usernamePlaceholder": "输入用户名",
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"newPasswordPlaceholder": "留空保持当前密码",
|
||||
"confirmPasswordPlaceholder": "确认新密码",
|
||||
"createError": "创建用户失败",
|
||||
"updateError": "更新用户失败",
|
||||
"deleteError": "删除用户失败",
|
||||
"statsError": "获取用户统计失败",
|
||||
"deleteConfirmation": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。",
|
||||
"confirmDelete": "删除用户",
|
||||
"deleteWarning": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import App from './App';
|
||||
import './index.css';
|
||||
// Import the i18n configuration
|
||||
import './i18n';
|
||||
// Setup fetch interceptors
|
||||
import './utils/setupInterceptors';
|
||||
import { loadRuntimeConfig } from './utils/runtime';
|
||||
|
||||
// Load runtime configuration before starting the app
|
||||
|
||||
@@ -32,9 +32,9 @@ const GroupsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleDeleteGroup = async (groupId: string) => {
|
||||
const success = await deleteGroup(groupId);
|
||||
if (!success) {
|
||||
setGroupError(t('groups.deleteError'));
|
||||
const result = await deleteGroup(groupId);
|
||||
if (!result || !result.success) {
|
||||
setGroupError(result?.message || t('groups.deleteError'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -103,7 +103,6 @@ const ServersPage: React.FC = () => {
|
||||
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm error-box">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-red-600 text-lg font-medium">{t('app.error')}</h3>
|
||||
<p className="text-gray-600 mt-1">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -4,45 +4,27 @@ import {
|
||||
RegisterCredentials,
|
||||
ChangePasswordCredentials,
|
||||
} from '../types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiPost, apiGet } from '../utils/fetchInterceptor';
|
||||
import { getToken, setToken, removeToken } from '../utils/interceptors';
|
||||
|
||||
// Token key in localStorage
|
||||
const TOKEN_KEY = 'mcphub_token';
|
||||
|
||||
// Get token from localStorage
|
||||
export const getToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
// Set token in localStorage
|
||||
export const setToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
// Remove token from localStorage
|
||||
export const removeToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
};
|
||||
// Export token management functions
|
||||
export { getToken, setToken, removeToken };
|
||||
|
||||
// Login user
|
||||
export const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
|
||||
try {
|
||||
console.log(getApiUrl('/auth/login'));
|
||||
const response = await fetch(getApiUrl('/auth/login'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
const response = await apiPost<AuthResponse>('/auth/login', credentials);
|
||||
|
||||
const data: AuthResponse = await response.json();
|
||||
|
||||
if (data.success && data.token) {
|
||||
setToken(data.token);
|
||||
// The auth API returns data directly, not wrapped in a data field
|
||||
if (response.success && response.token) {
|
||||
setToken(response.token);
|
||||
return response;
|
||||
}
|
||||
|
||||
return data;
|
||||
return {
|
||||
success: false,
|
||||
message: response.message || 'Login failed',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return {
|
||||
@@ -55,21 +37,17 @@ export const login = async (credentials: LoginCredentials): Promise<AuthResponse
|
||||
// Register user
|
||||
export const register = async (credentials: RegisterCredentials): Promise<AuthResponse> => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/auth/register'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
const response = await apiPost<AuthResponse>('/auth/register', credentials);
|
||||
|
||||
const data: AuthResponse = await response.json();
|
||||
|
||||
if (data.success && data.token) {
|
||||
setToken(data.token);
|
||||
if (response.success && response.token) {
|
||||
setToken(response.token);
|
||||
return response;
|
||||
}
|
||||
|
||||
return data;
|
||||
return {
|
||||
success: false,
|
||||
message: response.message || 'Registration failed',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Register error:', error);
|
||||
return {
|
||||
@@ -91,14 +69,8 @@ export const getCurrentUser = async (): Promise<AuthResponse> => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/auth/user'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-auth-token': token,
|
||||
},
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
const response = await apiGet<AuthResponse>('/auth/user');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
return {
|
||||
@@ -122,16 +94,8 @@ export const changePassword = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/auth/change-password'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token,
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
const response = await apiPost<AuthResponse>('/auth/change-password', credentials);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getApiUrl, getBasePath } from '../utils/runtime';
|
||||
import { apiGet, fetchWithInterceptors } from '../utils/fetchInterceptor';
|
||||
import { getBasePath } from '../utils/runtime';
|
||||
|
||||
export interface SystemConfig {
|
||||
routing?: {
|
||||
@@ -43,7 +44,7 @@ export interface SystemConfigResponse {
|
||||
export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
||||
try {
|
||||
const basePath = getBasePath();
|
||||
const response = await fetch(`${basePath}/public-config`, {
|
||||
const response = await fetchWithInterceptors(`${basePath}/public-config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -69,16 +70,10 @@ export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
||||
*/
|
||||
export const getSystemConfigPublic = async (): Promise<SystemConfig | null> => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const response = await apiGet<SystemConfigResponse>('/settings');
|
||||
|
||||
if (response.ok) {
|
||||
const data: SystemConfigResponse = await response.json();
|
||||
return data.data?.systemConfig || null;
|
||||
if (response.success) {
|
||||
return response.data?.systemConfig || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getToken } from './authService'; // Import getToken function
|
||||
import { apiGet, apiDelete } from '../utils/fetchInterceptor';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { getToken } from '../utils/interceptors';
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
@@ -13,21 +14,13 @@ export interface LogEntry {
|
||||
// Fetch all logs
|
||||
export const fetchLogs = async (): Promise<LogEntry[]> => {
|
||||
try {
|
||||
// Get authentication token
|
||||
const token = getToken();
|
||||
const response = await fetch(getApiUrl('/logs'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const response = await apiGet<{ success: boolean; data: LogEntry[]; error?: string }>('/logs');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch logs');
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to fetch logs');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching logs:', error);
|
||||
throw error;
|
||||
@@ -37,19 +30,10 @@ export const fetchLogs = async (): Promise<LogEntry[]> => {
|
||||
// Clear all logs
|
||||
export const clearLogs = async (): Promise<void> => {
|
||||
try {
|
||||
// Get authentication token
|
||||
const token = getToken();
|
||||
const response = await fetch(getApiUrl('/logs'), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const response = await apiDelete<{ success: boolean; error?: string }>('/logs');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to clear logs');
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to clear logs');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing logs:', error);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { getToken } from './authService';
|
||||
import { apiPost, apiPut } from '../utils/fetchInterceptor';
|
||||
|
||||
export interface ToolCallRequest {
|
||||
toolName: string;
|
||||
@@ -25,38 +24,32 @@ export const callTool = async (
|
||||
server?: string,
|
||||
): Promise<ToolCallResult> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
// Construct the URL with optional server parameter
|
||||
const url = server ? `/tools/call/${server}` : '/tools/call';
|
||||
|
||||
const response = await fetch(getApiUrl(url), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '', // Include token for authentication
|
||||
Authorization: `Bearer ${token}`, // Add bearer auth for MCP routing
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const response = await apiPost<any>(
|
||||
url,
|
||||
{
|
||||
toolName: request.toolName,
|
||||
arguments: request.arguments,
|
||||
}),
|
||||
});
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`, // Add bearer auth for MCP routing
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
if (!response.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message || 'Tool call failed',
|
||||
error: response.message || 'Tool call failed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: data.data.content || [],
|
||||
content: response.data?.content || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calling tool:', error);
|
||||
@@ -76,25 +69,19 @@ export const toggleTool = async (
|
||||
enabled: boolean,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const response = await fetch(getApiUrl(`/servers/${serverName}/tools/${toolName}/toggle`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
Authorization: `Bearer ${token}`,
|
||||
const response = await apiPost<any>(
|
||||
`/servers/${serverName}/tools/${toolName}/toggle`,
|
||||
{ enabled },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||
},
|
||||
},
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success,
|
||||
error: data.success ? undefined : data.message,
|
||||
success: response.success,
|
||||
error: response.success ? undefined : response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error toggling tool:', error);
|
||||
@@ -114,28 +101,19 @@ export const updateToolDescription = async (
|
||||
description: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const response = await fetch(
|
||||
getApiUrl(`/servers/${serverName}/tools/${toolName}/description`),
|
||||
const response = await apiPut<any>(
|
||||
`/servers/${serverName}/tools/${toolName}/description`,
|
||||
{ description },
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
Authorization: `Bearer ${token || ''}`,
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||
},
|
||||
body: JSON.stringify({ description }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success,
|
||||
error: data.success ? undefined : data.message,
|
||||
success: response.success,
|
||||
error: response.success ? undefined : response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating tool description:', error);
|
||||
|
||||
174
frontend/src/utils/fetchInterceptor.ts
Normal file
174
frontend/src/utils/fetchInterceptor.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { getApiUrl } from './runtime';
|
||||
|
||||
// Define the interceptor interface
|
||||
export interface FetchInterceptor {
|
||||
request?: (url: string, config: RequestInit) => Promise<{ url: string; config: RequestInit }>;
|
||||
response?: (response: Response) => Promise<Response>;
|
||||
error?: (error: Error) => Promise<Error>;
|
||||
}
|
||||
|
||||
// Define the enhanced fetch response interface
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Global interceptors store
|
||||
const interceptors: FetchInterceptor[] = [];
|
||||
|
||||
// Add an interceptor
|
||||
export const addInterceptor = (interceptor: FetchInterceptor): void => {
|
||||
interceptors.push(interceptor);
|
||||
};
|
||||
|
||||
// Remove an interceptor
|
||||
export const removeInterceptor = (interceptor: FetchInterceptor): void => {
|
||||
const index = interceptors.indexOf(interceptor);
|
||||
if (index > -1) {
|
||||
interceptors.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear all interceptors
|
||||
export const clearInterceptors = (): void => {
|
||||
interceptors.length = 0;
|
||||
};
|
||||
|
||||
// Enhanced fetch function with interceptors
|
||||
export const fetchWithInterceptors = async (
|
||||
input: string | URL | Request,
|
||||
init: RequestInit = {},
|
||||
): Promise<Response> => {
|
||||
let url = input.toString();
|
||||
let config = { ...init };
|
||||
|
||||
try {
|
||||
// Apply request interceptors
|
||||
for (const interceptor of interceptors) {
|
||||
if (interceptor.request) {
|
||||
const result = await interceptor.request(url, config);
|
||||
url = result.url;
|
||||
config = result.config;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the actual fetch request
|
||||
let response = await fetch(url, config);
|
||||
|
||||
// Apply response interceptors
|
||||
for (const interceptor of interceptors) {
|
||||
if (interceptor.response) {
|
||||
response = await interceptor.response(response);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
let processedError = error as Error;
|
||||
|
||||
// Apply error interceptors
|
||||
for (const interceptor of interceptors) {
|
||||
if (interceptor.error) {
|
||||
processedError = await interceptor.error(processedError);
|
||||
}
|
||||
}
|
||||
|
||||
throw processedError;
|
||||
}
|
||||
};
|
||||
|
||||
// Convenience function for API calls with automatic URL construction
|
||||
export const apiRequest = async <T = any>(endpoint: string, init: RequestInit = {}): Promise<T> => {
|
||||
try {
|
||||
const url = getApiUrl(endpoint);
|
||||
const response = await fetchWithInterceptors(url, init);
|
||||
|
||||
// Try to parse JSON response
|
||||
let data: T;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, create a generic response
|
||||
const genericResponse = {
|
||||
success: response.ok,
|
||||
message: response.ok
|
||||
? 'Request successful'
|
||||
: `HTTP ${response.status}: ${response.statusText}`,
|
||||
};
|
||||
data = genericResponse as T;
|
||||
}
|
||||
|
||||
// If response is not ok, but no explicit error in parsed data
|
||||
if (!response.ok && typeof data === 'object' && data !== null) {
|
||||
const responseObj = data as any;
|
||||
if (responseObj.success !== false) {
|
||||
responseObj.success = false;
|
||||
responseObj.message =
|
||||
responseObj.message || `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API request error:', error);
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'An unknown error occurred',
|
||||
};
|
||||
return errorResponse as T;
|
||||
}
|
||||
};
|
||||
|
||||
// Convenience methods for common HTTP methods
|
||||
export const apiGet = <T = any>(endpoint: string, init: Omit<RequestInit, 'method'> = {}) =>
|
||||
apiRequest<T>(endpoint, { ...init, method: 'GET' });
|
||||
|
||||
export const apiPost = <T = any>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||
) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...init,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
|
||||
export const apiPut = <T = any>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||
) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...init,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
|
||||
export const apiDelete = <T = any>(endpoint: string, init: Omit<RequestInit, 'method'> = {}) =>
|
||||
apiRequest<T>(endpoint, { ...init, method: 'DELETE' });
|
||||
|
||||
export const apiPatch = <T = any>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||
) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...init,
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
99
frontend/src/utils/interceptors.ts
Normal file
99
frontend/src/utils/interceptors.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { addInterceptor, removeInterceptor, type FetchInterceptor } from './fetchInterceptor';
|
||||
|
||||
// Token key in localStorage
|
||||
const TOKEN_KEY = 'mcphub_token';
|
||||
|
||||
// Get token from localStorage
|
||||
export const getToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
// Set token in localStorage
|
||||
export const setToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
// Remove token from localStorage
|
||||
export const removeToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
// Auth interceptor for automatically adding authorization headers
|
||||
export const authInterceptor: FetchInterceptor = {
|
||||
request: async (url: string, config: RequestInit) => {
|
||||
const headers = new Headers(config.headers);
|
||||
const language = localStorage.getItem('i18nextLng') || 'en';
|
||||
headers.set('Accept-Language', language);
|
||||
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers.set('x-auth-token', token);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
config: {
|
||||
...config,
|
||||
headers,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
response: async (response: Response) => {
|
||||
// Handle unauthorized responses
|
||||
if (response.status === 401) {
|
||||
// Token might be expired or invalid, remove it
|
||||
removeToken();
|
||||
|
||||
// You could also trigger a redirect to login page here
|
||||
// window.location.href = '/login';
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
error: async (error: Error) => {
|
||||
console.error('Auth interceptor error:', error);
|
||||
return error;
|
||||
},
|
||||
};
|
||||
|
||||
// Install the auth interceptor
|
||||
export const installAuthInterceptor = (): void => {
|
||||
addInterceptor(authInterceptor);
|
||||
};
|
||||
|
||||
// Uninstall the auth interceptor
|
||||
export const uninstallAuthInterceptor = (): void => {
|
||||
removeInterceptor(authInterceptor);
|
||||
};
|
||||
|
||||
// Logging interceptor for development
|
||||
export const loggingInterceptor: FetchInterceptor = {
|
||||
request: async (url: string, config: RequestInit) => {
|
||||
console.log(`🚀 [${config.method || 'GET'}] ${url}`, config);
|
||||
return { url, config };
|
||||
},
|
||||
|
||||
response: async (response: Response) => {
|
||||
console.log(`✅ [${response.status}] ${response.url}`);
|
||||
return response;
|
||||
},
|
||||
|
||||
error: async (error: Error) => {
|
||||
console.error(`❌ Fetch error:`, error);
|
||||
return error;
|
||||
},
|
||||
};
|
||||
|
||||
// Install the logging interceptor (only in development)
|
||||
export const installLoggingInterceptor = (): void => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
addInterceptor(loggingInterceptor);
|
||||
}
|
||||
};
|
||||
|
||||
// Uninstall the logging interceptor
|
||||
export const uninstallLoggingInterceptor = (): void => {
|
||||
removeInterceptor(loggingInterceptor);
|
||||
};
|
||||
19
frontend/src/utils/setupInterceptors.ts
Normal file
19
frontend/src/utils/setupInterceptors.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { installAuthInterceptor, installLoggingInterceptor } from './interceptors';
|
||||
|
||||
/**
|
||||
* Setup all default interceptors for the application
|
||||
* This should be called once when the app initializes
|
||||
*/
|
||||
export const setupInterceptors = (): void => {
|
||||
// Install auth interceptor for automatic token handling
|
||||
installAuthInterceptor();
|
||||
|
||||
// Install logging interceptor in development mode
|
||||
installLoggingInterceptor();
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize interceptors automatically when this module is imported
|
||||
* This ensures interceptors are set up as early as possible
|
||||
*/
|
||||
setupInterceptors();
|
||||
Reference in New Issue
Block a user