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
+1382 -83

No files matched your search

+1 -1
View File
@@ -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
+20 -5
View File
@@ -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 "提交成功,等待审核",
}
-1
View File
@@ -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 */}
+63 -62
View File
@@ -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>
-2
View File
@@ -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;
+31
View File
@@ -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;
+43 -12
View File
@@ -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 || '',