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,
|
||||
description=req.description or "",
|
||||
icon=req.icon or "",
|
||||
price=req.price,
|
||||
price=0.0, # 管理员导入/安装/开发的 Agent 默认免费
|
||||
is_member_free=req.is_member_free,
|
||||
skill_content=json.dumps(config, ensure_ascii=False),
|
||||
status="published",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -178,23 +178,38 @@ def upload_app(
|
||||
db: Session = Depends(get_db),
|
||||
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(
|
||||
id=gen_uuid(),
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
icon=req.icon,
|
||||
type=req.type if req.type in ("skill","agent","mcp") else "skill",
|
||||
source=req.source if req.source in ("official","user") else "user",
|
||||
price=req.price,
|
||||
source=app_source,
|
||||
price=app_price,
|
||||
install_count=0,
|
||||
rating_avg=0,
|
||||
review_count=0,
|
||||
publisher_id=current_user.id,
|
||||
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,
|
||||
agent_endpoint=req.agent_endpoint,
|
||||
status="published", # 用户创建的应用直接发布
|
||||
status=app_status,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
@@ -228,7 +243,7 @@ def upload_app(
|
||||
"id": app.id, "name": app.name, "status": app.status,
|
||||
"created_at": str(app.created_at),
|
||||
},
|
||||
"message": "提交成功,等待审核",
|
||||
"message": "发布成功" if is_admin else "提交成功,等待审核",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -326,7 +326,6 @@ const Agents: React.FC = () => {
|
||||
管理 AI Agent/Skill/MCP,支持配置、关联技能和模型
|
||||
</p>
|
||||
</div>
|
||||
<button className="admin-btn primary" onClick={openCreate}>+ 创建 Agent</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { adminAPI } from '@/api';
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
container: { padding: 24, backgroundColor: '#f5f7fa', minHeight: '100vh' },
|
||||
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 },
|
||||
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)' },
|
||||
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 },
|
||||
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' },
|
||||
@@ -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 },
|
||||
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)' },
|
||||
modalTitle: { fontSize: 18, fontWeight: 700, marginBottom: 24, color: '#1a1a2e' },
|
||||
modalTitle: { fontSize: 18, fontWeight: 700, marginBottom: 24, color: '#1A1F36' },
|
||||
formGroup: { marginBottom: 16 },
|
||||
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 },
|
||||
@@ -37,16 +37,16 @@ const styles: Record<string, React.CSSProperties> = {
|
||||
};
|
||||
|
||||
const TRANSPORT_TYPES = [
|
||||
{ value: '', label: 'All Types' },
|
||||
{ value: '', label: '全部类型' },
|
||||
{ value: 'stdio', label: 'STDIO' },
|
||||
{ value: 'sse', label: 'SSE' },
|
||||
];
|
||||
|
||||
const STATUS_TYPES = [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'connected', label: 'Connected' },
|
||||
{ value: 'disconnected', label: 'Disconnected' },
|
||||
{ value: 'error', label: 'Error' },
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'connected', label: '已连接' },
|
||||
{ value: 'disconnected', label: '未连接' },
|
||||
{ value: 'error', label: '错误' },
|
||||
];
|
||||
|
||||
interface MCPServer {
|
||||
@@ -108,7 +108,7 @@ export default function AdminMcp() {
|
||||
setTotal(result.data.total || 0);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Failed to load servers', 'error');
|
||||
showToast('加载服务失败', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -130,25 +130,25 @@ export default function AdminMcp() {
|
||||
};
|
||||
|
||||
const handleDelete = async (server: MCPServer) => {
|
||||
if (!confirm(`Delete MCP server "${server.name}"?`)) return;
|
||||
if (!confirm(`确认删除 MCP 服务 "${server.name}"?`)) return;
|
||||
try {
|
||||
await adminAPI.deleteAdminMcpServer(server.id);
|
||||
showToast('Server deleted');
|
||||
showToast('服务已删除');
|
||||
fetchServers();
|
||||
fetchStats();
|
||||
} catch (e) {
|
||||
showToast('Delete failed', 'error');
|
||||
showToast('删除失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async (server: MCPServer) => {
|
||||
try {
|
||||
await adminAPI.toggleAdminMcpServer(server.id);
|
||||
showToast(server.is_enabled ? 'Disabled' : 'Enabled');
|
||||
showToast(server.is_enabled ? '已禁用' : '已启用');
|
||||
fetchServers();
|
||||
fetchStats();
|
||||
} catch (e) {
|
||||
showToast('Toggle failed', 'error');
|
||||
showToast('切换失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,12 +156,12 @@ export default function AdminMcp() {
|
||||
try {
|
||||
const result = await adminAPI.testAdminMcpServer(server.id) as any;
|
||||
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();
|
||||
fetchStats();
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Test failed', 'error');
|
||||
showToast('测试失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -169,32 +169,33 @@ export default function AdminMcp() {
|
||||
try {
|
||||
if (editingServer) {
|
||||
await adminAPI.updateAdminMcpServer(editingServer.id, formData);
|
||||
showToast('Server updated');
|
||||
showToast('服务已更新');
|
||||
} else {
|
||||
await adminAPI.createAdminMcpServer(formData);
|
||||
showToast('Server created');
|
||||
showToast('服务已创建');
|
||||
}
|
||||
setShowModal(false);
|
||||
fetchServers();
|
||||
fetchStats();
|
||||
} catch (e: any) {
|
||||
showToast(e.message || 'Operation failed', 'error');
|
||||
showToast(e.message || '操作失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const labels: Record<string, string> = { connected: '已连接', disconnected: '未连接', error: '错误' };
|
||||
const colors: Record<string, { bg: string; color: string }> = {
|
||||
connected: { bg: '#dcfce7', color: '#166534' },
|
||||
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>;
|
||||
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' };
|
||||
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);
|
||||
@@ -208,25 +209,25 @@ export default function AdminMcp() {
|
||||
)}
|
||||
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>MCP Servers</h1>
|
||||
<button style={styles.addBtn} onClick={handleCreate}>+ Add Server</button>
|
||||
<h1 style={styles.title}>MCP 服务管理</h1>
|
||||
{/* 手动添加服务入口已移除:MCP 仅支持通过 URL 导入(见应用市场「导入」)或由 AI 聊天自动部署安装 */}
|
||||
</div>
|
||||
|
||||
<div style={styles.statsRow}>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statLabel}>By Transport</div>
|
||||
<div style={styles.statLabel}>传输方式分布</div>
|
||||
<div style={{ fontSize: 13, color: '#374151' }}>
|
||||
{Object.entries(stats.by_transport || {}).map(([k, v]) => (
|
||||
<span key={k} style={{ marginRight: 12 }}>{k}: {v as number}</span>
|
||||
@@ -238,7 +239,7 @@ export default function AdminMcp() {
|
||||
<div style={styles.toolbar}>
|
||||
<input
|
||||
style={styles.searchInput}
|
||||
placeholder="Search servers..."
|
||||
placeholder="搜索服务..."
|
||||
value={search}
|
||||
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||||
/>
|
||||
@@ -253,20 +254,20 @@ export default function AdminMcp() {
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={styles.th}>Name</th>
|
||||
<th style={styles.th}>Transport</th>
|
||||
<th style={styles.th}>Status</th>
|
||||
<th style={styles.th}>Tools</th>
|
||||
<th style={styles.th}>Last Connected</th>
|
||||
<th style={styles.th}>Enabled</th>
|
||||
<th style={styles.th}>Actions</th>
|
||||
<th style={styles.th}>名称</th>
|
||||
<th style={styles.th}>传输方式</th>
|
||||
<th style={styles.th}>状态</th>
|
||||
<th style={styles.th}>工具数</th>
|
||||
<th style={styles.th}>最后连接</th>
|
||||
<th style={styles.th}>启用</th>
|
||||
<th style={styles.th}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 ? (
|
||||
<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 => (
|
||||
<tr key={s.id}>
|
||||
<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}>{getStatusBadge(s.status)}</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}>
|
||||
<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>
|
||||
</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)}>
|
||||
{s.is_enabled ? 'Disable' : 'Enable'}
|
||||
{s.is_enabled ? '禁用' : '启用'}
|
||||
</button>
|
||||
<button style={{ ...styles.actionBtn, color: '#4f46e5' }} onClick={() => handleTest(s)}>Test</button>
|
||||
<button style={{ ...styles.actionBtn, color: '#ef4444' }} onClick={() => handleDelete(s)}>Del</button>
|
||||
<button style={{ ...styles.actionBtn, color: '#4f46e5' }} onClick={() => handleTest(s)}>测试</button>
|
||||
<button style={{ ...styles.actionBtn, color: '#ef4444' }} onClick={() => handleDelete(s)}>删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -297,9 +298,9 @@ export default function AdminMcp() {
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div style={styles.pagination}>
|
||||
<button style={styles.pageBtn} disabled={page <= 1} onClick={() => setPage(p => p - 1)}>Prev</button>
|
||||
<span style={{ padding: '8px 12px', fontSize: 14 }}>Page {page} / {totalPages}</span>
|
||||
<button style={styles.pageBtn} disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>Next</button>
|
||||
<button style={styles.pageBtn} disabled={page <= 1} onClick={() => setPage(p => p - 1)}>上一页</button>
|
||||
<span style={{ padding: '8px 12px', fontSize: 14 }}>第 {page} / {totalPages} 页</span>
|
||||
<button style={styles.pageBtn} disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>下一页</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -347,55 +348,55 @@ function McpModal({ server, onSubmit, onClose }: { server: MCPServer | null; onS
|
||||
return (
|
||||
<div style={styles.modalOverlay} onClick={onClose}>
|
||||
<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}>
|
||||
<div style={styles.formGroup}>
|
||||
<label style={styles.label}>Name *</label>
|
||||
<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>
|
||||
<input style={styles.input} value={form.name} onChange={e => handleChange('name', e.target.value)} required placeholder="例如:文件系统服务" />
|
||||
</div>
|
||||
<div style={styles.formGroup}>
|
||||
<label style={styles.label}>Description</label>
|
||||
<textarea style={styles.textarea} value={form.description} onChange={e => handleChange('description', e.target.value)} placeholder="Optional description" />
|
||||
<label style={styles.label}>描述</label>
|
||||
<textarea style={styles.textarea} value={form.description} onChange={e => handleChange('description', e.target.value)} placeholder="可选描述信息" />
|
||||
</div>
|
||||
<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)}>
|
||||
<option value="stdio">STDIO (command-based)</option>
|
||||
<option value="sse">SSE (HTTP-based)</option>
|
||||
<option value="stdio">STDIO(命令行方式)</option>
|
||||
<option value="sse">SSE(HTTP 方式)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{form.transport_type === 'stdio' ? (
|
||||
<>
|
||||
<div style={styles.formGroup}>
|
||||
<label style={styles.label}>Command</label>
|
||||
<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>
|
||||
<input style={styles.input} value={form.command} onChange={e => handleChange('command', e.target.value)} placeholder="例如:npx @modelcontextprotocol/server-filesystem" />
|
||||
</div>
|
||||
<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"]' />
|
||||
</div>
|
||||
<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"}' />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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" />
|
||||
</div>
|
||||
<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 ..."}' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={styles.modalActions}>
|
||||
<button type="button" style={styles.cancelBtn} onClick={onClose}>Cancel</button>
|
||||
<button type="submit" style={styles.submitBtn}>{server ? 'Update' : 'Create'}</button>
|
||||
<button type="button" style={styles.cancelBtn} onClick={onClose}>取消</button>
|
||||
<button type="submit" style={styles.submitBtn}>{server ? '更新' : '创建'}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -334,7 +334,6 @@ const Skills: React.FC = () => {
|
||||
管理全平台 AI 技能,支持版本历史与市场发布
|
||||
</p>
|
||||
</div>
|
||||
<button className="admin-btn primary" onClick={openCreate}>+ 创建技能</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -664,4 +663,3 @@ const Skills: React.FC = () => {
|
||||
};
|
||||
|
||||
export default Skills;
|
||||
|
||||
@@ -983,6 +983,37 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -193,6 +193,8 @@ const skillsHubPlatforms = [
|
||||
|
||||
const mcpHubPlatforms = [
|
||||
{ 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) =====
|
||||
@@ -868,12 +870,13 @@ interface HubImportModalProps {
|
||||
appType: 'skill' | 'mcp';
|
||||
platforms: typeof skillsHubPlatforms;
|
||||
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 [importUrl, setImportUrl] = useState<Record<string, string>>({});
|
||||
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 [statusMsg, setStatusMsg] = useState('');
|
||||
|
||||
@@ -925,9 +928,9 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
setTimeout(() => {
|
||||
setImportStatus(step.status);
|
||||
setStatusMsg(step.msg);
|
||||
if (step.status === 'published') {
|
||||
if (step.status === 'published') {
|
||||
setTimeout(() => {
|
||||
onImport(platformId, appType === 'mcp' ? hubCode : url, extractSkillName(url, platformId));
|
||||
onImport(platformId, appType === 'mcp' ? hubCode : url, extractSkillName(url, platformId), importPrice);
|
||||
setImportingId(null);
|
||||
setImportStatus('idle');
|
||||
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"><span className="hub-step-num">3</span> {appType === 'skill' ? '粘贴链接至输入框并点击导入' : '粘贴代码到编辑器,通过AI验证后点击导入'}</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验证 */}
|
||||
{appType === 'mcp' && (
|
||||
@@ -1093,13 +1110,14 @@ const mcpClient = new MCPClient({
|
||||
// ===== Agent Platform Connect Modal (修改4) =====
|
||||
interface AgentConnectModalProps {
|
||||
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 [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [endpoint, setEndpoint] = useState('');
|
||||
const [price, setPrice] = useState<number>(0);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [connected, setConnected] = useState<string | null>(null);
|
||||
|
||||
@@ -1109,7 +1127,7 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
|
||||
setTimeout(() => {
|
||||
setConnecting(false);
|
||||
setConnected(selectedPlatform);
|
||||
onConnect(selectedPlatform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() });
|
||||
onConnect(selectedPlatform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
@@ -1177,6 +1195,19 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
|
||||
onChange={e => setEndpoint(e.target.value)}
|
||||
/>
|
||||
</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
|
||||
className="form-submit-btn"
|
||||
onClick={handleConnect}
|
||||
@@ -1286,14 +1317,14 @@ const MarketPage: React.FC = () => {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
await marketAPI.createApp({
|
||||
name: skillName,
|
||||
type: showHubImport || 'skill',
|
||||
description: `从 ${platformName} 导入的${showHubImport === 'skill' ? 'Skills工具' : 'MCP应用'} (来源: ${url})`,
|
||||
price: 0,
|
||||
is_member_free: true,
|
||||
price: price || 0,
|
||||
is_member_free: (price || 0) === 0,
|
||||
join_promotion: false,
|
||||
source: 'user',
|
||||
});
|
||||
@@ -1306,7 +1337,7 @@ const MarketPage: React.FC = () => {
|
||||
};
|
||||
|
||||
// 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);
|
||||
if (!platform) return;
|
||||
try {
|
||||
@@ -1314,8 +1345,8 @@ const MarketPage: React.FC = () => {
|
||||
name: `${platform.name} Agent`,
|
||||
type: 'agent',
|
||||
description: `从 ${platform.name} 对接导入的智能Agent`,
|
||||
price: 0,
|
||||
is_member_free: true,
|
||||
price: price || 0,
|
||||
is_member_free: (price || 0) === 0,
|
||||
join_promotion: false,
|
||||
source: 'user',
|
||||
agent_endpoint: config.endpoint || '',
|
||||
|
||||
Reference in new issue
Block a user