Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
基于 Tauri MCP 实机排查发现并修复:
1. VikingPanel: viking_ls('/') 返回0 → 改为 viking_ls('') 返回100条记忆
2. 技能列表: loadSkillsCatalog 静默失败 → 添加直接 invoke('skill_list') 回退
3. 审计日志: 面板读Gateway API无数据 → 回退读localStorage双源数据
4. 工作区: 浏览按钮无事件 → 接入prompt选择 + workspace_dir_stats 命令
5. MCP: 空列表无引导 → 添加配置文件路径提示
6. 新增 workspace_dir_stats Tauri 命令 (Rust)
排查确认正常的功能: 安全存储(OS Keyring✅), 心跳引擎(运行中✅),
定时任务(管道连通), Kernel(已初始化✅), SaaS relay模式
200 lines
7.8 KiB
TypeScript
200 lines
7.8 KiB
TypeScript
import { FileText, Globe, RefreshCw, Wrench } from 'lucide-react';
|
||
import { useCallback, useEffect, useState } from 'react';
|
||
import { useConfigStore } from '../../store/configStore';
|
||
import { silentErrorHandler } from '../../lib/error-utils';
|
||
import {
|
||
listMcpServices,
|
||
startMcpService,
|
||
stopMcpService,
|
||
type McpServiceConfig,
|
||
type McpServiceStatus,
|
||
type McpToolInfo,
|
||
} from '../../lib/mcp-client';
|
||
|
||
export function MCPServices() {
|
||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
|
||
|
||
const services = quickConfig.mcpServices || [];
|
||
const [runningServices, setRunningServices] = useState<McpServiceStatus[]>([]);
|
||
const [loading, setLoading] = useState<string | null>(null);
|
||
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
|
||
|
||
// Fetch running services on mount
|
||
const refreshRunning = useCallback(async () => {
|
||
try {
|
||
const running = await listMcpServices();
|
||
setRunningServices(running);
|
||
} catch {
|
||
// MCP might not be available yet
|
||
setRunningServices([]);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
refreshRunning();
|
||
}, [refreshRunning]);
|
||
|
||
const toggleTools = (name: string) => {
|
||
setExpandedTools((prev) => {
|
||
const next = new Set(prev);
|
||
if (next.has(name)) next.delete(name);
|
||
else next.add(name);
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const toggleService = async (id: string) => {
|
||
const svc = services.find((s) => s.id === id);
|
||
if (!svc) return;
|
||
|
||
setLoading(id);
|
||
try {
|
||
if (svc.enabled) {
|
||
// Currently enabled → stop it
|
||
await stopMcpService(svc.name || svc.id).catch(() => {});
|
||
} else {
|
||
// Currently disabled → start it
|
||
const config: McpServiceConfig = {
|
||
name: svc.name || svc.id,
|
||
command: svc.command || '',
|
||
args: svc.args,
|
||
env: svc.env,
|
||
cwd: svc.cwd,
|
||
};
|
||
await startMcpService(config);
|
||
}
|
||
// Update config flag
|
||
const nextServices = services.map((s) =>
|
||
s.id === id ? { ...s, enabled: !s.enabled } : s
|
||
);
|
||
await saveQuickConfig({ mcpServices: nextServices });
|
||
// Refresh running status
|
||
await refreshRunning();
|
||
} catch (err) {
|
||
console.error('[MCP] Toggle failed:', err);
|
||
} finally {
|
||
setLoading(null);
|
||
}
|
||
};
|
||
|
||
// Build a map of service name → running status for quick lookup
|
||
const runningMap = new Map(runningServices.map((rs) => [rs.name, rs]));
|
||
|
||
return (
|
||
<div className="max-w-3xl">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h1 className="text-xl font-bold text-gray-900">MCP 服务</h1>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs text-gray-400">
|
||
{runningServices.length} 个运行中 / {services.length} 个已声明
|
||
</span>
|
||
<button
|
||
onClick={() => refreshRunning().catch(() => {})}
|
||
className="p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
|
||
title="刷新状态"
|
||
>
|
||
<RefreshCw className="w-3.5 h-3.5 text-gray-400" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="text-xs text-gray-500 mb-6">
|
||
MCP(模型上下文协议)服务为 Agent 扩展外部工具 — 文件系统、数据库、网页搜索等。
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm mb-6">
|
||
{services.length > 0 ? services.map((svc) => {
|
||
const isRunning = runningMap.has(svc.name || svc.id);
|
||
const status = runningMap.get(svc.name || svc.id);
|
||
const isLoading = loading === svc.id;
|
||
const showTools = expandedTools.has(svc.id);
|
||
|
||
return (
|
||
<div key={svc.id}>
|
||
<div className="flex justify-between items-center p-4">
|
||
<div className="flex items-center gap-3">
|
||
{svc.id === 'filesystem'
|
||
? <FileText className="w-4 h-4 text-gray-500" />
|
||
: <Globe className="w-4 h-4 text-gray-500" />}
|
||
<div>
|
||
<div className="text-sm text-gray-900">{svc.name}</div>
|
||
<div className="text-xs text-gray-400 mt-0.5">
|
||
{svc.id}
|
||
{svc.command && (
|
||
<span className="ml-2 text-gray-300">|</span>
|
||
)}
|
||
{svc.command && (
|
||
<span className="ml-2 font-mono text-gray-400">{svc.command}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2 items-center">
|
||
{isRunning && status && (
|
||
<button
|
||
onClick={() => toggleTools(svc.id)}
|
||
className="flex items-center gap-1 text-xs px-2 py-1 rounded-full bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors"
|
||
>
|
||
<Wrench className="w-3 h-3" />
|
||
{status.tool_count} 工具
|
||
</button>
|
||
)}
|
||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||
isLoading ? 'bg-yellow-50 text-yellow-600' :
|
||
isRunning ? 'bg-green-50 text-green-600' :
|
||
svc.enabled ? 'bg-amber-50 text-amber-600' :
|
||
'bg-gray-100 text-gray-500'
|
||
}`}>
|
||
{isLoading ? '处理中...' :
|
||
isRunning ? '运行中' :
|
||
svc.enabled ? '已启用' : '已停用'}
|
||
</span>
|
||
<button
|
||
onClick={() => toggleService(svc.id).catch(silentErrorHandler('MCPServices'))}
|
||
disabled={isLoading}
|
||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||
>
|
||
{svc.enabled ? '停用' : '启用'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/* Expanded tools list */}
|
||
{showTools && status && status.tools.length > 0 && (
|
||
<div className="px-4 pb-3 border-t border-gray-50">
|
||
<div className="text-xs text-gray-500 mb-2 mt-2">已发现的工具:</div>
|
||
<div className="space-y-1">
|
||
{status.tools.map((tool: McpToolInfo) => (
|
||
<div key={`${tool.service_name}-${tool.tool_name}`} className="flex items-start gap-2 text-xs py-1">
|
||
<Wrench className="w-3 h-3 text-gray-400 mt-0.5 shrink-0" />
|
||
<div>
|
||
<span className="font-medium text-gray-700">{tool.tool_name}</span>
|
||
{tool.description && (
|
||
<span className="text-gray-400 ml-2">{tool.description}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}) : (
|
||
<div className="p-8 text-center">
|
||
<div className="text-sm text-gray-400 mb-2">尚未配置 MCP 服务</div>
|
||
<div className="text-xs text-gray-300">
|
||
MCP 服务为 Agent 扩展外部工具能力。可通过编辑配置文件
|
||
<code className="mx-1 text-gray-500 bg-gray-100 px-1 rounded">config/mcp.toml</code>
|
||
添加服务。
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
|
||
新增/删除服务尚未在桌面端 UI 接入。可通过编辑 config/mcp.toml 手动添加 MCP 服务配置,重启后生效。
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|