Files
zclaw_openfang/desktop/src/components/Settings/MCPServices.tsx
iven 1d0e60d028
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
fix(ui): 9项端到端真实审计 — 修复记忆/技能/审计/工作区/MCP数据流断裂
基于 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模式
2026-04-10 23:00:19 +08:00

200 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}