feat(admin): 移除后台 Agent/技能/MCP 手动创建按钮,改为仅支持 URL 导入/AI 开发/AI 安装 - 后台 Agent管理/技能管理/MCP服务 移除 创建/添加 按钮 - 后端上传/创建按操作者角色设定默认值:管理员=官方+已发布+免费;用户=用户+待审核+定价自选 - 应用市场 Hub 导入新增 mcp.so / MCPMarket 平台,导入表单增加定价输入(用户可选收费) - 清理全部演示数据(market_apps/mcp_servers/mcp_tool_configs/evolution_skills)

This commit is contained in:
qyyfw committed 2026-07-10 16:21:07 +08:00
1 parent d62848ee98
commit 27ba91ae5e
8 files changed
+7414 -6115

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+405 -404
View File
@@ -1,404 +1,405 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
const styles: Record<string, React.CSSProperties> = { const styles: Record<string, React.CSSProperties> = {
container: { padding: 24, backgroundColor: '#f5f7fa', minHeight: '100vh' }, container: { padding: 24, backgroundColor: '#f5f7fa', minHeight: '100vh' },
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 },
title: { fontSize: 24, fontWeight: 700, color: '#1a1a2e' }, title: { fontSize: 24, fontWeight: 700, color: '#1A1F36' },
addBtn: { padding: '10px 24px', backgroundColor: '#4f46e5', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600 }, addBtn: { padding: '10px 24px', backgroundColor: '#4f46e5', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600 },
statsRow: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 24 }, statsRow: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 24 },
statCard: { backgroundColor: '#fff', padding: 20, borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }, statCard: { backgroundColor: '#fff', padding: 20, borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' },
statLabel: { fontSize: 13, color: '#6b7280', marginBottom: 8 }, statLabel: { fontSize: 13, color: '#6b7280', marginBottom: 8 },
statValue: { fontSize: 28, fontWeight: 700, color: '#1a1a2e' }, statValue: { fontSize: 28, fontWeight: 700, color: '#1A1F36' },
toolbar: { display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' as const }, toolbar: { display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' as const },
searchInput: { padding: '8px 16px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, width: 250 }, searchInput: { padding: '8px 16px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, width: 250 },
filterSelect: { padding: '8px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, backgroundColor: '#fff' }, filterSelect: { padding: '8px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, backgroundColor: '#fff' },
table: { width: '100%', borderCollapse: 'collapse' as const, backgroundColor: '#fff', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }, table: { width: '100%', borderCollapse: 'collapse' as const, backgroundColor: '#fff', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' },
th: { padding: '12px 16px', textAlign: 'left' as const, fontSize: 13, fontWeight: 600, color: '#6b7280', backgroundColor: '#f9fafb', borderBottom: '1px solid #e5e7eb' }, th: { padding: '12px 16px', textAlign: 'left' as const, fontSize: 13, fontWeight: 600, color: '#6b7280', backgroundColor: '#f9fafb', borderBottom: '1px solid #e5e7eb' },
td: { padding: '12px 16px', fontSize: 14, color: '#374151', borderBottom: '1px solid #f3f4f6' }, td: { padding: '12px 16px', fontSize: 14, color: '#374151', borderBottom: '1px solid #f3f4f6' },
badge: { display: 'inline-block', padding: '2px 10px', borderRadius: 20, fontSize: 12, fontWeight: 600 }, badge: { display: 'inline-block', padding: '2px 10px', borderRadius: 20, fontSize: 12, fontWeight: 600 },
actionBtn: { padding: '4px 12px', border: '1px solid #e5e7eb', borderRadius: 6, fontSize: 13, cursor: 'pointer', marginRight: 6, backgroundColor: '#fff' }, actionBtn: { padding: '4px 12px', border: '1px solid #e5e7eb', borderRadius: 6, fontSize: 13, cursor: 'pointer', marginRight: 6, backgroundColor: '#fff' },
pagination: { display: 'flex', justifyContent: 'center', gap: 8, marginTop: 20 }, pagination: { display: 'flex', justifyContent: 'center', gap: 8, marginTop: 20 },
pageBtn: { padding: '8px 16px', border: '1px solid #e5e7eb', borderRadius: 6, cursor: 'pointer', backgroundColor: '#fff', fontSize: 14 }, pageBtn: { padding: '8px 16px', border: '1px solid #e5e7eb', borderRadius: 6, cursor: 'pointer', backgroundColor: '#fff', fontSize: 14 },
modalOverlay: { position: 'fixed' as const, top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000 }, modalOverlay: { position: 'fixed' as const, top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000 },
modal: { backgroundColor: '#fff', borderRadius: 16, padding: 32, width: 560, maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)' }, modal: { backgroundColor: '#fff', borderRadius: 16, padding: 32, width: 560, maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)' },
modalTitle: { fontSize: 18, fontWeight: 700, marginBottom: 24, color: '#1a1a2e' }, modalTitle: { fontSize: 18, fontWeight: 700, marginBottom: 24, color: '#1A1F36' },
formGroup: { marginBottom: 16 }, formGroup: { marginBottom: 16 },
label: { display: 'block', fontSize: 13, fontWeight: 600, color: '#374151', marginBottom: 6 }, label: { display: 'block', fontSize: 13, fontWeight: 600, color: '#374151', marginBottom: 6 },
input: { width: '100%', padding: '10px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, boxSizing: 'border-box' as const }, input: { width: '100%', padding: '10px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, boxSizing: 'border-box' as const },
textarea: { width: '100%', padding: '10px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, minHeight: 80, resize: 'vertical' as const, boxSizing: 'border-box' as const }, textarea: { width: '100%', padding: '10px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, minHeight: 80, resize: 'vertical' as const, boxSizing: 'border-box' as const },
modalActions: { display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 24 }, modalActions: { display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 24 },
cancelBtn: { padding: '10px 24px', border: '1px solid #e5e7eb', borderRadius: 8, cursor: 'pointer', backgroundColor: '#fff', fontSize: 14 }, cancelBtn: { padding: '10px 24px', border: '1px solid #e5e7eb', borderRadius: 8, cursor: 'pointer', backgroundColor: '#fff', fontSize: 14 },
submitBtn: { padding: '10px 24px', backgroundColor: '#4f46e5', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600 }, submitBtn: { padding: '10px 24px', backgroundColor: '#4f46e5', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600 },
toast: { position: 'fixed' as const, top: 20, right: 20, padding: '12px 24px', borderRadius: 8, fontSize: 14, fontWeight: 600, zIndex: 2000, color: '#fff' }, toast: { position: 'fixed' as const, top: 20, right: 20, padding: '12px 24px', borderRadius: 8, fontSize: 14, fontWeight: 600, zIndex: 2000, color: '#fff' },
detailsGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 16 }, detailsGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 16 },
detailsItem: { fontSize: 13 }, detailsItem: { fontSize: 13 },
detailsLabel: { color: '#6b7280', marginBottom: 4 }, detailsLabel: { color: '#6b7280', marginBottom: 4 },
}; };
const TRANSPORT_TYPES = [ const TRANSPORT_TYPES = [
{ value: '', label: 'All Types' }, { value: '', label: '全部类型' },
{ value: 'stdio', label: 'STDIO' }, { value: 'stdio', label: 'STDIO' },
{ value: 'sse', label: 'SSE' }, { value: 'sse', label: 'SSE' },
]; ];
const STATUS_TYPES = [ const STATUS_TYPES = [
{ value: '', label: 'All Status' }, { value: '', label: '全部状态' },
{ value: 'connected', label: 'Connected' }, { value: 'connected', label: '已连接' },
{ value: 'disconnected', label: 'Disconnected' }, { value: 'disconnected', label: '未连接' },
{ value: 'error', label: 'Error' }, { value: 'error', label: '错误' },
]; ];
interface MCPServer { interface MCPServer {
id: string; id: string;
name: string; name: string;
description: string; description: string;
transport_type: string; transport_type: string;
transport_type_label: string; transport_type_label: string;
command: string; command: string;
args: Record<string, any>; args: Record<string, any>;
env_vars: Record<string, string>; env_vars: Record<string, string>;
url: string; url: string;
headers: Record<string, string>; headers: Record<string, string>;
is_enabled: boolean; is_enabled: boolean;
status: string; status: string;
last_connected_at: string; last_connected_at: string;
tools_count: number; tools_count: number;
resources_count: number; resources_count: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export default function AdminMcp() { export default function AdminMcp() {
const [servers, setServers] = useState<MCPServer[]>([]); const [servers, setServers] = useState<MCPServer[]>([]);
const [stats, setStats] = useState<any>({}); const [stats, setStats] = useState<any>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [filterTransport, setFilterTransport] = useState(''); const [filterTransport, setFilterTransport] = useState('');
const [filterStatus, setFilterStatus] = useState(''); const [filterStatus, setFilterStatus] = useState('');
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [editingServer, setEditingServer] = useState<MCPServer | null>(null); const [editingServer, setEditingServer] = useState<MCPServer | null>(null);
const [toast, setToast] = useState<{ message: string; type: string } | null>(null); const [toast, setToast] = useState<{ message: string; type: string } | null>(null);
const pageSize = 20; const pageSize = 20;
const showToast = (message: string, type: string = 'success') => { const showToast = (message: string, type: string = 'success') => {
setToast({ message, type }); setToast({ message, type });
setTimeout(() => setToast(null), 3000); setTimeout(() => setToast(null), 3000);
}; };
const fetchStats = useCallback(async () => { const fetchStats = useCallback(async () => {
try { try {
const result = await adminAPI.getAdminMcpStats() as any; const result = await adminAPI.getAdminMcpStats() as any;
if (result.code === 0) setStats(result.data); if (result.code === 0) setStats(result.data);
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}, []); }, []);
const fetchServers = useCallback(async () => { const fetchServers = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const params: any = { page, page_size: pageSize }; const params: any = { page, page_size: pageSize };
if (search) params.search = search; if (search) params.search = search;
if (filterTransport) params.transport_type = filterTransport; if (filterTransport) params.transport_type = filterTransport;
if (filterStatus) params.status = filterStatus; if (filterStatus) params.status = filterStatus;
const result = await adminAPI.getAdminMcpServers(params) as any; const result = await adminAPI.getAdminMcpServers(params) as any;
if (result.code === 0) { if (result.code === 0) {
setServers(result.data.items || []); setServers(result.data.items || []);
setTotal(result.data.total || 0); setTotal(result.data.total || 0);
} }
} catch (e) { } catch (e) {
showToast('Failed to load servers', 'error'); showToast('加载服务失败', 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, search, filterTransport, filterStatus]); }, [page, search, filterTransport, filterStatus]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
fetchServers(); fetchServers();
}, [fetchStats, fetchServers]); }, [fetchStats, fetchServers]);
const handleCreate = () => { const handleCreate = () => {
setEditingServer(null); setEditingServer(null);
setShowModal(true); setShowModal(true);
}; };
const handleEdit = (server: MCPServer) => { const handleEdit = (server: MCPServer) => {
setEditingServer(server); setEditingServer(server);
setShowModal(true); setShowModal(true);
}; };
const handleDelete = async (server: MCPServer) => { const handleDelete = async (server: MCPServer) => {
if (!confirm(`Delete MCP server "${server.name}"?`)) return; if (!confirm(`确认删除 MCP 服务 "${server.name}"?`)) return;
try { try {
await adminAPI.deleteAdminMcpServer(server.id); await adminAPI.deleteAdminMcpServer(server.id);
showToast('Server deleted'); showToast('服务已删除');
fetchServers(); fetchServers();
fetchStats(); fetchStats();
} catch (e) { } catch (e) {
showToast('Delete failed', 'error'); showToast('删除失败', 'error');
} }
}; };
const handleToggle = async (server: MCPServer) => { const handleToggle = async (server: MCPServer) => {
try { try {
await adminAPI.toggleAdminMcpServer(server.id); await adminAPI.toggleAdminMcpServer(server.id);
showToast(server.is_enabled ? 'Disabled' : 'Enabled'); showToast(server.is_enabled ? '已禁用' : '已启用');
fetchServers(); fetchServers();
fetchStats(); fetchStats();
} catch (e) { } catch (e) {
showToast('Toggle failed', 'error'); showToast('切换失败', 'error');
} }
}; };
const handleTest = async (server: MCPServer) => { const handleTest = async (server: MCPServer) => {
try { try {
const result = await adminAPI.testAdminMcpServer(server.id) as any; const result = await adminAPI.testAdminMcpServer(server.id) as any;
if (result.code === 0) { if (result.code === 0) {
showToast(result.data.success ? 'Connection successful' : `Failed: ${result.data.message}`, result.data.success ? 'success' : 'error'); showToast(result.data.success ? '连接成功' : `连接失败:${result.data.message}`, result.data.success ? 'success' : 'error');
fetchServers(); fetchServers();
fetchStats(); fetchStats();
} }
} catch (e) { } catch (e) {
showToast('Test failed', 'error'); showToast('测试失败', 'error');
} }
}; };
const handleSubmit = async (formData: any) => { const handleSubmit = async (formData: any) => {
try { try {
if (editingServer) { if (editingServer) {
await adminAPI.updateAdminMcpServer(editingServer.id, formData); await adminAPI.updateAdminMcpServer(editingServer.id, formData);
showToast('Server updated'); showToast('服务已更新');
} else { } else {
await adminAPI.createAdminMcpServer(formData); await adminAPI.createAdminMcpServer(formData);
showToast('Server created'); showToast('服务已创建');
} }
setShowModal(false); setShowModal(false);
fetchServers(); fetchServers();
fetchStats(); fetchStats();
} catch (e: any) { } catch (e: any) {
showToast(e.message || 'Operation failed', 'error'); showToast(e.message || '操作失败', 'error');
} }
}; };
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
const colors: Record<string, { bg: string; color: string }> = { const labels: Record<string, string> = { connected: '已连接', disconnected: '未连接', error: '错误' };
connected: { bg: '#dcfce7', color: '#166534' }, const colors: Record<string, { bg: string; color: string }> = {
disconnected: { bg: '#f3f4f6', color: '#6b7280' }, connected: { bg: '#dcfce7', color: '#166534' },
error: { bg: '#fee2e2', color: '#991b1b' }, disconnected: { bg: '#f3f4f6', color: '#6b7280' },
}; error: { bg: '#fee2e2', color: '#991b1b' },
const c = colors[status] || colors.disconnected; };
return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{status}</span>; const c = colors[status] || colors.disconnected;
}; return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{labels[status] || status}</span>;
};
const getTransportBadge = (type: string) => {
const c = type === 'sse' ? { bg: '#e0e7ff', color: '#3730a3' } : { bg: '#fef3c7', color: '#92400e' }; const getTransportBadge = (type: string) => {
return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{type.toUpperCase()}</span>; const c = type === 'sse' ? { bg: '#e0e7ff', color: '#3730a3' } : { bg: '#fef3c7', color: '#92400e' };
}; return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{type === 'sse' ? 'SSE' : type === 'stdio' ? '标准IO' : type}</span>;
};
const totalPages = Math.ceil(total / pageSize);
const totalPages = Math.ceil(total / pageSize);
return (
<div style={styles.container}> return (
{toast && ( <div style={styles.container}>
<div style={{ ...styles.toast, backgroundColor: toast.type === 'error' ? '#ef4444' : '#10b981' }}> {toast && (
{toast.message} <div style={{ ...styles.toast, backgroundColor: toast.type === 'error' ? '#ef4444' : '#10b981' }}>
</div> {toast.message}
)} </div>
)}
<div style={styles.header}>
<h1 style={styles.title}>MCP Servers</h1> <div style={styles.header}>
<button style={styles.addBtn} onClick={handleCreate}>+ Add Server</button> <h1 style={styles.title}>MCP 服务管理</h1>
</div> {/* 手动添加服务入口已移除:MCP 仅支持通过 URL 导入(见应用市场「导入」)或由 AI 聊天自动部署安装 */}
</div>
<div style={styles.statsRow}>
<div style={styles.statCard}> <div style={styles.statsRow}>
<div style={styles.statLabel}>Total Servers</div> <div style={styles.statCard}>
<div style={styles.statValue}>{stats.total_servers || 0}</div> <div style={styles.statLabel}>服务总数</div>
</div> <div style={styles.statValue}>{stats.total_servers || 0}</div>
<div style={styles.statCard}> </div>
<div style={styles.statLabel}>Enabled</div> <div style={styles.statCard}>
<div style={styles.statValue}>{stats.enabled_servers || 0}</div> <div style={styles.statLabel}>已启用</div>
</div> <div style={styles.statValue}>{stats.enabled_servers || 0}</div>
<div style={styles.statCard}> </div>
<div style={styles.statLabel}>Connected</div> <div style={styles.statCard}>
<div style={{ ...styles.statValue, color: '#10b981' }}>{stats.connected_servers || 0}</div> <div style={styles.statLabel}>已连接</div>
</div> <div style={{ ...styles.statValue, color: '#10b981' }}>{stats.connected_servers || 0}</div>
<div style={styles.statCard}> </div>
<div style={styles.statLabel}>By Transport</div> <div style={styles.statCard}>
<div style={{ fontSize: 13, color: '#374151' }}> <div style={styles.statLabel}>传输方式分布</div>
{Object.entries(stats.by_transport || {}).map(([k, v]) => ( <div style={{ fontSize: 13, color: '#374151' }}>
<span key={k} style={{ marginRight: 12 }}>{k}: {v as number}</span> {Object.entries(stats.by_transport || {}).map(([k, v]) => (
))} <span key={k} style={{ marginRight: 12 }}>{k}: {v as number}</span>
</div> ))}
</div> </div>
</div> </div>
</div>
<div style={styles.toolbar}>
<input <div style={styles.toolbar}>
style={styles.searchInput} <input
placeholder="Search servers..." style={styles.searchInput}
value={search} placeholder="搜索服务..."
onChange={e => { setSearch(e.target.value); setPage(1); }} value={search}
/> onChange={e => { setSearch(e.target.value); setPage(1); }}
<select style={styles.filterSelect} value={filterTransport} onChange={e => { setFilterTransport(e.target.value); setPage(1); }}> />
{TRANSPORT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)} <select style={styles.filterSelect} value={filterTransport} onChange={e => { setFilterTransport(e.target.value); setPage(1); }}>
</select> {TRANSPORT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
<select style={styles.filterSelect} value={filterStatus} onChange={e => { setFilterStatus(e.target.value); setPage(1); }}> </select>
{STATUS_TYPES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)} <select style={styles.filterSelect} value={filterStatus} onChange={e => { setFilterStatus(e.target.value); setPage(1); }}>
</select> {STATUS_TYPES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
</div> </select>
</div>
<table style={styles.table}>
<thead> <table style={styles.table}>
<tr> <thead>
<th style={styles.th}>Name</th> <tr>
<th style={styles.th}>Transport</th> <th style={styles.th}>名称</th>
<th style={styles.th}>Status</th> <th style={styles.th}>传输方式</th>
<th style={styles.th}>Tools</th> <th style={styles.th}>状态</th>
<th style={styles.th}>Last Connected</th> <th style={styles.th}>工具数</th>
<th style={styles.th}>Enabled</th> <th style={styles.th}>最后连接</th>
<th style={styles.th}>Actions</th> <th style={styles.th}>启用</th>
</tr> <th style={styles.th}>操作</th>
</thead> </tr>
<tbody> </thead>
{loading ? ( <tbody>
<tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40 }}>Loading...</td></tr> {loading ? (
) : servers.length === 0 ? ( <tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40 }}>加载中...</td></tr>
<tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40, color: '#9ca3af' }}>No MCP servers found</td></tr> ) : servers.length === 0 ? (
) : servers.map(s => ( <tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40, color: '#9ca3af' }}>未找到 MCP 服务</td></tr>
<tr key={s.id}> ) : servers.map(s => (
<td style={styles.td}> <tr key={s.id}>
<div style={{ fontWeight: 600 }}>{s.name}</div> <td style={styles.td}>
<div style={{ fontSize: 12, color: '#9ca3af' }}>{s.description?.substring(0, 50)}{s.description?.length > 50 ? '...' : ''}</div> <div style={{ fontWeight: 600 }}>{s.name}</div>
</td> <div style={{ fontSize: 12, color: '#9ca3af' }}>{s.description?.substring(0, 50)}{s.description?.length > 50 ? '...' : ''}</div>
<td style={styles.td}>{getTransportBadge(s.transport_type)}</td> </td>
<td style={styles.td}>{getStatusBadge(s.status)}</td> <td style={styles.td}>{getTransportBadge(s.transport_type)}</td>
<td style={styles.td}>{s.tools_count || '-'}</td> <td style={styles.td}>{getStatusBadge(s.status)}</td>
<td style={styles.td}>{s.last_connected_at ? new Date(s.last_connected_at).toLocaleString() : 'Never'}</td> <td style={styles.td}>{s.tools_count || '-'}</td>
<td style={styles.td}> <td style={styles.td}>{s.last_connected_at ? new Date(s.last_connected_at).toLocaleString() : '从未'}</td>
<span style={{ ...styles.badge, backgroundColor: s.is_enabled ? '#dcfce7' : '#f3f4f6', color: s.is_enabled ? '#166534' : '#6b7280' }}> <td style={styles.td}>
{s.is_enabled ? 'Yes' : 'No'} <span style={{ ...styles.badge, backgroundColor: s.is_enabled ? '#dcfce7' : '#f3f4f6', color: s.is_enabled ? '#166534' : '#6b7280' }}>
</span> {s.is_enabled ? '是' : '否'}
</td> </span>
<td style={styles.td}> </td>
<button style={styles.actionBtn} onClick={() => handleEdit(s)}>Edit</button> <td style={styles.td}>
<button style={{ ...styles.actionBtn, color: s.is_enabled ? '#ef4444' : '#10b981' }} onClick={() => handleToggle(s)}> <button style={styles.actionBtn} onClick={() => handleEdit(s)}>编辑</button>
{s.is_enabled ? 'Disable' : 'Enable'} <button style={{ ...styles.actionBtn, color: s.is_enabled ? '#ef4444' : '#10b981' }} onClick={() => handleToggle(s)}>
</button> {s.is_enabled ? '禁用' : '启用'}
<button style={{ ...styles.actionBtn, color: '#4f46e5' }} onClick={() => handleTest(s)}>Test</button> </button>
<button style={{ ...styles.actionBtn, color: '#ef4444' }} onClick={() => handleDelete(s)}>Del</button> <button style={{ ...styles.actionBtn, color: '#4f46e5' }} onClick={() => handleTest(s)}>测试</button>
</td> <button style={{ ...styles.actionBtn, color: '#ef4444' }} onClick={() => handleDelete(s)}>删除</button>
</tr> </td>
))} </tr>
</tbody> ))}
</table> </tbody>
</table>
{totalPages > 1 && (
<div style={styles.pagination}> {totalPages > 1 && (
<button style={styles.pageBtn} disabled={page <= 1} onClick={() => setPage(p => p - 1)}>Prev</button> <div style={styles.pagination}>
<span style={{ padding: '8px 12px', fontSize: 14 }}>Page {page} / {totalPages}</span> <button style={styles.pageBtn} disabled={page <= 1} onClick={() => setPage(p => p - 1)}>上一页</button>
<button style={styles.pageBtn} disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>Next</button> <span style={{ padding: '8px 12px', fontSize: 14 }}>第 {page} / {totalPages} 页</span>
</div> <button style={styles.pageBtn} disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>下一页</button>
)} </div>
)}
{showModal && <McpModal server={editingServer} onSubmit={handleSubmit} onClose={() => setShowModal(false)} />}
</div> {showModal && <McpModal server={editingServer} onSubmit={handleSubmit} onClose={() => setShowModal(false)} />}
); </div>
} );
}
function McpModal({ server, onSubmit, onClose }: { server: MCPServer | null; onSubmit: (data: any) => void; onClose: () => void }) {
const [form, setForm] = useState({ function McpModal({ server, onSubmit, onClose }: { server: MCPServer | null; onSubmit: (data: any) => void; onClose: () => void }) {
name: server?.name || '', const [form, setForm] = useState({
description: server?.description || '', name: server?.name || '',
transport_type: server?.transport_type || 'stdio', description: server?.description || '',
command: server?.command || '', transport_type: server?.transport_type || 'stdio',
args: server?.args ? JSON.stringify(server.args, null, 2) : '', command: server?.command || '',
env_vars: server?.env_vars ? JSON.stringify(server.env_vars, null, 2) : '', args: server?.args ? JSON.stringify(server.args, null, 2) : '',
url: server?.url || '', env_vars: server?.env_vars ? JSON.stringify(server.env_vars, null, 2) : '',
headers: server?.headers ? JSON.stringify(server.headers, null, 2) : '', url: server?.url || '',
}); headers: server?.headers ? JSON.stringify(server.headers, null, 2) : '',
});
const handleChange = (field: string, value: string) => {
setForm(prev => ({ ...prev, [field]: value })); const handleChange = (field: string, value: string) => {
}; setForm(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); const handleSubmit = (e: React.FormEvent) => {
const data: any = { e.preventDefault();
name: form.name, const data: any = {
description: form.description, name: form.name,
transport_type: form.transport_type, description: form.description,
}; transport_type: form.transport_type,
};
if (form.transport_type === 'stdio') {
data.command = form.command; if (form.transport_type === 'stdio') {
try { data.args = form.args ? JSON.parse(form.args) : {}; } catch { data.args = {}; } data.command = form.command;
try { data.env_vars = form.env_vars ? JSON.parse(form.env_vars) : {}; } catch { data.env_vars = {}; } try { data.args = form.args ? JSON.parse(form.args) : {}; } catch { data.args = {}; }
} else { try { data.env_vars = form.env_vars ? JSON.parse(form.env_vars) : {}; } catch { data.env_vars = {}; }
data.url = form.url; } else {
try { data.headers = form.headers ? JSON.parse(form.headers) : {}; } catch { data.headers = {}; } data.url = form.url;
} try { data.headers = form.headers ? JSON.parse(form.headers) : {}; } catch { data.headers = {}; }
}
onSubmit(data);
}; onSubmit(data);
};
return (
<div style={styles.modalOverlay} onClick={onClose}> return (
<div style={styles.modal} onClick={e => e.stopPropagation()}> <div style={styles.modalOverlay} onClick={onClose}>
<div style={styles.modalTitle}>{server ? 'Edit MCP Server' : 'Add MCP Server'}</div> <div style={styles.modal} onClick={e => e.stopPropagation()}>
<form onSubmit={handleSubmit}> <div style={styles.modalTitle}>{server ? '编辑 MCP 服务' : '添加 MCP 服务'}</div>
<div style={styles.formGroup}> <form onSubmit={handleSubmit}>
<label style={styles.label}>Name *</label> <div style={styles.formGroup}>
<input style={styles.input} value={form.name} onChange={e => handleChange('name', e.target.value)} required placeholder="e.g., Filesystem Server" /> <label style={styles.label}>名称 *</label>
</div> <input style={styles.input} value={form.name} onChange={e => handleChange('name', e.target.value)} required placeholder="例如:文件系统服务" />
<div style={styles.formGroup}> </div>
<label style={styles.label}>Description</label> <div style={styles.formGroup}>
<textarea style={styles.textarea} value={form.description} onChange={e => handleChange('description', e.target.value)} placeholder="Optional description" /> <label style={styles.label}>描述</label>
</div> <textarea style={styles.textarea} value={form.description} onChange={e => handleChange('description', e.target.value)} placeholder="可选描述信息" />
<div style={styles.formGroup}> </div>
<label style={styles.label}>Transport Type</label> <div style={styles.formGroup}>
<select style={{ ...styles.input, width: '100%' }} value={form.transport_type} onChange={e => handleChange('transport_type', e.target.value)}> <label style={styles.label}>传输类型</label>
<option value="stdio">STDIO (command-based)</option> <select style={{ ...styles.input, width: '100%' }} value={form.transport_type} onChange={e => handleChange('transport_type', e.target.value)}>
<option value="sse">SSE (HTTP-based)</option> <option value="stdio">STDIO(命令行方式)</option>
</select> <option value="sse">SSE(HTTP 方式)</option>
</div> </select>
</div>
{form.transport_type === 'stdio' ? (
<> {form.transport_type === 'stdio' ? (
<div style={styles.formGroup}> <>
<label style={styles.label}>Command</label> <div style={styles.formGroup}>
<input style={styles.input} value={form.command} onChange={e => handleChange('command', e.target.value)} placeholder="e.g., npx @modelcontextprotocol/server-filesystem" /> <label style={styles.label}>命令</label>
</div> <input style={styles.input} value={form.command} onChange={e => handleChange('command', e.target.value)} placeholder="例如:npx @modelcontextprotocol/server-filesystem" />
<div style={styles.formGroup}> </div>
<label style={styles.label}>Args (JSON)</label> <div style={styles.formGroup}>
<textarea style={styles.textarea} value={form.args} onChange={e => handleChange('args', e.target.value)} placeholder='["/path/to/allowed"]' /> <label style={styles.label}>参数(JSON)</label>
</div> <textarea style={styles.textarea} value={form.args} onChange={e => handleChange('args', e.target.value)} placeholder='["/path/to/allowed"]' />
<div style={styles.formGroup}> </div>
<label style={styles.label}>Env Vars (JSON)</label> <div style={styles.formGroup}>
<textarea style={styles.textarea} value={form.env_vars} onChange={e => handleChange('env_vars', e.target.value)} placeholder='{"KEY": "value"}' /> <label style={styles.label}>环境变量(JSON)</label>
</div> <textarea style={styles.textarea} value={form.env_vars} onChange={e => handleChange('env_vars', e.target.value)} placeholder='{"KEY": "value"}' />
</> </div>
) : ( </>
<> ) : (
<div style={styles.formGroup}> <>
<label style={styles.label}>SSE URL</label> <div style={styles.formGroup}>
<input style={styles.input} value={form.url} onChange={e => handleChange('url', e.target.value)} placeholder="http://localhost:3001/sse" /> <label style={styles.label}>SSE 地址</label>
</div> <input style={styles.input} value={form.url} onChange={e => handleChange('url', e.target.value)} placeholder="http://localhost:3001/sse" />
<div style={styles.formGroup}> </div>
<label style={styles.label}>Headers (JSON)</label> <div style={styles.formGroup}>
<textarea style={styles.textarea} value={form.headers} onChange={e => handleChange('headers', e.target.value)} placeholder='{"Authorization": "Bearer ..."}' /> <label style={styles.label}>请求头(JSON)</label>
</div> <textarea style={styles.textarea} value={form.headers} onChange={e => handleChange('headers', e.target.value)} placeholder='{"Authorization": "Bearer ..."}' />
</> </div>
)} </>
)}
<div style={styles.modalActions}>
<button type="button" style={styles.cancelBtn} onClick={onClose}>Cancel</button> <div style={styles.modalActions}>
<button type="submit" style={styles.submitBtn}>{server ? 'Update' : 'Create'}</button> <button type="button" style={styles.cancelBtn} onClick={onClose}>取消</button>
</div> <button type="submit" style={styles.submitBtn}>{server ? '更新' : '创建'}</button>
</form> </div>
</div> </form>
</div> </div>
); </div>
} );
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff