mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-19 04:00:16 +09:00
fix: prevent MCP operations from blocking chat interface
This commit is contained in:
parent
bc71ae247b
commit
bfeea4ed49
@ -167,6 +167,11 @@
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
&.initializing {
|
||||
background-color: #f59e0b;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
|
@ -13,7 +13,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
addMcpServer,
|
||||
getClientStatus,
|
||||
getClientsStatus,
|
||||
getClientTools,
|
||||
getMcpConfigFromFile,
|
||||
isMcpEnabled,
|
||||
@ -71,6 +71,23 @@ export function McpMarketPage() {
|
||||
checkMcpStatus();
|
||||
}, [navigate]);
|
||||
|
||||
// 添加状态轮询
|
||||
useEffect(() => {
|
||||
if (!mcpEnabled || !config) return;
|
||||
|
||||
const updateStatuses = async () => {
|
||||
const statuses = await getClientsStatus();
|
||||
setClientStatuses(statuses);
|
||||
};
|
||||
|
||||
// 立即执行一次
|
||||
updateStatuses();
|
||||
// 每 1000ms 轮询一次
|
||||
const timer = setInterval(updateStatuses, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [mcpEnabled, config]);
|
||||
|
||||
// 加载预设服务器
|
||||
useEffect(() => {
|
||||
const loadPresetServers = async () => {
|
||||
@ -103,10 +120,7 @@ export function McpMarketPage() {
|
||||
setConfig(config);
|
||||
|
||||
// 获取所有客户端的状态
|
||||
const statuses: Record<string, any> = {};
|
||||
for (const clientId of Object.keys(config.mcpServers)) {
|
||||
statuses[clientId] = await getClientStatus(clientId);
|
||||
}
|
||||
const statuses = await getClientsStatus();
|
||||
setClientStatuses(statuses);
|
||||
} catch (error) {
|
||||
console.error("Failed to load initial state:", error);
|
||||
@ -165,7 +179,6 @@ export function McpMarketPage() {
|
||||
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||
if (!preset || !preset.configSchema || !editingServerId) return;
|
||||
|
||||
// 先关闭模态框
|
||||
const savingServerId = editingServerId;
|
||||
setEditingServerId(undefined);
|
||||
|
||||
@ -200,31 +213,8 @@ export function McpMarketPage() {
|
||||
...(Object.keys(env).length > 0 ? { env } : {}),
|
||||
};
|
||||
|
||||
// 检查是否是新增还是编辑
|
||||
const isNewServer = !isServerAdded(savingServerId);
|
||||
|
||||
// 如果是编辑现有服务器,保持原有状态
|
||||
if (!isNewServer) {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const currentStatus = currentConfig.mcpServers[savingServerId]?.status;
|
||||
if (currentStatus) {
|
||||
serverConfig.status = currentStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置并初始化新服务器
|
||||
const newConfig = await addMcpServer(savingServerId, serverConfig);
|
||||
setConfig(newConfig);
|
||||
|
||||
// 只有新增的服务器才需要获取状态(因为会自动启动)
|
||||
if (isNewServer) {
|
||||
const status = await getClientStatus(savingServerId);
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[savingServerId]: status,
|
||||
}));
|
||||
}
|
||||
|
||||
showToast("Server configuration updated successfully");
|
||||
} catch (error) {
|
||||
showToast(
|
||||
@ -277,11 +267,8 @@ export function McpMarketPage() {
|
||||
setConfig(newConfig);
|
||||
|
||||
// 更新状态
|
||||
const status = await getClientStatus(preset.id);
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[preset.id]: status,
|
||||
}));
|
||||
const statuses = await getClientsStatus();
|
||||
setClientStatuses(statuses);
|
||||
} finally {
|
||||
updateLoadingState(preset.id, null);
|
||||
}
|
||||
@ -298,11 +285,6 @@ export function McpMarketPage() {
|
||||
updateLoadingState(id, "Stopping server...");
|
||||
const newConfig = await pauseMcpServer(id);
|
||||
setConfig(newConfig);
|
||||
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[id]: { status: "paused", errorMsg: null },
|
||||
}));
|
||||
showToast("Server stopped successfully");
|
||||
} catch (error) {
|
||||
showToast("Failed to stop server");
|
||||
@ -316,19 +298,7 @@ export function McpMarketPage() {
|
||||
const restartServer = async (id: string) => {
|
||||
try {
|
||||
updateLoadingState(id, "Starting server...");
|
||||
|
||||
const success = await resumeMcpServer(id);
|
||||
const status = await getClientStatus(id);
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[id]: status,
|
||||
}));
|
||||
|
||||
if (success) {
|
||||
showToast("Server started successfully");
|
||||
} else {
|
||||
throw new Error("Failed to start server");
|
||||
}
|
||||
await resumeMcpServer(id);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error
|
||||
@ -347,14 +317,7 @@ export function McpMarketPage() {
|
||||
updateLoadingState("all", "Restarting all servers...");
|
||||
const newConfig = await restartAllClients();
|
||||
setConfig(newConfig);
|
||||
|
||||
const statuses: Record<string, any> = {};
|
||||
for (const clientId of Object.keys(newConfig.mcpServers)) {
|
||||
statuses[clientId] = await getClientStatus(clientId);
|
||||
}
|
||||
setClientStatuses(statuses);
|
||||
|
||||
showToast("Successfully restarted all clients");
|
||||
showToast("Restarting all clients");
|
||||
} catch (error) {
|
||||
showToast("Failed to restart clients");
|
||||
console.error(error);
|
||||
@ -452,6 +415,12 @@ export function McpMarketPage() {
|
||||
|
||||
const statusMap = {
|
||||
undefined: null, // 未配置/未找到不显示
|
||||
// 添加初始化状态
|
||||
initializing: (
|
||||
<span className={clsx(styles["server-status"], styles["initializing"])}>
|
||||
Initializing
|
||||
</span>
|
||||
),
|
||||
paused: (
|
||||
<span className={clsx(styles["server-status"], styles["stopped"])}>
|
||||
Stopped
|
||||
@ -517,10 +486,11 @@ export function McpMarketPage() {
|
||||
const statusPriority: Record<string, number> = {
|
||||
error: 0, // Highest priority for error status
|
||||
active: 1, // Second for active
|
||||
starting: 2, // Starting
|
||||
stopping: 3, // Stopping
|
||||
paused: 4, // Paused
|
||||
undefined: 5, // Lowest priority for undefined
|
||||
initializing: 2, // Initializing
|
||||
starting: 3, // Starting
|
||||
stopping: 4, // Stopping
|
||||
paused: 5, // Paused
|
||||
undefined: 6, // Lowest priority for undefined
|
||||
};
|
||||
|
||||
// Get actual status (including loading status)
|
||||
@ -529,6 +499,11 @@ export function McpMarketPage() {
|
||||
const operationType = getOperationStatusType(loading);
|
||||
return operationType === "default" ? status : operationType;
|
||||
}
|
||||
|
||||
if (status === "initializing" && !loading) {
|
||||
return "active";
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
@ -538,8 +513,8 @@ export function McpMarketPage() {
|
||||
// 首先按状态排序
|
||||
if (aEffectiveStatus !== bEffectiveStatus) {
|
||||
return (
|
||||
(statusPriority[aEffectiveStatus] ?? 5) -
|
||||
(statusPriority[bEffectiveStatus] ?? 5)
|
||||
(statusPriority[aEffectiveStatus] ?? 6) -
|
||||
(statusPriority[bEffectiveStatus] ?? 6)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -24,40 +24,54 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
|
||||
const clientsMap = new Map<string, McpClientData>();
|
||||
|
||||
// 获取客户端状态
|
||||
export async function getClientStatus(
|
||||
clientId: string,
|
||||
): Promise<ServerStatusResponse> {
|
||||
const status = clientsMap.get(clientId);
|
||||
export async function getClientsStatus(): Promise<
|
||||
Record<string, ServerStatusResponse>
|
||||
> {
|
||||
const config = await getMcpConfigFromFile();
|
||||
const serverConfig = config.mcpServers[clientId];
|
||||
const result: Record<string, ServerStatusResponse> = {};
|
||||
|
||||
// 如果配置中不存在该服务器
|
||||
if (!serverConfig) {
|
||||
return { status: "undefined", errorMsg: null };
|
||||
for (const clientId of Object.keys(config.mcpServers)) {
|
||||
const status = clientsMap.get(clientId);
|
||||
const serverConfig = config.mcpServers[clientId];
|
||||
|
||||
if (!serverConfig) {
|
||||
result[clientId] = { status: "undefined", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (serverConfig.status === "paused") {
|
||||
result[clientId] = { status: "paused", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
result[clientId] = { status: "undefined", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
status.client === null &&
|
||||
status.tools === null &&
|
||||
status.errorMsg === null
|
||||
) {
|
||||
result[clientId] = { status: "initializing", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.errorMsg) {
|
||||
result[clientId] = { status: "error", errorMsg: status.errorMsg };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.client) {
|
||||
result[clientId] = { status: "active", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
result[clientId] = { status: "error", errorMsg: "Client not found" };
|
||||
}
|
||||
|
||||
// 如果服务器配置为暂停状态
|
||||
if (serverConfig.status === "paused") {
|
||||
return { status: "paused", errorMsg: null };
|
||||
}
|
||||
|
||||
// 如果 clientsMap 中没有记录
|
||||
if (!status) {
|
||||
return { status: "undefined", errorMsg: null };
|
||||
}
|
||||
|
||||
// 如果有错误
|
||||
if (status.errorMsg) {
|
||||
return { status: "error", errorMsg: status.errorMsg };
|
||||
}
|
||||
|
||||
// 如果客户端正常运行
|
||||
if (status.client) {
|
||||
return { status: "active", errorMsg: null };
|
||||
}
|
||||
|
||||
// 如果客户端不存在
|
||||
return { status: "error", errorMsg: "Client not found" };
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取客户端工具
|
||||
@ -96,22 +110,32 @@ async function initializeSingleClient(
|
||||
}
|
||||
|
||||
logger.info(`Initializing client [${clientId}]...`);
|
||||
try {
|
||||
const client = await createClient(clientId, serverConfig);
|
||||
const tools = await listTools(client);
|
||||
logger.info(
|
||||
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
|
||||
);
|
||||
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||
logger.success(`Client [${clientId}] initialized successfully`);
|
||||
} catch (error) {
|
||||
clientsMap.set(clientId, {
|
||||
client: null,
|
||||
tools: null,
|
||||
errorMsg: error instanceof Error ? error.message : String(error),
|
||||
|
||||
// 先设置初始化状态
|
||||
clientsMap.set(clientId, {
|
||||
client: null,
|
||||
tools: null,
|
||||
errorMsg: null, // null 表示正在初始化
|
||||
});
|
||||
|
||||
// 异步初始化
|
||||
createClient(clientId, serverConfig)
|
||||
.then(async (client) => {
|
||||
const tools = await listTools(client);
|
||||
logger.info(
|
||||
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
|
||||
);
|
||||
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||
logger.success(`Client [${clientId}] initialized successfully`);
|
||||
})
|
||||
.catch((error) => {
|
||||
clientsMap.set(clientId, {
|
||||
client: null,
|
||||
tools: null,
|
||||
errorMsg: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||
});
|
||||
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化系统
|
||||
@ -184,7 +208,7 @@ export async function pauseMcpServer(clientId: string) {
|
||||
...currentConfig.mcpServers,
|
||||
[clientId]: {
|
||||
...serverConfig,
|
||||
status: "paused" as const,
|
||||
status: "paused",
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -205,7 +229,7 @@ export async function pauseMcpServer(clientId: string) {
|
||||
}
|
||||
|
||||
// 恢复服务器
|
||||
export async function resumeMcpServer(clientId: string): Promise<boolean> {
|
||||
export async function resumeMcpServer(clientId: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const serverConfig = currentConfig.mcpServers[clientId];
|
||||
@ -233,10 +257,6 @@ export async function resumeMcpServer(clientId: string): Promise<boolean> {
|
||||
},
|
||||
};
|
||||
await updateMcpConfig(newConfig);
|
||||
|
||||
// 再次确认状态
|
||||
const status = await getClientStatus(clientId);
|
||||
return status.status === "active";
|
||||
} catch (error) {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const serverConfig = currentConfig.mcpServers[clientId];
|
||||
@ -254,7 +274,7 @@ export async function resumeMcpServer(clientId: string): Promise<boolean> {
|
||||
errorMsg: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||
return false;
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to resume server [${clientId}]: ${error}`);
|
||||
@ -297,6 +317,7 @@ export async function restartAllClients() {
|
||||
await removeClient(client.client);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空状态
|
||||
clientsMap.clear();
|
||||
|
||||
@ -350,21 +371,11 @@ async function updateMcpConfig(config: McpConfigData): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化单个客户端
|
||||
export async function reinitializeClient(clientId: string) {
|
||||
const config = await getMcpConfigFromFile();
|
||||
const serverConfig = config.mcpServers[clientId];
|
||||
if (!serverConfig) {
|
||||
throw new Error(`Server config not found for client ${clientId}`);
|
||||
}
|
||||
await initializeSingleClient(clientId, serverConfig);
|
||||
}
|
||||
|
||||
// 检查 MCP 是否启用
|
||||
export async function isMcpEnabled() {
|
||||
try {
|
||||
const serverConfig = getServerSideConfig();
|
||||
return !!serverConfig.enableMcp;
|
||||
return serverConfig.enableMcp;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to check MCP status: ${error}`);
|
||||
return false;
|
||||
|
@ -73,7 +73,16 @@ export interface ListToolsResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export type McpClientData = McpActiveClient | McpErrorClient;
|
||||
export type McpClientData =
|
||||
| McpActiveClient
|
||||
| McpErrorClient
|
||||
| McpInitializingClient;
|
||||
|
||||
interface McpInitializingClient {
|
||||
client: null;
|
||||
tools: null;
|
||||
errorMsg: null;
|
||||
}
|
||||
|
||||
interface McpActiveClient {
|
||||
client: Client;
|
||||
@ -88,7 +97,12 @@ interface McpErrorClient {
|
||||
}
|
||||
|
||||
// 服务器状态类型
|
||||
export type ServerStatus = "undefined" | "active" | "paused" | "error";
|
||||
export type ServerStatus =
|
||||
| "undefined"
|
||||
| "active"
|
||||
| "paused"
|
||||
| "error"
|
||||
| "initializing";
|
||||
|
||||
export interface ServerStatusResponse {
|
||||
status: ServerStatus;
|
||||
|
Loading…
Reference in New Issue
Block a user