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; name: string;
mode: 'stdio' | 'sse'; mode: 'stdio' | 'sse';
enable: boolean; enable: boolean;
status: 'connected' | 'disconnected' | 'error'; status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: number; tools: number;
error?: string; error?: string;
config: MCPServerConfig; config: MCPServerConfig;
@@ -13,9 +13,14 @@ export class MCPCardVO {
this.name = data.name; this.name = data.name;
this.mode = data.mode; this.mode = data.mode;
this.enable = data.enable; this.enable = data.enable;
this.status = data.status; // 将后端返回的 "enabled" 状态映射为 "connected"
this.status = (data.status as string) === 'enabled'
? 'connected'
: data.status;
// tools可能是数组或数字 // 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.error = data.error;
this.config = data.config; this.config = data.config;
} }
@@ -28,6 +33,8 @@ export class MCPCardVO {
return 'text-gray-500'; return 'text-gray-500';
case 'error': case 'error':
return 'text-red-600'; return 'text-red-600';
case 'disabled':
return 'text-gray-400';
default: default:
return 'text-gray-500'; 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'; 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': case 'error':
return 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'; 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: default:
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'; 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 // eslint-disable-next-line react/display-name
const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => { const MCPComponent = forwardRef<MCPComponentRef>((_props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [serverList, setServerList] = useState<MCPCardVO[]>([]); const [serverList, setServerList] = useState<MCPCardVO[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false); const [modalOpen, setModalOpen] = useState<boolean>(false);
@@ -39,6 +39,9 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
const [serverToDelete, setServerToDelete] = useState<MCPCardVO | null>(null); const [serverToDelete, setServerToDelete] = useState<MCPCardVO | null>(null);
const [deleting, setDeleting] = useState<boolean>(false); 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(() => { useEffect(() => {
initData(); initData();
@@ -46,22 +49,130 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
}, []); }, []);
function initData() { function initData() {
getServerList(); getServerList(true);
} }
function getServerList() { function getServerList(shouldAutoTest: boolean = false) {
console.log('[MCP] Fetching server list...');
httpClient httpClient
.getMCPServers() .getMCPServers()
.then((value) => { .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) => { .catch((error) => {
toast.error(t('mcp.getServerListError') + error.message); 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, () => ({ useImperativeHandle(ref, () => ({
refreshServerList: getServerList, refreshServerList: () => getServerList(false),
createServer: () => { createServer: () => {
setSelectedServer(null); setSelectedServer(null);
setModalOpen(true); setModalOpen(true);
@@ -99,7 +210,7 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
toast.error(t('mcp.deleteError') + taskResp.runtime.exception); toast.error(t('mcp.deleteError') + taskResp.runtime.exception);
} else { } else {
toast.success(t('mcp.deleteSuccess')); toast.success(t('mcp.deleteSuccess'));
getServerList(); getServerList(false);
} }
} }
}); });
@@ -128,13 +239,13 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
</div> </div>
) : ( ) : (
<div className={`${styles.pluginListContainer}`}> <div className={`${styles.pluginListContainer}`}>
{serverList.map((vo, index) => { {serverList.map((vo) => {
return ( return (
<div key={index} className="relative group"> <div key={vo.name} className="relative group">
<MCPCardComponent <MCPCardComponent
cardVO={vo} cardVO={vo}
onCardClick={() => handleServerClick(vo)} onCardClick={() => handleServerClick(vo)}
onRefresh={getServerList} onRefresh={() => getServerList(false)}
/> />
{/* 删除按钮 */} {/* 删除按钮 */}
@@ -177,7 +288,7 @@ const MCPComponent = forwardRef<MCPComponentRef>((props, ref) => {
isEdit={!!selectedServer} isEdit={!!selectedServer}
onFormSubmit={() => { onFormSubmit={() => {
setModalOpen(false); setModalOpen(false);
getServerList(); getServerList(false);
}} }}
onFormCancel={() => { onFormCancel={() => {
setModalOpen(false); setModalOpen(false);

View File

@@ -1,5 +1,5 @@
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO'; 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 { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
@@ -21,6 +21,51 @@ export default function MCPCardComponent({
const [switchEnable, setSwitchEnable] = useState(true); const [switchEnable, setSwitchEnable] = useState(true);
const [testing, setTesting] = useState(false); const [testing, setTesting] = useState(false);
const [toolsCount, setToolsCount] = useState(cardVO.tools); 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) { function handleEnable(checked: boolean) {
setSwitchEnable(false); setSwitchEnable(false);
@@ -55,6 +100,17 @@ export default function MCPCardComponent({
} else { } else {
// 解析测试结果获取工具数量 // 解析测试结果获取工具数量
try { try {
const rawResult = taskResp.runtime.result as
| string
| {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
}
| undefined;
if (rawResult) {
let result: { let result: {
status?: string; status?: string;
tools_count?: number; tools_count?: number;
@@ -62,16 +118,21 @@ export default function MCPCardComponent({
error?: string; error?: string;
}; };
const rawResult: any = taskResp.runtime.result;
if (typeof rawResult === 'string') { if (typeof rawResult === 'string') {
result = JSON.parse(rawResult.replace(/'/g, '"')); result = JSON.parse(rawResult.replace(/'/g, '"'));
} else { } else {
result = rawResult as typeof result; result = rawResult;
} }
if (result.tools_count !== undefined) { if (result.tools_count !== undefined) {
setToolsCount(result.tools_count); 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 { } else {
toast.success(t('mcp.testSuccess')); 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"> <div className="flex flex-row items-center justify-start gap-[0.4rem] mt-1">
<svg <svg
className={`w-4 h-4 ${cardVO.getStatusColor()}`} className={`w-4 h-4 ${getStatusColor()}`}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -130,20 +191,20 @@ export default function MCPCardComponent({
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} strokeWidth={2}
d={cardVO.getStatusIcon()} d={getStatusIcon()}
/> />
</svg> </svg>
<div className={`text-[0.8rem] ${cardVO.getStatusColor()}`}> <div className={`text-[0.8rem] ${getStatusColor()}`}>
{cardVO.status === 'connected' && t('mcp.statusConnected')} {status === 'connected' && t('mcp.statusConnected')}
{cardVO.status === 'disconnected' && {status === 'disconnected' && t('mcp.statusDisconnected')}
t('mcp.statusDisconnected')} {status === 'error' && t('mcp.statusError')}
{cardVO.status === 'error' && t('mcp.statusError')} {status === 'disabled' && t('mcp.statusDisabled')}
</div> </div>
</div> </div>
{cardVO.error && ( {error && (
<div className="text-[0.7rem] text-red-500 line-clamp-2 mt-1"> <div className="text-[0.7rem] text-red-500 line-clamp-2 mt-1">
{cardVO.error} {error}
</div> </div>
)} )}
</div> </div>

View File

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

View File

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