mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 11:29:39 +08:00
fix: status icon
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ const zhHans = {
|
||||
statusConnected: '已连接',
|
||||
statusDisconnected: '未连接',
|
||||
statusError: '连接错误',
|
||||
statusDisabled: '已禁用',
|
||||
serverStatus: '服务器状态',
|
||||
marketplace: 'MCP商店',
|
||||
searchServer: '搜索MCP服务器',
|
||||
|
||||
Reference in New Issue
Block a user