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