mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 20:32:46 +08:00
feat: support stop/start MCP servers
This commit is contained in:
@@ -98,6 +98,10 @@
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
&.stopped {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
@@ -151,21 +155,11 @@
|
||||
|
||||
.mcp-market-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
justify-content: flex-end;
|
||||
|
||||
:global(.icon-button) {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,30 +207,6 @@
|
||||
color: var(--gray-300);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.icon-button) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--gray-200);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-100);
|
||||
border-color: var(--gray-300);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.icon-button.add-path-button) {
|
||||
|
@@ -17,16 +17,20 @@ import {
|
||||
getClientStatus,
|
||||
getClientTools,
|
||||
getMcpConfigFromFile,
|
||||
removeMcpServer,
|
||||
restartAllClients,
|
||||
pauseMcpServer,
|
||||
resumeMcpServer,
|
||||
} from "../mcp/actions";
|
||||
import {
|
||||
ListToolsResponse,
|
||||
McpConfigData,
|
||||
PresetServer,
|
||||
ServerConfig,
|
||||
ServerStatusResponse,
|
||||
} from "../mcp/types";
|
||||
import clsx from "clsx";
|
||||
import PlayIcon from "../icons/play.svg";
|
||||
import StopIcon from "../icons/pause.svg";
|
||||
|
||||
const presetServers = presetServersJson as PresetServer[];
|
||||
|
||||
@@ -47,13 +51,7 @@ export function McpMarketPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [config, setConfig] = useState<McpConfigData>();
|
||||
const [clientStatuses, setClientStatuses] = useState<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
status: "active" | "error" | "undefined";
|
||||
errorMsg: string | null;
|
||||
}
|
||||
>
|
||||
Record<string, ServerStatusResponse>
|
||||
>({});
|
||||
|
||||
// 检查服务器是否已添加
|
||||
@@ -253,18 +251,74 @@ export function McpMarketPage() {
|
||||
};
|
||||
|
||||
// 移除服务器
|
||||
const removeServer = async (id: string) => {
|
||||
// const removeServer = async (id: string) => {
|
||||
// try {
|
||||
// setIsLoading(true);
|
||||
// const newConfig = await removeMcpServer(id);
|
||||
// setConfig(newConfig);
|
||||
|
||||
// // 移除状态
|
||||
// setClientStatuses((prev) => {
|
||||
// const newStatuses = { ...prev };
|
||||
// delete newStatuses[id];
|
||||
// return newStatuses;
|
||||
// });
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// 暂停服务器
|
||||
const pauseServer = async (id: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const newConfig = await removeMcpServer(id);
|
||||
showToast("Stopping server...");
|
||||
const newConfig = await pauseMcpServer(id);
|
||||
setConfig(newConfig);
|
||||
|
||||
// 移除状态
|
||||
setClientStatuses((prev) => {
|
||||
const newStatuses = { ...prev };
|
||||
delete newStatuses[id];
|
||||
return newStatuses;
|
||||
});
|
||||
// 更新状态为暂停
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[id]: { status: "paused", errorMsg: null },
|
||||
}));
|
||||
showToast("Server stopped successfully");
|
||||
} catch (error) {
|
||||
showToast("Failed to stop server");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复服务器
|
||||
const resumeServer = async (id: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
showToast("Starting server...");
|
||||
|
||||
// 尝试启动服务器
|
||||
const success = await resumeMcpServer(id);
|
||||
|
||||
// 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态)
|
||||
const status = await getClientStatus(id);
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[id]: status,
|
||||
}));
|
||||
|
||||
// 根据启动结果显示消息
|
||||
if (success) {
|
||||
showToast("Server started successfully");
|
||||
} else {
|
||||
throw new Error("Failed to start server");
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to start server, please check logs",
|
||||
);
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -332,7 +386,12 @@ export function McpMarketPage() {
|
||||
} else if (prop.type === "string") {
|
||||
const currentValue = userConfig[key as keyof typeof userConfig] || "";
|
||||
return (
|
||||
<ListItem key={key} title={key} subTitle={prop.description}>
|
||||
<ListItem
|
||||
key={key}
|
||||
title={key}
|
||||
subTitle={prop.description}
|
||||
vertical
|
||||
>
|
||||
<div className={styles["input-item"]}>
|
||||
<input
|
||||
type="text"
|
||||
@@ -356,6 +415,29 @@ export function McpMarketPage() {
|
||||
return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
|
||||
};
|
||||
|
||||
// 修改状态显示逻辑
|
||||
const getServerStatusDisplay = (clientId: string) => {
|
||||
const status = checkServerStatus(clientId);
|
||||
|
||||
const statusMap = {
|
||||
undefined: null, // 未配置/未找到不显示
|
||||
paused: (
|
||||
<span className={clsx(styles["server-status"], styles["stopped"])}>
|
||||
Stopped
|
||||
</span>
|
||||
),
|
||||
active: <span className={styles["server-status"]}>Running</span>,
|
||||
error: (
|
||||
<span className={clsx(styles["server-status"], styles["error"])}>
|
||||
Error
|
||||
<span className={styles["error-message"]}>: {status.errorMsg}</span>
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
return statusMap[status.status];
|
||||
};
|
||||
|
||||
// 渲染服务器列表
|
||||
const renderServerList = () => {
|
||||
return presetServers
|
||||
@@ -373,15 +455,18 @@ export function McpMarketPage() {
|
||||
const bStatus = checkServerStatus(b.id).status;
|
||||
|
||||
// 定义状态优先级
|
||||
const statusPriority = {
|
||||
error: 0,
|
||||
active: 1,
|
||||
undefined: 2,
|
||||
const statusPriority: Record<string, number> = {
|
||||
error: 0, // 最高优先级
|
||||
active: 1, // 运行中
|
||||
paused: 2, // 已暂停
|
||||
undefined: 3, // 未配置/未找到
|
||||
};
|
||||
|
||||
// 首先按状态排序
|
||||
if (aStatus !== bStatus) {
|
||||
return statusPriority[aStatus] - statusPriority[bStatus];
|
||||
return (
|
||||
(statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3)
|
||||
);
|
||||
}
|
||||
|
||||
// 然后按名称排序
|
||||
@@ -398,25 +483,7 @@ export function McpMarketPage() {
|
||||
<div className={styles["mcp-market-title"]}>
|
||||
<div className={styles["mcp-market-name"]}>
|
||||
{server.name}
|
||||
{checkServerStatus(server.id).status !== "undefined" && (
|
||||
<span
|
||||
className={clsx(styles["server-status"], {
|
||||
[styles["error"]]:
|
||||
checkServerStatus(server.id).status === "error",
|
||||
})}
|
||||
>
|
||||
{checkServerStatus(server.id).status === "error" ? (
|
||||
<>
|
||||
Error
|
||||
<span className={styles["error-message"]}>
|
||||
: {checkServerStatus(server.id).errorMsg}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
"Active"
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{getServerStatusDisplay(server.id)}
|
||||
{server.repo && (
|
||||
<a
|
||||
href={server.repo}
|
||||
@@ -450,39 +517,52 @@ export function McpMarketPage() {
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text="Configure"
|
||||
className={clsx({
|
||||
[styles["action-error"]]:
|
||||
checkServerStatus(server.id).status === "error",
|
||||
})}
|
||||
onClick={() => setEditingServerId(server.id)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text="Tools"
|
||||
onClick={async () => {
|
||||
setViewingServerId(server.id);
|
||||
await loadTools(server.id);
|
||||
}}
|
||||
disabled={
|
||||
isLoading ||
|
||||
checkServerStatus(server.id).status === "error"
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
text="Remove"
|
||||
className={styles["action-danger"]}
|
||||
onClick={() => removeServer(server.id)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{checkServerStatus(server.id).status === "paused" ? (
|
||||
<>
|
||||
<IconButton
|
||||
icon={<PlayIcon />}
|
||||
text="Start"
|
||||
onClick={() => resumeServer(server.id)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{/* <IconButton
|
||||
icon={<DeleteIcon />}
|
||||
text="Remove"
|
||||
onClick={() => removeServer(server.id)}
|
||||
disabled={isLoading}
|
||||
/> */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text="Tools"
|
||||
onClick={async () => {
|
||||
setViewingServerId(server.id);
|
||||
await loadTools(server.id);
|
||||
}}
|
||||
disabled={
|
||||
isLoading ||
|
||||
checkServerStatus(server.id).status === "error"
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<StopIcon />}
|
||||
text="Stop"
|
||||
onClick={() => pauseServer(server.id)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text="Add"
|
||||
className={styles["action-primary"]}
|
||||
onClick={() => addServer(server)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
Reference in New Issue
Block a user