fix: status icon

This commit is contained in:
wangcham
2025-10-25 01:58:52 +00:00
parent e3821b3f09
commit 8345edd9f7
5 changed files with 221 additions and 39 deletions

View File

@@ -4,7 +4,7 @@ export class MCPCardVO {
name: string;
mode: 'stdio' | 'sse';
enable: boolean;
status: 'connected' | 'disconnected' | 'error';
status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: number;
error?: string;
config: MCPServerConfig;
@@ -13,9 +13,14 @@ export class MCPCardVO {
this.name = data.name;
this.mode = data.mode;
this.enable = data.enable;
this.status = data.status;
// 将后端返回的 "enabled" 状态映射为 "connected"
this.status = (data.status as string) === 'enabled'
? 'connected'
: data.status;
// tools可能是数组或数字
this.tools = Array.isArray(data.tools) ? data.tools.length : (data.tools || 0);
this.tools = Array.isArray(data.tools)
? data.tools.length
: data.tools || 0;
this.error = data.error;
this.config = data.config;
}
@@ -28,6 +33,8 @@ export class MCPCardVO {
return 'text-gray-500';
case 'error':
return 'text-red-600';
case 'disabled':
return 'text-gray-400';
default:
return 'text-gray-500';
}
@@ -41,6 +48,8 @@ export class MCPCardVO {
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'error':
return 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disabled':
return 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636';
default:
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
}

View File

@@ -31,7 +31,7 @@ export interface MCPComponentRef {
}
// eslint-disable-next-line react/display-name
const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
const MCPComponent = forwardRef<MCPComponentRef>((_props, ref) => {
const { t } = useTranslation();
const [serverList, setServerList] = useState<MCPCardVO[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
@@ -39,6 +39,9 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
const [serverToDelete, setServerToDelete] = useState<MCPCardVO | null>(null);
const [deleting, setDeleting] = useState<boolean>(false);
const [autoTestTriggered, setAutoTestTriggered] = useState<boolean>(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [testingServers, setTestingServers] = useState<Set<string>>(new Set());
useEffect(() => {
initData();
@@ -46,22 +49,130 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
}, []);
function initData() {
getServerList();
getServerList(true);
}
function getServerList() {
function getServerList(shouldAutoTest: boolean = false) {
console.log('[MCP] Fetching server list...');
httpClient
.getMCPServers()
.then((value) => {
setServerList(value.servers.map((server) => new MCPCardVO(server)));
const servers = value.servers.map((server) => new MCPCardVO(server));
console.log(
'[MCP] Server list updated:',
servers.map((s) => ({
name: s.name,
status: s.status,
tools: s.tools,
})),
);
setServerList(servers);
// 自动测试:仅在初始加载且还未触发过自动测试时执行
if (shouldAutoTest && !autoTestTriggered && servers.length > 0) {
setAutoTestTriggered(true);
testAllServers(servers);
}
})
.catch((error) => {
toast.error(t('mcp.getServerListError') + error.message);
});
}
async function testAllServers(servers: MCPCardVO[]) {
// 为每个服务器启动测试
console.log('[MCP] Starting tests for all servers:', servers.length);
const testPromises = servers.map((server) => testServer(server.name));
// 等待所有测试完成
try {
await Promise.all(testPromises);
console.log('[MCP] All tests completed, refreshing server list...');
// 所有测试完成后延迟1秒再刷新确保后端状态已更新
setTimeout(() => {
console.log('[MCP] Refreshing server list after tests');
getServerList(false);
}, 1000);
} catch (err) {
console.error('[MCP] Some tests failed:', err);
// 即使有失败,也要刷新列表
setTimeout(() => {
console.log('[MCP] Refreshing server list after test failures');
getServerList(false);
}, 1000);
}
}
function testServer(serverName: string): Promise<void> {
return new Promise((resolve, reject) => {
// 标记为正在测试
console.log(`[MCP] Starting test for server: ${serverName}`);
setTestingServers((prev) => new Set(prev).add(serverName));
httpClient
.testMCPServer(serverName)
.then((resp) => {
const taskId = resp.task_id;
console.log(
`[MCP] Test task created for ${serverName}, task_id: ${taskId}`,
);
// 监控任务状态
const interval = setInterval(() => {
httpClient
.getAsyncTask(taskId)
.then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
// 标记测试完成
setTestingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
if (taskResp.runtime.exception) {
console.error(
`[MCP] Test failed for ${serverName}:`,
taskResp.runtime.exception,
);
reject(new Error(taskResp.runtime.exception));
} else {
console.log(
`[MCP] Test completed successfully for ${serverName}`,
);
resolve();
}
}
})
.catch((err) => {
clearInterval(interval);
setTestingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
console.error(
`[MCP] Error monitoring task for ${serverName}:`,
err,
);
reject(err);
});
}, 1000);
})
.catch((err) => {
console.error(`[MCP] Failed to start test for ${serverName}:`, err);
setTestingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
reject(err);
});
});
}
useImperativeHandle(ref, () => ({
refreshServerList: getServerList,
refreshServerList: () => getServerList(false),
createServer: () => {
setSelectedServer(null);
setModalOpen(true);
@@ -99,7 +210,7 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
toast.error(t('mcp.deleteError') + taskResp.runtime.exception);
} else {
toast.success(t('mcp.deleteSuccess'));
getServerList();
getServerList(false);
}
}
});
@@ -128,13 +239,13 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
</div>
) : (
<div className={`${styles.pluginListContainer}`}>
{serverList.map((vo, index) => {
{serverList.map((vo) => {
return (
<div key={index} className="relative group">
<div key={vo.name} className="relative group">
<MCPCardComponent
cardVO={vo}
onCardClick={() => handleServerClick(vo)}
onRefresh={getServerList}
onRefresh={() => getServerList(false)}
/>
{/* 删除按钮 */}
@@ -177,7 +288,7 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
isEdit={!!selectedServer}
onFormSubmit={() => {
setModalOpen(false);
getServerList();
getServerList(false);
}}
onFormCancel={() => {
setModalOpen(false);

View File

@@ -1,5 +1,5 @@
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
@@ -21,6 +21,51 @@ export default function MCPCardComponent({
const [switchEnable, setSwitchEnable] = useState(true);
const [testing, setTesting] = useState(false);
const [toolsCount, setToolsCount] = useState(cardVO.tools);
const [status, setStatus] = useState(cardVO.status);
const [error, setError] = useState(cardVO.error);
// 响应cardVO的变化更新本地状态
useEffect(() => {
console.log(`[MCPCard ${cardVO.name}] Status updated:`, {
status: cardVO.status,
tools: cardVO.tools,
error: cardVO.error,
});
setStatus(cardVO.status);
setError(cardVO.error);
setToolsCount(cardVO.tools);
setEnabled(cardVO.enable);
}, [cardVO.name, cardVO.status, cardVO.error, cardVO.tools, cardVO.enable]);
function getStatusColor(): string {
switch (status) {
case 'connected':
return 'text-green-600';
case 'disconnected':
return 'text-gray-500';
case 'error':
return 'text-red-600';
case 'disabled':
return 'text-gray-400';
default:
return 'text-gray-500';
}
}
function getStatusIcon(): string {
switch (status) {
case 'connected':
return 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disconnected':
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'error':
return 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disabled':
return 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636';
default:
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
}
}
function handleEnable(checked: boolean) {
setSwitchEnable(false);
@@ -55,6 +100,17 @@ export default function MCPCardComponent({
} else {
// 解析测试结果获取工具数量
try {
const rawResult = taskResp.runtime.result as
| string
| {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
}
| undefined;
if (rawResult) {
let result: {
status?: string;
tools_count?: number;
@@ -62,16 +118,21 @@ export default function MCPCardComponent({
error?: string;
};
const rawResult: any = taskResp.runtime.result;
if (typeof rawResult === 'string') {
result = JSON.parse(rawResult.replace(/'/g, '"'));
} else {
result = rawResult as typeof result;
result = rawResult;
}
if (result.tools_count !== undefined) {
setToolsCount(result.tools_count);
toast.success(t('mcp.testSuccess') + ` - ${result.tools_count} ${t('mcp.toolsFound')}`);
toast.success(
t('mcp.testSuccess') +
` - ${result.tools_count} ${t('mcp.toolsFound')}`,
);
} else {
toast.success(t('mcp.testSuccess'));
}
} else {
toast.success(t('mcp.testSuccess'));
}
@@ -120,7 +181,7 @@ export default function MCPCardComponent({
<div className="flex flex-row items-center justify-start gap-[0.4rem] mt-1">
<svg
className={`w-4 h-4 ${cardVO.getStatusColor()}`}
className={`w-4 h-4 ${getStatusColor()}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -130,20 +191,20 @@ export default function MCPCardComponent({
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d={cardVO.getStatusIcon()}
d={getStatusIcon()}
/>
</svg>
<div className={`text-[0.8rem] ${cardVO.getStatusColor()}`}>
{cardVO.status === 'connected' && t('mcp.statusConnected')}
{cardVO.status === 'disconnected' &&
t('mcp.statusDisconnected')}
{cardVO.status === 'error' && t('mcp.statusError')}
<div className={`text-[0.8rem] ${getStatusColor()}`}>
{status === 'connected' && t('mcp.statusConnected')}
{status === 'disconnected' && t('mcp.statusDisconnected')}
{status === 'error' && t('mcp.statusError')}
{status === 'disabled' && t('mcp.statusDisabled')}
</div>
</div>
{cardVO.error && (
{error && (
<div className="text-[0.7rem] text-red-500 line-clamp-2 mt-1">
{cardVO.error}
{error}
</div>
)}
</div>

View File

@@ -324,7 +324,7 @@ export interface MCPServer {
mode: 'stdio' | 'sse';
enable: boolean;
config: MCPServerConfig;
status: 'connected' | 'disconnected' | 'error';
status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: MCPTool[];
error?: string;
}

View File

@@ -320,6 +320,7 @@ const zhHans = {
statusConnected: '已连接',
statusDisconnected: '未连接',
statusError: '连接错误',
statusDisabled: '已禁用',
serverStatus: '服务器状态',
marketplace: 'MCP商店',
searchServer: '搜索MCP服务器',