import { useEffect, useMemo, useState } from 'react'; import { Bot, Edit, Eye, Plus, Search, Sparkles, Trash2, X } from 'lucide-react'; import { useTheme } from '../../contexts/ThemeContext'; interface AgentItem { key: string; type: 'builtin' | 'custom' | string; deletable: boolean; name: string; description: string; default_prompt: string; schema_hint: string; bound_skills: string[]; order: number; } interface SkillItem { key: string; name: string; description?: string; } interface AgentCardListProps { onEdit?: (key: string) => void; } type TypeFilter = 'all' | 'builtin' | 'custom'; interface CreateAgentForm { name: string; description: string; bound_skills: string[]; } const INITIAL_FORM: CreateAgentForm = { name: '', description: '', bound_skills: [], }; const backUrl = 'https://ch1.b.uat.4.cn/zncmback'; export default function AgentCardList({ onEdit }: AgentCardListProps) { const { theme } = useTheme(); const isDark = theme === 'dark'; const [agents, setAgents] = useState([]); const [skills, setSkills] = useState([]); const [keyword, setKeyword] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [creating, setCreating] = useState(false); const [createForm, setCreateForm] = useState(INITIAL_FORM); const [skillKeyword, setSkillKeyword] = useState(''); const showNotice = (message: string) => { setNotice(message); window.setTimeout(() => { setNotice((current) => (current === message ? null : current)); }, 2400); }; const loadData = async () => { setLoading(true); setError(null); try { const [agentRes, skillRes] = await Promise.all([fetch(`${backUrl}/api/agents`), fetch(`${backUrl}/api/skills`)]); if (!agentRes.ok) { throw new Error(`加载 MCP 失败:${agentRes.status}`); } const agentData = (await agentRes.json()) as AgentItem[]; const skillData = skillRes.ok ? ((await skillRes.json()) as SkillItem[]) : []; setAgents(agentData.sort((a, b) => a.order - b.order)); setSkills(skillData); } catch (e) { setError(e instanceof Error ? e.message : '加载失败'); } finally { setLoading(false); } }; useEffect(() => { void loadData(); }, []); const filteredAgents = useMemo(() => { const kw = keyword.trim().toLowerCase(); return agents.filter((agent) => { const matchesKeyword = !kw || agent.name.toLowerCase().includes(kw) || agent.key.toLowerCase().includes(kw) || agent.description.toLowerCase().includes(kw); const matchesType = typeFilter === 'all' || agent.type === typeFilter; return matchesKeyword && matchesType; }); }, [agents, keyword, typeFilter]); const filteredSkills = useMemo(() => { const kw = skillKeyword.trim().toLowerCase(); if (!kw) { return skills; } return skills.filter((skill) => `${skill.name} ${skill.key} ${skill.description || ''}`.toLowerCase().includes(kw), ); }, [skillKeyword, skills]); const resetCreateForm = () => { setCreateForm(INITIAL_FORM); setSkillKeyword(''); }; const openCreateModal = () => { resetCreateForm(); setCreateOpen(true); }; const closeCreateModal = () => { setCreateOpen(false); resetCreateForm(); }; const toggleSkill = (skillKey: string) => { setCreateForm((prev) => ({ ...prev, bound_skills: prev.bound_skills.includes(skillKey) ? prev.bound_skills.filter((item) => item !== skillKey) : [...prev.bound_skills, skillKey], })); }; const handleCreate = async () => { const name = createForm.name.trim(); const description = createForm.description.trim(); if (!name) { setError('请先输入 MCP 名称'); return; } setCreating(true); setError(null); try { const res = await fetch(`${backUrl}/api/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, bound_skills: createForm.bound_skills, }), }); if (!res.ok) { let detail = `新增失败:${res.status}`; try { const body = (await res.json()) as { detail?: string }; if (body.detail) { detail = body.detail; } } catch { // ignore json parse errors } throw new Error(detail); } await res.json(); await loadData(); closeCreateModal(); showNotice('MCP 已新增'); } catch (e) { setError(e instanceof Error ? e.message : '新增失败'); } finally { setCreating(false); } }; const handleDelete = async (agent: AgentItem) => { if (!agent.deletable) return; const ok = window.confirm(`确认删除 MCP “${agent.name}”吗?`); if (!ok) return; const res = await fetch(`${backUrl}/api/agents/${agent.key}`, { method: 'DELETE' }); if (!res.ok) { setError(`删除失败:${res.status}`); return; } await loadData(); showNotice('MCP 已删除'); }; const selectedSkillCount = createForm.bound_skills.length; return (

MCP 智能体配置

管理内置与用户新增的 MCP,并支持绑定技能

setKeyword(e.target.value)} placeholder="搜索 MCP 名称、Key 或描述" className={`w-full rounded-xl border py-2.5 pl-10 pr-4 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-red-500/20 ${ isDark ? 'border-blue-800/30 bg-slate-800/50 text-white placeholder-slate-500' : 'border-red-200 bg-white text-gray-900 placeholder-red-300' }`} />
{[ { key: 'all', label: '全部' }, { key: 'builtin', label: '内置 MCP' }, { key: 'custom', label: '用户新增' }, ].map((item) => { const active = typeFilter === item.key; return ( ); })}
{loading ?
加载中...
: null} {error ?
{error}
: null} {notice ?
{notice}
: null} {!loading && !error ? (
{filteredAgents.map((agent) => (

{agent.name}

{agent.key}

{agent.type === 'builtin' ? '内置' : '自定义'} #{agent.order + 1}

{agent.description || '暂无描述'}

已绑定技能
{agent.bound_skills.length === 0 ? ( 未绑定 ) : ( agent.bound_skills.map((skillKey) => { const skill = skills.find((item) => item.key === skillKey); return ( {skill?.name || skillKey} ); }) )}
{agent.deletable ? ( ) : null}
))}
) : null} {!loading && !error && filteredAgents.length === 0 ? (
没有匹配的 MCP
) : null} {createOpen ? (
{ if (event.target === event.currentTarget) { closeCreateModal(); } }} >

新增 MCP

输入 MCP 名称、详情并选择要绑定的技能

setCreateForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="例如:游戏分析助手" className={`w-full rounded-xl border px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-red-500/20 ${ isDark ? 'border-slate-700 bg-slate-950 text-white placeholder:text-slate-500' : 'border-gray-200 bg-white text-gray-900 placeholder:text-gray-400' }`} />