mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-20 04:30:17 +09:00
feat: improve async operations and UI feedback
This commit is contained in:
parent
4d535b1cd0
commit
65810d918b
@ -85,6 +85,50 @@
|
|||||||
border-bottom-right-radius: 10px;
|
border-bottom-right-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.2),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading-pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #16a34a;
|
||||||
|
color: #fff;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
|
||||||
|
&[data-status="stopping"] {
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-status="starting"] {
|
||||||
|
background-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-status="error"] {
|
||||||
|
background-color: #f87171;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mcp-market-header {
|
.mcp-market-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -585,3 +629,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes loading-pulse {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -52,6 +52,9 @@ export function McpMarketPage() {
|
|||||||
>({});
|
>({});
|
||||||
const [loadingPresets, setLoadingPresets] = useState(true);
|
const [loadingPresets, setLoadingPresets] = useState(true);
|
||||||
const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
|
const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
|
||||||
|
const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPresetServers = async () => {
|
const loadPresetServers = async () => {
|
||||||
@ -141,8 +144,12 @@ export function McpMarketPage() {
|
|||||||
const preset = presetServers.find((s) => s.id === editingServerId);
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
if (!preset || !preset.configSchema || !editingServerId) return;
|
if (!preset || !preset.configSchema || !editingServerId) return;
|
||||||
|
|
||||||
|
// 先关闭模态框
|
||||||
|
const savingServerId = editingServerId;
|
||||||
|
setEditingServerId(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
updateLoadingState(savingServerId, "Updating configuration...");
|
||||||
// 构建服务器配置
|
// 构建服务器配置
|
||||||
const args = [...preset.baseArgs];
|
const args = [...preset.baseArgs];
|
||||||
const env: Record<string, string> = {};
|
const env: Record<string, string> = {};
|
||||||
@ -172,25 +179,38 @@ export function McpMarketPage() {
|
|||||||
...(Object.keys(env).length > 0 ? { env } : {}),
|
...(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(editingServerId, serverConfig);
|
const newConfig = await addMcpServer(savingServerId, serverConfig);
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
|
||||||
// 更新状态
|
// 只有新增的服务器才需要获取状态(因为会自动启动)
|
||||||
const status = await getClientStatus(editingServerId);
|
if (isNewServer) {
|
||||||
setClientStatuses((prev) => ({
|
const status = await getClientStatus(savingServerId);
|
||||||
...prev,
|
setClientStatuses((prev) => ({
|
||||||
[editingServerId]: status,
|
...prev,
|
||||||
}));
|
[savingServerId]: status,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
setEditingServerId(undefined);
|
showToast("Server configuration updated successfully");
|
||||||
showToast("Server configuration saved successfully");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(
|
showToast(
|
||||||
error instanceof Error ? error.message : "Failed to save configuration",
|
error instanceof Error ? error.message : "Failed to save configuration",
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
updateLoadingState(savingServerId, null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -210,36 +230,24 @@ export function McpMarketPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重启所有客户端
|
// 更新加载状态的辅助函数
|
||||||
const handleRestartAll = async () => {
|
const updateLoadingState = (id: string, message: string | null) => {
|
||||||
try {
|
setLoadingStates((prev) => {
|
||||||
setIsLoading(true);
|
if (message === null) {
|
||||||
const newConfig = await restartAllClients();
|
const { [id]: _, ...rest } = prev;
|
||||||
setConfig(newConfig);
|
return rest;
|
||||||
|
|
||||||
// 更新所有客户端状态
|
|
||||||
const statuses: Record<string, any> = {};
|
|
||||||
for (const clientId of Object.keys(newConfig.mcpServers)) {
|
|
||||||
statuses[clientId] = await getClientStatus(clientId);
|
|
||||||
}
|
}
|
||||||
setClientStatuses(statuses);
|
return { ...prev, [id]: message };
|
||||||
|
});
|
||||||
showToast("Successfully restarted all clients");
|
|
||||||
} catch (error) {
|
|
||||||
showToast("Failed to restart clients");
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加服务器
|
// 修改添加服务器函数
|
||||||
const addServer = async (preset: PresetServer) => {
|
const addServer = async (preset: PresetServer) => {
|
||||||
if (!preset.configurable) {
|
if (!preset.configurable) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
const serverId = preset.id;
|
||||||
showToast("Creating MCP client...");
|
updateLoadingState(serverId, "Creating MCP client...");
|
||||||
// 如果服务器不需要配置,直接添加
|
|
||||||
const serverConfig: ServerConfig = {
|
const serverConfig: ServerConfig = {
|
||||||
command: preset.command,
|
command: preset.command,
|
||||||
args: [...preset.baseArgs],
|
args: [...preset.baseArgs],
|
||||||
@ -254,7 +262,7 @@ export function McpMarketPage() {
|
|||||||
[preset.id]: status,
|
[preset.id]: status,
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
updateLoadingState(preset.id, null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果需要配置,打开配置对话框
|
// 如果需要配置,打开配置对话框
|
||||||
@ -263,33 +271,13 @@ export function McpMarketPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 移除服务器
|
// 修改暂停服务器函数
|
||||||
// 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) => {
|
const pauseServer = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
updateLoadingState(id, "Stopping server...");
|
||||||
showToast("Stopping server...");
|
|
||||||
const newConfig = await pauseMcpServer(id);
|
const newConfig = await pauseMcpServer(id);
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
|
||||||
// 更新状态为暂停
|
|
||||||
setClientStatuses((prev) => ({
|
setClientStatuses((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[id]: { status: "paused", errorMsg: null },
|
[id]: { status: "paused", errorMsg: null },
|
||||||
@ -299,27 +287,22 @@ export function McpMarketPage() {
|
|||||||
showToast("Failed to stop server");
|
showToast("Failed to stop server");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
updateLoadingState(id, null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 恢复服务器
|
// 修改恢复服务器函数
|
||||||
const resumeServer = async (id: string) => {
|
const resumeServer = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
updateLoadingState(id, "Starting server...");
|
||||||
showToast("Starting server...");
|
|
||||||
|
|
||||||
// 尝试启动服务器
|
|
||||||
const success = await resumeMcpServer(id);
|
const success = await resumeMcpServer(id);
|
||||||
|
|
||||||
// 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态)
|
|
||||||
const status = await getClientStatus(id);
|
const status = await getClientStatus(id);
|
||||||
setClientStatuses((prev) => ({
|
setClientStatuses((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[id]: status,
|
[id]: status,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 根据启动结果显示消息
|
|
||||||
if (success) {
|
if (success) {
|
||||||
showToast("Server started successfully");
|
showToast("Server started successfully");
|
||||||
} else {
|
} else {
|
||||||
@ -333,7 +316,29 @@ export function McpMarketPage() {
|
|||||||
);
|
);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
updateLoadingState(id, null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改重启所有客户端函数
|
||||||
|
const handleRestartAll = async () => {
|
||||||
|
try {
|
||||||
|
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");
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to restart clients");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
updateLoadingState("all", null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -445,6 +450,14 @@ export function McpMarketPage() {
|
|||||||
return statusMap[status.status];
|
return statusMap[status.status];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取操作状态的类型
|
||||||
|
const getOperationStatusType = (message: string) => {
|
||||||
|
if (message.toLowerCase().includes("stopping")) return "stopping";
|
||||||
|
if (message.toLowerCase().includes("starting")) return "starting";
|
||||||
|
if (message.toLowerCase().includes("error")) return "error";
|
||||||
|
return "default";
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染服务器列表
|
// 渲染服务器列表
|
||||||
const renderServerList = () => {
|
const renderServerList = () => {
|
||||||
if (loadingPresets) {
|
if (loadingPresets) {
|
||||||
@ -478,29 +491,46 @@ export function McpMarketPage() {
|
|||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aStatus = checkServerStatus(a.id).status;
|
const aStatus = checkServerStatus(a.id).status;
|
||||||
const bStatus = checkServerStatus(b.id).status;
|
const bStatus = checkServerStatus(b.id).status;
|
||||||
|
const aLoading = loadingStates[a.id];
|
||||||
|
const bLoading = loadingStates[b.id];
|
||||||
|
|
||||||
// 定义状态优先级
|
// 定义状态优先级
|
||||||
const statusPriority: Record<string, number> = {
|
const statusPriority: Record<string, number> = {
|
||||||
error: 0, // 最高优先级
|
error: 0, // 错误状态最高优先级
|
||||||
active: 1, // 运行中
|
active: 1, // 已启动次之
|
||||||
paused: 2, // 已暂停
|
starting: 2, // 正在启动
|
||||||
undefined: 3, // 未配置/未找到
|
stopping: 3, // 正在停止
|
||||||
|
paused: 4, // 已暂停
|
||||||
|
undefined: 5, // 未配置最低优先级
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取实际状态(包括加载状态)
|
||||||
|
const getEffectiveStatus = (status: string, loading?: string) => {
|
||||||
|
if (loading) {
|
||||||
|
const operationType = getOperationStatusType(loading);
|
||||||
|
return operationType === "default" ? status : operationType;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
|
||||||
|
const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
|
||||||
|
|
||||||
// 首先按状态排序
|
// 首先按状态排序
|
||||||
if (aStatus !== bStatus) {
|
if (aEffectiveStatus !== bEffectiveStatus) {
|
||||||
return (
|
return (
|
||||||
(statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3)
|
(statusPriority[aEffectiveStatus] ?? 5) -
|
||||||
|
(statusPriority[bEffectiveStatus] ?? 5)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 然后按名称排序
|
// 状态相同时按名称排序
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
})
|
})
|
||||||
.map((server) => (
|
.map((server) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles["mcp-market-item"], {
|
className={clsx(styles["mcp-market-item"], {
|
||||||
[styles["disabled"]]: isLoading,
|
[styles["loading"]]: loadingStates[server.id],
|
||||||
})}
|
})}
|
||||||
key={server.id}
|
key={server.id}
|
||||||
>
|
>
|
||||||
@ -508,7 +538,17 @@ export function McpMarketPage() {
|
|||||||
<div className={styles["mcp-market-title"]}>
|
<div className={styles["mcp-market-title"]}>
|
||||||
<div className={styles["mcp-market-name"]}>
|
<div className={styles["mcp-market-name"]}>
|
||||||
{server.name}
|
{server.name}
|
||||||
{getServerStatusDisplay(server.id)}
|
{loadingStates[server.id] && (
|
||||||
|
<span
|
||||||
|
className={styles["operation-status"]}
|
||||||
|
data-status={getOperationStatusType(
|
||||||
|
loadingStates[server.id],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loadingStates[server.id]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!loadingStates[server.id] && getServerStatusDisplay(server.id)}
|
||||||
{server.repo && (
|
{server.repo && (
|
||||||
<a
|
<a
|
||||||
href={server.repo}
|
href={server.repo}
|
||||||
@ -605,8 +645,10 @@ export function McpMarketPage() {
|
|||||||
<div className="window-header-title">
|
<div className="window-header-title">
|
||||||
<div className="window-header-main-title">
|
<div className="window-header-main-title">
|
||||||
MCP Market
|
MCP Market
|
||||||
{isLoading && (
|
{loadingStates["all"] && (
|
||||||
<span className={styles["loading-indicator"]}>Loading...</span>
|
<span className={styles["loading-indicator"]}>
|
||||||
|
{loadingStates["all"]}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="window-header-sub-title">
|
<div className="window-header-sub-title">
|
||||||
|
@ -98,6 +98,9 @@ async function initializeSingleClient(
|
|||||||
try {
|
try {
|
||||||
const client = await createClient(clientId, serverConfig);
|
const client = await createClient(clientId, serverConfig);
|
||||||
const tools = await listTools(client);
|
const tools = await listTools(client);
|
||||||
|
logger.info(
|
||||||
|
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
|
||||||
|
);
|
||||||
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||||
logger.success(`Client [${clientId}] initialized successfully`);
|
logger.success(`Client [${clientId}] initialized successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -130,6 +133,13 @@ export async function initializeMcpSystem() {
|
|||||||
export async function addMcpServer(clientId: string, config: ServerConfig) {
|
export async function addMcpServer(clientId: string, config: ServerConfig) {
|
||||||
try {
|
try {
|
||||||
const currentConfig = await getMcpConfigFromFile();
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const isNewServer = !(clientId in currentConfig.mcpServers);
|
||||||
|
|
||||||
|
// 如果是新服务器,设置默认状态为 active
|
||||||
|
if (isNewServer && !config.status) {
|
||||||
|
config.status = "active";
|
||||||
|
}
|
||||||
|
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
...currentConfig,
|
...currentConfig,
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@ -138,8 +148,12 @@ export async function addMcpServer(clientId: string, config: ServerConfig) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
await updateMcpConfig(newConfig);
|
await updateMcpConfig(newConfig);
|
||||||
// 只初始化新添加的服务器
|
|
||||||
await initializeSingleClient(clientId, config);
|
// 只有新服务器或状态为 active 的服务器才初始化
|
||||||
|
if (isNewServer || config.status === "active") {
|
||||||
|
await initializeSingleClient(clientId, config);
|
||||||
|
}
|
||||||
|
|
||||||
return newConfig;
|
return newConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to add server [${clientId}]: ${error}`);
|
logger.error(`Failed to add server [${clientId}]: ${error}`);
|
||||||
|
Loading…
Reference in New Issue
Block a user