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:
1 parent
d62848ee98
commit
27ba91ae5e
8 files changed
+1382
-83
No files matched your search
@@ -274,7 +274,7 @@ async def create_agent(
|
|||||||
publisher_name=admin_user.username,
|
publisher_name=admin_user.username,
|
||||||
description=req.description or "",
|
description=req.description or "",
|
||||||
icon=req.icon or "",
|
icon=req.icon or "",
|
||||||
price=req.price,
|
price=0.0, # 管理员导入/安装/开发的 Agent 默认免费
|
||||||
is_member_free=req.is_member_free,
|
is_member_free=req.is_member_free,
|
||||||
skill_content=json.dumps(config, ensure_ascii=False),
|
skill_content=json.dumps(config, ensure_ascii=False),
|
||||||
status="published",
|
status="published",
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -178,23 +178,38 @@ def upload_app(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
# 根据操作者角色决定默认发布状态与计费策略(需求:管理员/用户导入、AI安装、AI开发)
|
||||||
|
# - 管理员:官方来源、审核通过(已发布)、免费
|
||||||
|
# - 用户:用户来源、未发布待审核、是否收费由客户选择
|
||||||
|
is_admin = getattr(current_user, "role", "user") == "admin"
|
||||||
|
if is_admin:
|
||||||
|
app_source = "official"
|
||||||
|
app_status = "published"
|
||||||
|
app_price = 0.0
|
||||||
|
app_member_free = True
|
||||||
|
else:
|
||||||
|
app_source = req.source if req.source in ("official", "user") else "user"
|
||||||
|
app_status = "pending"
|
||||||
|
app_price = req.price if req.price else 0.0
|
||||||
|
app_member_free = req.is_member_free if req.is_member_free is not None else False
|
||||||
|
|
||||||
app = MarketApp(
|
app = MarketApp(
|
||||||
id=gen_uuid(),
|
id=gen_uuid(),
|
||||||
name=req.name,
|
name=req.name,
|
||||||
description=req.description,
|
description=req.description,
|
||||||
icon=req.icon,
|
icon=req.icon,
|
||||||
type=req.type if req.type in ("skill","agent","mcp") else "skill",
|
type=req.type if req.type in ("skill","agent","mcp") else "skill",
|
||||||
source=req.source if req.source in ("official","user") else "user",
|
source=app_source,
|
||||||
price=req.price,
|
price=app_price,
|
||||||
install_count=0,
|
install_count=0,
|
||||||
rating_avg=0,
|
rating_avg=0,
|
||||||
review_count=0,
|
review_count=0,
|
||||||
publisher_id=current_user.id,
|
publisher_id=current_user.id,
|
||||||
publisher_name=current_user.username,
|
publisher_name=current_user.username,
|
||||||
is_member_free=req.is_member_free if req.is_member_free is not None else False,
|
is_member_free=app_member_free,
|
||||||
join_promotion=req.join_promotion if req.join_promotion is not None else False,
|
join_promotion=req.join_promotion if req.join_promotion is not None else False,
|
||||||
agent_endpoint=req.agent_endpoint,
|
agent_endpoint=req.agent_endpoint,
|
||||||
status="published", # 用户创建的应用直接发布
|
status=app_status,
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
updated_at=datetime.now(),
|
updated_at=datetime.now(),
|
||||||
)
|
)
|
||||||
@@ -228,7 +243,7 @@ def upload_app(
|
|||||||
"id": app.id, "name": app.name, "status": app.status,
|
"id": app.id, "name": app.name, "status": app.status,
|
||||||
"created_at": str(app.created_at),
|
"created_at": str(app.created_at),
|
||||||
},
|
},
|
||||||
"message": "提交成功,等待审核",
|
"message": "发布成功" if is_admin else "提交成功,等待审核",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -326,7 +326,6 @@ const Agents: React.FC = () => {
|
|||||||
管理 AI Agent/Skill/MCP,支持配置、关联技能和模型
|
管理 AI Agent/Skill/MCP,支持配置、关联技能和模型
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="admin-btn primary" onClick={openCreate}>+ 创建 Agent</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ 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' },
|
||||||
@@ -22,7 +22,7 @@ const styles: Record<string, React.CSSProperties> = {
|
|||||||
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 },
|
||||||
@@ -37,16 +37,16 @@ const styles: Record<string, React.CSSProperties> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
@@ -108,7 +108,7 @@ export default function AdminMcp() {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -130,25 +130,25 @@ export default function AdminMcp() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,12 +156,12 @@ export default function AdminMcp() {
|
|||||||
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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -169,32 +169,33 @@ export default function AdminMcp() {
|
|||||||
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 labels: Record<string, string> = { connected: '已连接', disconnected: '未连接', error: '错误' };
|
||||||
const colors: Record<string, { bg: string; color: string }> = {
|
const colors: Record<string, { bg: string; color: string }> = {
|
||||||
connected: { bg: '#dcfce7', color: '#166534' },
|
connected: { bg: '#dcfce7', color: '#166534' },
|
||||||
disconnected: { bg: '#f3f4f6', color: '#6b7280' },
|
disconnected: { bg: '#f3f4f6', color: '#6b7280' },
|
||||||
error: { bg: '#fee2e2', color: '#991b1b' },
|
error: { bg: '#fee2e2', color: '#991b1b' },
|
||||||
};
|
};
|
||||||
const c = colors[status] || colors.disconnected;
|
const c = colors[status] || colors.disconnected;
|
||||||
return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{status}</span>;
|
return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{labels[status] || status}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTransportBadge = (type: string) => {
|
const getTransportBadge = (type: string) => {
|
||||||
const c = type === 'sse' ? { bg: '#e0e7ff', color: '#3730a3' } : { bg: '#fef3c7', color: '#92400e' };
|
const c = type === 'sse' ? { bg: '#e0e7ff', color: '#3730a3' } : { bg: '#fef3c7', color: '#92400e' };
|
||||||
return <span style={{ ...styles.badge, backgroundColor: c.bg, color: c.color }}>{type.toUpperCase()}</span>;
|
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);
|
||||||
@@ -208,25 +209,25 @@ export default function AdminMcp() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={styles.header}>
|
<div style={styles.header}>
|
||||||
<h1 style={styles.title}>MCP Servers</h1>
|
<h1 style={styles.title}>MCP 服务管理</h1>
|
||||||
<button style={styles.addBtn} onClick={handleCreate}>+ Add Server</button>
|
{/* 手动添加服务入口已移除:MCP 仅支持通过 URL 导入(见应用市场「导入」)或由 AI 聊天自动部署安装 */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.statsRow}>
|
<div style={styles.statsRow}>
|
||||||
<div style={styles.statCard}>
|
<div style={styles.statCard}>
|
||||||
<div style={styles.statLabel}>Total Servers</div>
|
<div style={styles.statLabel}>服务总数</div>
|
||||||
<div style={styles.statValue}>{stats.total_servers || 0}</div>
|
<div style={styles.statValue}>{stats.total_servers || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.statCard}>
|
<div style={styles.statCard}>
|
||||||
<div style={styles.statLabel}>Enabled</div>
|
<div style={styles.statLabel}>已启用</div>
|
||||||
<div style={styles.statValue}>{stats.enabled_servers || 0}</div>
|
<div style={styles.statValue}>{stats.enabled_servers || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.statCard}>
|
<div style={styles.statCard}>
|
||||||
<div style={styles.statLabel}>Connected</div>
|
<div style={styles.statLabel}>已连接</div>
|
||||||
<div style={{ ...styles.statValue, color: '#10b981' }}>{stats.connected_servers || 0}</div>
|
<div style={{ ...styles.statValue, color: '#10b981' }}>{stats.connected_servers || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.statCard}>
|
<div style={styles.statCard}>
|
||||||
<div style={styles.statLabel}>By Transport</div>
|
<div style={styles.statLabel}>传输方式分布</div>
|
||||||
<div style={{ fontSize: 13, color: '#374151' }}>
|
<div style={{ fontSize: 13, color: '#374151' }}>
|
||||||
{Object.entries(stats.by_transport || {}).map(([k, v]) => (
|
{Object.entries(stats.by_transport || {}).map(([k, v]) => (
|
||||||
<span key={k} style={{ marginRight: 12 }}>{k}: {v as number}</span>
|
<span key={k} style={{ marginRight: 12 }}>{k}: {v as number}</span>
|
||||||
@@ -238,7 +239,7 @@ export default function AdminMcp() {
|
|||||||
<div style={styles.toolbar}>
|
<div style={styles.toolbar}>
|
||||||
<input
|
<input
|
||||||
style={styles.searchInput}
|
style={styles.searchInput}
|
||||||
placeholder="Search servers..."
|
placeholder="搜索服务..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||||||
/>
|
/>
|
||||||
@@ -253,20 +254,20 @@ export default function AdminMcp() {
|
|||||||
<table style={styles.table}>
|
<table style={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={styles.th}>Name</th>
|
<th style={styles.th}>名称</th>
|
||||||
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40 }}>Loading...</td></tr>
|
<tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40 }}>加载中...</td></tr>
|
||||||
) : servers.length === 0 ? (
|
) : servers.length === 0 ? (
|
||||||
<tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40, color: '#9ca3af' }}>No MCP servers found</td></tr>
|
<tr><td colSpan={7} style={{ ...styles.td, textAlign: 'center', padding: 40, color: '#9ca3af' }}>未找到 MCP 服务</td></tr>
|
||||||
) : servers.map(s => (
|
) : servers.map(s => (
|
||||||
<tr key={s.id}>
|
<tr key={s.id}>
|
||||||
<td style={styles.td}>
|
<td style={styles.td}>
|
||||||
@@ -276,19 +277,19 @@ export default function AdminMcp() {
|
|||||||
<td style={styles.td}>{getTransportBadge(s.transport_type)}</td>
|
<td style={styles.td}>{getTransportBadge(s.transport_type)}</td>
|
||||||
<td style={styles.td}>{getStatusBadge(s.status)}</td>
|
<td style={styles.td}>{getStatusBadge(s.status)}</td>
|
||||||
<td style={styles.td}>{s.tools_count || '-'}</td>
|
<td style={styles.td}>{s.tools_count || '-'}</td>
|
||||||
<td style={styles.td}>{s.last_connected_at ? new Date(s.last_connected_at).toLocaleString() : 'Never'}</td>
|
<td style={styles.td}>{s.last_connected_at ? new Date(s.last_connected_at).toLocaleString() : '从未'}</td>
|
||||||
<td style={styles.td}>
|
<td style={styles.td}>
|
||||||
<span style={{ ...styles.badge, backgroundColor: s.is_enabled ? '#dcfce7' : '#f3f4f6', color: s.is_enabled ? '#166534' : '#6b7280' }}>
|
<span style={{ ...styles.badge, backgroundColor: s.is_enabled ? '#dcfce7' : '#f3f4f6', color: s.is_enabled ? '#166534' : '#6b7280' }}>
|
||||||
{s.is_enabled ? 'Yes' : 'No'}
|
{s.is_enabled ? '是' : '否'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={styles.td}>
|
<td style={styles.td}>
|
||||||
<button style={styles.actionBtn} onClick={() => handleEdit(s)}>Edit</button>
|
<button style={styles.actionBtn} onClick={() => handleEdit(s)}>编辑</button>
|
||||||
<button style={{ ...styles.actionBtn, color: s.is_enabled ? '#ef4444' : '#10b981' }} onClick={() => handleToggle(s)}>
|
<button style={{ ...styles.actionBtn, color: s.is_enabled ? '#ef4444' : '#10b981' }} onClick={() => handleToggle(s)}>
|
||||||
{s.is_enabled ? 'Disable' : 'Enable'}
|
{s.is_enabled ? '禁用' : '启用'}
|
||||||
</button>
|
</button>
|
||||||
<button style={{ ...styles.actionBtn, color: '#4f46e5' }} onClick={() => handleTest(s)}>Test</button>
|
<button style={{ ...styles.actionBtn, color: '#4f46e5' }} onClick={() => handleTest(s)}>测试</button>
|
||||||
<button style={{ ...styles.actionBtn, color: '#ef4444' }} onClick={() => handleDelete(s)}>Del</button>
|
<button style={{ ...styles.actionBtn, color: '#ef4444' }} onClick={() => handleDelete(s)}>删除</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -297,9 +298,9 @@ export default function AdminMcp() {
|
|||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div style={styles.pagination}>
|
<div style={styles.pagination}>
|
||||||
<button style={styles.pageBtn} disabled={page <= 1} onClick={() => setPage(p => p - 1)}>Prev</button>
|
<button style={styles.pageBtn} disabled={page <= 1} onClick={() => setPage(p => p - 1)}>上一页</button>
|
||||||
<span style={{ padding: '8px 12px', fontSize: 14 }}>Page {page} / {totalPages}</span>
|
<span style={{ padding: '8px 12px', fontSize: 14 }}>第 {page} / {totalPages} 页</span>
|
||||||
<button style={styles.pageBtn} disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>Next</button>
|
<button style={styles.pageBtn} disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>下一页</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -347,55 +348,55 @@ function McpModal({ server, onSubmit, onClose }: { server: MCPServer | null; onS
|
|||||||
return (
|
return (
|
||||||
<div style={styles.modalOverlay} onClick={onClose}>
|
<div style={styles.modalOverlay} onClick={onClose}>
|
||||||
<div style={styles.modal} onClick={e => e.stopPropagation()}>
|
<div style={styles.modal} onClick={e => e.stopPropagation()}>
|
||||||
<div style={styles.modalTitle}>{server ? 'Edit MCP Server' : 'Add MCP Server'}</div>
|
<div style={styles.modalTitle}>{server ? '编辑 MCP 服务' : '添加 MCP 服务'}</div>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Name *</label>
|
<label style={styles.label}>名称 *</label>
|
||||||
<input style={styles.input} value={form.name} onChange={e => handleChange('name', e.target.value)} required placeholder="e.g., Filesystem Server" />
|
<input style={styles.input} value={form.name} onChange={e => handleChange('name', e.target.value)} required placeholder="例如:文件系统服务" />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Description</label>
|
<label style={styles.label}>描述</label>
|
||||||
<textarea style={styles.textarea} value={form.description} onChange={e => handleChange('description', e.target.value)} placeholder="Optional description" />
|
<textarea style={styles.textarea} value={form.description} onChange={e => handleChange('description', e.target.value)} placeholder="可选描述信息" />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Transport Type</label>
|
<label style={styles.label}>传输类型</label>
|
||||||
<select style={{ ...styles.input, width: '100%' }} value={form.transport_type} onChange={e => handleChange('transport_type', e.target.value)}>
|
<select style={{ ...styles.input, width: '100%' }} value={form.transport_type} onChange={e => handleChange('transport_type', e.target.value)}>
|
||||||
<option value="stdio">STDIO (command-based)</option>
|
<option value="stdio">STDIO(命令行方式)</option>
|
||||||
<option value="sse">SSE (HTTP-based)</option>
|
<option value="sse">SSE(HTTP 方式)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{form.transport_type === 'stdio' ? (
|
{form.transport_type === 'stdio' ? (
|
||||||
<>
|
<>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Command</label>
|
<label style={styles.label}>命令</label>
|
||||||
<input style={styles.input} value={form.command} onChange={e => handleChange('command', e.target.value)} placeholder="e.g., npx @modelcontextprotocol/server-filesystem" />
|
<input style={styles.input} value={form.command} onChange={e => handleChange('command', e.target.value)} placeholder="例如:npx @modelcontextprotocol/server-filesystem" />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Args (JSON)</label>
|
<label style={styles.label}>参数(JSON)</label>
|
||||||
<textarea style={styles.textarea} value={form.args} onChange={e => handleChange('args', e.target.value)} placeholder='["/path/to/allowed"]' />
|
<textarea style={styles.textarea} value={form.args} onChange={e => handleChange('args', e.target.value)} placeholder='["/path/to/allowed"]' />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Env Vars (JSON)</label>
|
<label style={styles.label}>环境变量(JSON)</label>
|
||||||
<textarea style={styles.textarea} value={form.env_vars} onChange={e => handleChange('env_vars', e.target.value)} placeholder='{"KEY": "value"}' />
|
<textarea style={styles.textarea} value={form.env_vars} onChange={e => handleChange('env_vars', e.target.value)} placeholder='{"KEY": "value"}' />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>SSE URL</label>
|
<label style={styles.label}>SSE 地址</label>
|
||||||
<input style={styles.input} value={form.url} onChange={e => handleChange('url', e.target.value)} placeholder="http://localhost:3001/sse" />
|
<input style={styles.input} value={form.url} onChange={e => handleChange('url', e.target.value)} placeholder="http://localhost:3001/sse" />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.formGroup}>
|
<div style={styles.formGroup}>
|
||||||
<label style={styles.label}>Headers (JSON)</label>
|
<label style={styles.label}>请求头(JSON)</label>
|
||||||
<textarea style={styles.textarea} value={form.headers} onChange={e => handleChange('headers', e.target.value)} placeholder='{"Authorization": "Bearer ..."}' />
|
<textarea style={styles.textarea} value={form.headers} onChange={e => handleChange('headers', e.target.value)} placeholder='{"Authorization": "Bearer ..."}' />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={styles.modalActions}>
|
<div style={styles.modalActions}>
|
||||||
<button type="button" style={styles.cancelBtn} onClick={onClose}>Cancel</button>
|
<button type="button" style={styles.cancelBtn} onClick={onClose}>取消</button>
|
||||||
<button type="submit" style={styles.submitBtn}>{server ? 'Update' : 'Create'}</button>
|
<button type="submit" style={styles.submitBtn}>{server ? '更新' : '创建'}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -334,7 +334,6 @@ const Skills: React.FC = () => {
|
|||||||
管理全平台 AI 技能,支持版本历史与市场发布
|
管理全平台 AI 技能,支持版本历史与市场发布
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="admin-btn primary" onClick={openCreate}>+ 创建技能</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@@ -664,4 +663,3 @@ const Skills: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default Skills;
|
export default Skills;
|
||||||
|
|
||||||
@@ -983,6 +983,37 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 导入定价字段 */
|
||||||
|
.hub-price-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin: var(--space-md) 0;
|
||||||
|
padding: var(--space-md);
|
||||||
|
background: var(--color-bg-subtle, #F8F9FC);
|
||||||
|
border: 1px solid var(--color-border, #E5E7EB);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.hub-price-input {
|
||||||
|
max-width: 200px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--color-border, #D1D5DB);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--color-text, #1A1F36);
|
||||||
|
}
|
||||||
|
.hub-price-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary, #6C63FF);
|
||||||
|
box-shadow: 0 0 0 2px rgba(108, 99, 255, 0.15);
|
||||||
|
}
|
||||||
|
.hub-price-hint {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-muted, #6B7280);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.hub-platforms {
|
.hub-platforms {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ const skillsHubPlatforms = [
|
|||||||
|
|
||||||
const mcpHubPlatforms = [
|
const mcpHubPlatforms = [
|
||||||
{ id: 'modelscope-mcp', name: 'ModelScope MCP', url: 'https://modelscope.cn/mcp' },
|
{ id: 'modelscope-mcp', name: 'ModelScope MCP', url: 'https://modelscope.cn/mcp' },
|
||||||
|
{ id: 'mcp.so', name: 'MCP.so', url: 'https://mcp.so' },
|
||||||
|
{ id: 'mcpmarket', name: 'MCPMarket', url: 'https://mcpmarket.cn' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ===== Agent External Platforms (修改4) =====
|
// ===== Agent External Platforms (修改4) =====
|
||||||
@@ -868,12 +870,13 @@ interface HubImportModalProps {
|
|||||||
appType: 'skill' | 'mcp';
|
appType: 'skill' | 'mcp';
|
||||||
platforms: typeof skillsHubPlatforms;
|
platforms: typeof skillsHubPlatforms;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onImport: (platformName: string, url: string, skillName: string) => void;
|
onImport: (platformName: string, url: string, skillName: string, price?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onClose, onImport }) => {
|
const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onClose, onImport }) => {
|
||||||
const [importUrl, setImportUrl] = useState<Record<string, string>>({});
|
const [importUrl, setImportUrl] = useState<Record<string, string>>({});
|
||||||
const [importingId, setImportingId] = useState<string | null>(null);
|
const [importingId, setImportingId] = useState<string | null>(null);
|
||||||
|
const [importPrice, setImportPrice] = useState<number>(0);
|
||||||
const [importStatus, setImportStatus] = useState<'idle' | 'verifying' | 'checking_format' | 'loading' | 'deploying' | 'published' | 'error'>('idle');
|
const [importStatus, setImportStatus] = useState<'idle' | 'verifying' | 'checking_format' | 'loading' | 'deploying' | 'published' | 'error'>('idle');
|
||||||
const [statusMsg, setStatusMsg] = useState('');
|
const [statusMsg, setStatusMsg] = useState('');
|
||||||
|
|
||||||
@@ -925,9 +928,9 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setImportStatus(step.status);
|
setImportStatus(step.status);
|
||||||
setStatusMsg(step.msg);
|
setStatusMsg(step.msg);
|
||||||
if (step.status === 'published') {
|
if (step.status === 'published') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onImport(platformId, appType === 'mcp' ? hubCode : url, extractSkillName(url, platformId));
|
onImport(platformId, appType === 'mcp' ? hubCode : url, extractSkillName(url, platformId), importPrice);
|
||||||
setImportingId(null);
|
setImportingId(null);
|
||||||
setImportStatus('idle');
|
setImportStatus('idle');
|
||||||
setImportUrl(prev => ({ ...prev, [platformId]: '' }));
|
setImportUrl(prev => ({ ...prev, [platformId]: '' }));
|
||||||
@@ -955,7 +958,21 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
|||||||
<div className="hub-step-arrow"/>
|
<div className="hub-step-arrow"/>
|
||||||
<div className="hub-step"><span className="hub-step-num">3</span> {appType === 'skill' ? '粘贴链接至输入框并点击导入' : '粘贴代码到编辑器,通过AI验证后点击导入'}</div>
|
<div className="hub-step"><span className="hub-step-num">3</span> {appType === 'skill' ? '粘贴链接至输入框并点击导入' : '粘贴代码到编辑器,通过AI验证后点击导入'}</div>
|
||||||
</div>
|
</div>
|
||||||
{appType === 'skill' && <div className="hub-free-note">通过以下平台导入的Skills默认为免费。</div>}
|
<div className="hub-price-section">
|
||||||
|
<label className="form-label">定价(元)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
className="hub-price-input"
|
||||||
|
value={importPrice}
|
||||||
|
onChange={e => setImportPrice(parseFloat(e.target.value) || 0)}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<span className="hub-price-hint">
|
||||||
|
用户导入的{appType === 'skill' ? 'Skills' : 'MCP'}:是否收费由您决定(0 为免费)。管理员导入默认免费且审核通过。
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* MCP Hub Import: Code Editor + AI验证 */}
|
{/* MCP Hub Import: Code Editor + AI验证 */}
|
||||||
{appType === 'mcp' && (
|
{appType === 'mcp' && (
|
||||||
@@ -1093,13 +1110,14 @@ const mcpClient = new MCPClient({
|
|||||||
// ===== Agent Platform Connect Modal (修改4) =====
|
// ===== Agent Platform Connect Modal (修改4) =====
|
||||||
interface AgentConnectModalProps {
|
interface AgentConnectModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConnect: (platformId: string, config: Record<string, string>) => void;
|
onConnect: (platformId: string, config: Record<string, string>, price?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnect }) => {
|
const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnect }) => {
|
||||||
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||||
const [apiKey, setApiKey] = useState('');
|
const [apiKey, setApiKey] = useState('');
|
||||||
const [endpoint, setEndpoint] = useState('');
|
const [endpoint, setEndpoint] = useState('');
|
||||||
|
const [price, setPrice] = useState<number>(0);
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
const [connected, setConnected] = useState<string | null>(null);
|
const [connected, setConnected] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -1109,7 +1127,7 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
setConnected(selectedPlatform);
|
setConnected(selectedPlatform);
|
||||||
onConnect(selectedPlatform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() });
|
onConnect(selectedPlatform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1177,6 +1195,19 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
|
|||||||
onChange={e => setEndpoint(e.target.value)}
|
onChange={e => setEndpoint(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-section">
|
||||||
|
<label className="form-label">定价(元)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="0 为免费"
|
||||||
|
value={price}
|
||||||
|
onChange={e => setPrice(parseFloat(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
<span className="hub-price-hint">用户对接的Agent:是否收费由您决定;管理员导入默认免费且审核通过。</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className="form-submit-btn"
|
className="form-submit-btn"
|
||||||
onClick={handleConnect}
|
onClick={handleConnect}
|
||||||
@@ -1286,14 +1317,14 @@ const MarketPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle hub import - 调用后端 API 创建应用
|
// Handle hub import - 调用后端 API 创建应用
|
||||||
const handleHubImport = async (platformName: string, url: string, skillName: string) => {
|
const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number) => {
|
||||||
try {
|
try {
|
||||||
await marketAPI.createApp({
|
await marketAPI.createApp({
|
||||||
name: skillName,
|
name: skillName,
|
||||||
type: showHubImport || 'skill',
|
type: showHubImport || 'skill',
|
||||||
description: `从 ${platformName} 导入的${showHubImport === 'skill' ? 'Skills工具' : 'MCP应用'} (来源: ${url})`,
|
description: `从 ${platformName} 导入的${showHubImport === 'skill' ? 'Skills工具' : 'MCP应用'} (来源: ${url})`,
|
||||||
price: 0,
|
price: price || 0,
|
||||||
is_member_free: true,
|
is_member_free: (price || 0) === 0,
|
||||||
join_promotion: false,
|
join_promotion: false,
|
||||||
source: 'user',
|
source: 'user',
|
||||||
});
|
});
|
||||||
@@ -1306,7 +1337,7 @@ const MarketPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle agent connect - 调用后端 API 创建应用
|
// Handle agent connect - 调用后端 API 创建应用
|
||||||
const handleAgentConnect = async (platformId: string, config: Record<string, string>) => {
|
const handleAgentConnect = async (platformId: string, config: Record<string, string>, price?: number) => {
|
||||||
const platform = agentPlatforms.find(p => p.id === platformId);
|
const platform = agentPlatforms.find(p => p.id === platformId);
|
||||||
if (!platform) return;
|
if (!platform) return;
|
||||||
try {
|
try {
|
||||||
@@ -1314,8 +1345,8 @@ const MarketPage: React.FC = () => {
|
|||||||
name: `${platform.name} Agent`,
|
name: `${platform.name} Agent`,
|
||||||
type: 'agent',
|
type: 'agent',
|
||||||
description: `从 ${platform.name} 对接导入的智能Agent`,
|
description: `从 ${platform.name} 对接导入的智能Agent`,
|
||||||
price: 0,
|
price: price || 0,
|
||||||
is_member_free: true,
|
is_member_free: (price || 0) === 0,
|
||||||
join_promotion: false,
|
join_promotion: false,
|
||||||
source: 'user',
|
source: 'user',
|
||||||
agent_endpoint: config.endpoint || '',
|
agent_endpoint: config.endpoint || '',
|
||||||
|
|||||||
Reference in new issue
Block a user