1 parent
1633d5c33e
commit
88b9a232ce
3 files changed
+118
-542
No files matched your search
@@ -21,6 +21,7 @@ const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage'));
|
||||
const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage'));
|
||||
const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage'));
|
||||
const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage'));
|
||||
const AgentCanvas = React.lazy(() => import('@/pages/front/AgentCanvas'));
|
||||
const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard'));
|
||||
|
||||
const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard'));
|
||||
@@ -45,7 +46,6 @@ const Achievement = React.lazy(() => import('@/pages/admin/Achievement'));
|
||||
const Tenant = React.lazy(() => import('@/pages/admin/Tenant'));
|
||||
const Billing = React.lazy(() => import('@/pages/admin/Billing'));
|
||||
const Analytics = React.lazy(() => import('@/pages/admin/Analytics'));
|
||||
const Observability = React.lazy(() => import('@/pages/admin/Observability'));
|
||||
const AdminKnowledge = React.lazy(() => import('@/pages/admin/Knowledge'));
|
||||
const AdminWorkflow = React.lazy(() => import('@/pages/admin/Workflow'));
|
||||
const AdminStorage = React.lazy(() => import('@/pages/admin/Storage'));
|
||||
@@ -113,6 +113,7 @@ const App: React.FC = () => {
|
||||
{/* Front Routes (authenticated only) */}
|
||||
<Route path="/" element={<FrontLayout />}>
|
||||
<Route path="chat" element={<ChatPage />} />
|
||||
<Route path="agent-canvas" element={<AgentCanvas />} />
|
||||
<Route path="chat/:id" element={<ChatPage />} />
|
||||
<Route path="knowledge" element={<KnowledgePage />} />
|
||||
<Route path="market" element={<MarketPage />} />
|
||||
@@ -152,7 +153,6 @@ const App: React.FC = () => {
|
||||
<Route path="tenant" element={<Tenant />} />
|
||||
<Route path="billing" element={<Billing />} />
|
||||
<Route path="analytics" element={<Analytics />} />
|
||||
<Route path="observability" element={<Observability />} />
|
||||
<Route path="knowledge" element={<AdminKnowledge />} />
|
||||
<Route path="workflow" element={<AdminWorkflow />} />
|
||||
<Route path="storage" element={<AdminStorage />} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useBalance } from '@/hooks/useBalance';
|
||||
import './FrontLayout.css';
|
||||
|
||||
const navGroups = [
|
||||
@@ -100,10 +101,14 @@ const FrontLayout: React.FC = () => {
|
||||
<nav className="sidebar-nav">
|
||||
{!collapsed && balance !== null && (
|
||||
<div className="nav-item balance-item">
|
||||
<span className="nav-icon">$</span>
|
||||
<span className="nav-label">余额: $ {balance.toFixed(2)}</span>
|
||||
<span className="nav-icon">$</span>
|
||||
<span className="nav-label">余额: $ {balance.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<NavLink to="/agent-canvas" className={({ isActive }) => `nav-item ${isActive ? "active" : ""}`} title="Agent 编排">
|
||||
<span className="nav-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4m10-10h-4M6 12H2"/><path d="m4.93 4.93 2.83 2.83m8.48 8.48 2.83 2.83M4.93 19.07l2.83-2.83m8.48-8.48 2.83-2.83"/></svg></span>
|
||||
{!collapsed && <span className="nav-label">Agent 编排</span>}
|
||||
</NavLink>
|
||||
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
|
||||
<span className="nav-item-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { marketAPI } from '@/api';
|
||||
import type { MarketApp, AppType, AppReview } from '@/types';
|
||||
import './MarketPage.css';
|
||||
@@ -173,7 +173,7 @@ function renderAppIconLarge(app: MarketApp): React.ReactNode {
|
||||
const typeLabels: Record<AppType, string> = {
|
||||
skill: 'Skills工具',
|
||||
agent: '智能Agent',
|
||||
mcp: 'MCP应用',
|
||||
mcp: 'MCP协议应用',
|
||||
};
|
||||
|
||||
const typeIcons: Record<AppType, React.ReactNode> = {
|
||||
@@ -182,77 +182,6 @@ const typeIcons: Record<AppType, React.ReactNode> = {
|
||||
mcp: <McpIcon />,
|
||||
};
|
||||
|
||||
// ===== 业务场景分类(面向用户的技能/MCP 分类,与后端模型路由能力解耦) =====
|
||||
// 技能/MCP 场景分类(与后端 MARKET_SCENES 保持一致,元宝生成的岗位/职能视角,扁平展示)。
|
||||
const SCENES: { id: string; label: string; color: string }[] = [
|
||||
{ id: 'design', label: '🎨 做设计', color: '#EC4899' },
|
||||
{ id: 'frontend', label: '💻 写前端', color: '#3B82F6' },
|
||||
{ id: 'mobile', label: '📱 做移动端', color: '#06B6D4' },
|
||||
{ id: 'backend', label: '🧪 后端/全栈', color: '#8B5CF6' },
|
||||
{ id: 'qa', label: '🧪 测试质量', color: '#6366F1' },
|
||||
{ id: 'search', label: '🔍 搜索信息', color: '#0EA5E9' },
|
||||
{ id: 'docs', label: '✍️ 写文档', color: '#F59E0B' },
|
||||
{ id: 'marketing', label: '📢 推广获客', color: '#F43F5E' },
|
||||
{ id: 'workflow', label: '🔄 工作流', color: '#14B8A6' },
|
||||
{ id: 'secops', label: '🛡️ 安全运维', color: '#EF4444' },
|
||||
{ id: 'learning', label: '🎓 学习教学', color: '#10B981' },
|
||||
{ id: 'video', label: '🎬 视频多媒体', color: '#D946EF' },
|
||||
{ id: 'collab', label: '🏢 办公协作', color: '#64748B' },
|
||||
{ id: 'ai', label: '🛠️ AI能力', color: '#A855F7' },
|
||||
];
|
||||
|
||||
function CapabilityBadge({ capability }: { capability?: string | null }) {
|
||||
if (!capability) return null;
|
||||
const cap = SCENES.find(c => c.id === capability);
|
||||
if (!cap) return null;
|
||||
return (
|
||||
<span
|
||||
className="capability-badge"
|
||||
style={{ color: cap.color, background: cap.color + '1A', borderColor: cap.color + '40' }}
|
||||
title={`业务场景:${cap.label}`}
|
||||
>
|
||||
{cap.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Phase 4.1: 安检 / 绿标 / 版本 徽章 =====
|
||||
function SecurityBadge({ scan }: { scan?: any }) {
|
||||
if (!scan || !scan.status) return null;
|
||||
const map: Record<string, { label: string; color: string }> = {
|
||||
pass: { label: '安检通过', color: '#16a34a' },
|
||||
warn: { label: '安检注意', color: '#d97706' },
|
||||
fail: { label: '安检风险', color: '#dc2626' },
|
||||
};
|
||||
const m = map[scan.status] || map.warn;
|
||||
return (
|
||||
<span
|
||||
className="capability-badge"
|
||||
style={{ color: m.color, background: m.color + '1A', borderColor: m.color + '40' }}
|
||||
title={'安全扫描:' + (scan.findings ? scan.findings.length + ' 项发现' : scan.status)}
|
||||
>
|
||||
{m.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function VerifiedBadge() {
|
||||
return (
|
||||
<span
|
||||
className="capability-badge"
|
||||
style={{ color: '#16a34a', background: '#16a34a1A', borderColor: '#16a34a40' }}
|
||||
title="已通过官方安全安检(绿标)"
|
||||
>
|
||||
✓ 已认证
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionChip({ version }: { version?: string }) {
|
||||
if (!version) return null;
|
||||
return <span style={{ fontSize: 12, color: '#6b7280', marginLeft: 6 }}>v{version}</span>;
|
||||
}
|
||||
|
||||
// ===== Hub Platforms (修改3 & 修改5) =====
|
||||
const skillsHubPlatforms = [
|
||||
{ id: 'skills.sh', name: 'skills.sh', url: 'https://www.skills.sh' },
|
||||
@@ -337,9 +266,6 @@ const AppDetailModal: React.FC<AppDetailModalProps> = ({ app, onClose, onAppChan
|
||||
setInstalled(!!app.installed);
|
||||
}, [app.id, app.installed]);
|
||||
|
||||
// 官方导入并发布的技能默认全局已安装,前端展示为「官方内置」(见 #716)
|
||||
const isOfficialBuiltin = app.source === 'official' && app.status === 'published';
|
||||
|
||||
useEffect(() => {
|
||||
// 加载评论
|
||||
marketAPI.getReviews(app.id).then((res: any) => {
|
||||
@@ -425,10 +351,6 @@ const AppDetailModal: React.FC<AppDetailModalProps> = ({ app, onClose, onAppChan
|
||||
{typeIcons[app.type]}
|
||||
{typeLabels[app.type]}
|
||||
</span>
|
||||
<CapabilityBadge capability={app.capability} />
|
||||
<VersionChip version={app.version} />
|
||||
{app.verified && <VerifiedBadge />}
|
||||
<SecurityBadge scan={app.securityScan} />
|
||||
<span className="modal-app-publisher">
|
||||
<UserIcon />
|
||||
{app.publisherName}
|
||||
@@ -466,9 +388,7 @@ const AppDetailModal: React.FC<AppDetailModalProps> = ({ app, onClose, onAppChan
|
||||
<span className="modal-price">{app.price} 元/月</span>
|
||||
)}
|
||||
</div>
|
||||
{isOfficialBuiltin ? (
|
||||
<span className="modal-official-badge">官方内置</span>
|
||||
) : installed ? (
|
||||
{installed ? (
|
||||
<button className="modal-install-btn uninstall" onClick={handleUninstall} disabled={uninstalling}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
@@ -588,10 +508,6 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
const [mcpCode, setMcpCode] = useState('');
|
||||
const [mcpCodeVerifyResult, setMcpCodeVerifyResult] = useState<'idle' | 'checking' | 'pass' | 'fail'>('idle');
|
||||
const [mcpCodeVerifyMsg, setMcpCodeVerifyMsg] = useState('');
|
||||
// 能力分类:AI验证后自动识别,可手动修改;空字符串表示交由后端 AI 自动归类
|
||||
const [capability, setCapability] = useState('');
|
||||
// Phase 4.1: 依赖声明(每行一个依赖名)
|
||||
const [dependencies, setDependencies] = useState('');
|
||||
|
||||
const handleCheckFormat = () => {
|
||||
const content = skillsFile || skillsContent;
|
||||
@@ -605,7 +521,6 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
if (hasMinLength && (hasMarkdownHeader || hasValidStructure)) {
|
||||
setFormatCheckResult('pass');
|
||||
setFormatCheckMsg('格式检测通过:符合Skills标准规范(工具定义 + 使用说明 + 参数配置)');
|
||||
detectCap(content);
|
||||
} else {
|
||||
setFormatCheckResult('fail');
|
||||
setFormatCheckMsg('格式不符合Skills标准:请确保包含工具定义、功能描述、使用说明和参数配置');
|
||||
@@ -623,7 +538,6 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
if (hasMinLength && hasKeywords) {
|
||||
setMcpCodeVerifyResult('pass');
|
||||
setMcpCodeVerifyMsg('AI验证通过:代码包含必要的传输协议和端点配置');
|
||||
detectCap(mcpCode);
|
||||
} else {
|
||||
setMcpCodeVerifyResult('fail');
|
||||
setMcpCodeVerifyMsg('AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性');
|
||||
@@ -643,18 +557,6 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
}
|
||||
};
|
||||
|
||||
// 调用后端 detect-capability,AI 验证阶段自动识别能力分类并预填
|
||||
const detectCap = (content: string) => {
|
||||
if (!name.trim()) return;
|
||||
marketAPI.detectCapability({ name: name.trim(), description: description.trim(), skill_content: content })
|
||||
.then((res: any) => {
|
||||
if (res && res.code === 0 && res.data && res.data.capability) {
|
||||
setCapability(res.data.capability);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!name.trim() || !description.trim()) return;
|
||||
// Skills requires format check pass
|
||||
@@ -668,7 +570,6 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
type: appType,
|
||||
capability: capability || undefined,
|
||||
description: description.trim(),
|
||||
price: isPaid ? parseFloat(price) || 0 : 0,
|
||||
isMemberFree: !isPaid,
|
||||
@@ -678,16 +579,7 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
publisherName: 'demo_user',
|
||||
status: 'published' as const,
|
||||
icon: appType === 'skill' ? 'S' : appType === 'agent' ? 'A' : 'M',
|
||||
// 关键修复:把粘贴的 MCP 传输代码随提交一起发给后端,否则后端拿不到代码、URL 为空、永远连不上
|
||||
mcpCode: appType === 'mcp' ? mcpCode.trim() : undefined,
|
||||
mcpTransport: appType === 'mcp' ? mcpTransport : undefined,
|
||||
// 关键修复:把粘贴/上传的 SKILL 内容随提交一起发给后端,否则 skill_content 为空、技能在对话里失效
|
||||
skillContent: appType === 'skill' ? (skillsContent.trim() || skillsFile || '') : undefined,
|
||||
// Phase 4.1: 依赖声明(每行一个 → 数组)
|
||||
dependencies: dependencies.trim()
|
||||
? dependencies.split('\n').map((s: string) => s.trim()).filter(Boolean).map((n: string) => ({ name: n }))
|
||||
: undefined,
|
||||
} as any);
|
||||
});
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
};
|
||||
@@ -838,31 +730,6 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Phase 4.1: 依赖声明 */}
|
||||
<div className="form-section">
|
||||
<label className="form-label">依赖声明 <span className="cap-hint">(可选,每行一个依赖)</span></label>
|
||||
<textarea
|
||||
className="form-textarea"
|
||||
placeholder={'例如:\nrequests\nopenai\nlangchain'}
|
||||
value={dependencies}
|
||||
onChange={e => setDependencies(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 能力分类(AI验证后自动识别,可手动修改) */}
|
||||
{appType !== 'agent' && (
|
||||
<div className="form-section">
|
||||
<label className="form-label">能力分类 <span className="cap-hint">(AI验证后自动识别,可修改)</span></label>
|
||||
<select className="form-input" value={capability} onChange={e => setCapability(e.target.value)}>
|
||||
<option value="">未指定(由AI自动归类)</option>
|
||||
{SCENES.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MCP-specific: Transport Type (修改5) */}
|
||||
{appType === 'mcp' && (
|
||||
<div className="form-section">
|
||||
@@ -892,17 +759,13 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
|
||||
<label className="form-label">MCP客户端代码 <span className="required">*</span></label>
|
||||
<textarea
|
||||
className="mcp-code-editor"
|
||||
placeholder={`粘贴 MCP 传输代码(标准 JSON 配置),例如:
|
||||
{
|
||||
"mcpServers": {
|
||||
"我的MCP服务": {
|
||||
"type": "streamable_http",
|
||||
"url": "https://your-mcp-server.com/mcp",
|
||||
"headers": { "X-API-KEY": "your-key" }
|
||||
placeholder={`粘贴MCP客户端代码,例如:
|
||||
const client = new MCPClient({
|
||||
transport: {
|
||||
type: "${mcpTransport}",
|
||||
endpoint: "https://your-mcp-server.com/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
注意:请直接粘贴完整的传输代码,系统会自动解析 url / headers / 传输类型并建立连接`}
|
||||
});`}
|
||||
value={mcpCode}
|
||||
onChange={e => { setMcpCode(e.target.value); setMcpCodeVerifyResult('idle'); }}
|
||||
rows={10}
|
||||
@@ -1045,7 +908,7 @@ interface HubImportModalProps {
|
||||
appType: 'skill' | 'mcp';
|
||||
platforms: typeof skillsHubPlatforms;
|
||||
onClose: () => void;
|
||||
onImport: (platformName: string, url: string, skillName: string, price?: number, description?: string, capability?: string) => void;
|
||||
onImport: (platformName: string, url: string, skillName: string, price?: number) => void;
|
||||
}
|
||||
|
||||
const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onClose, onImport }) => {
|
||||
@@ -1056,20 +919,9 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
const [statusMsg, setStatusMsg] = useState('');
|
||||
|
||||
// MCP code editor state for hub import
|
||||
// shared: used by the standalone custom-import button
|
||||
const [hubCode, setHubCode] = useState('');
|
||||
const [hubCodeVerifyResult, setHubCodeVerifyResult] = useState<'idle' | 'checking' | 'pass' | 'fail'>('idle');
|
||||
const [hubCodeVerifyMsg, setHubCodeVerifyMsg] = useState('');
|
||||
// per-platform: each MCP platform has its own Streamable HTTP code input
|
||||
const [hubCodeMap, setHubCodeMap] = useState<Record<string, string>>({});
|
||||
const [hubCodeVerifyResultMap, setHubCodeVerifyResultMap] = useState<Record<string, 'idle' | 'checking' | 'pass' | 'fail'>>({});
|
||||
const [hubCodeVerifyMsgMap, setHubCodeVerifyMsgMap] = useState<Record<string, string>>({});
|
||||
const [hubName, setHubName] = useState('');
|
||||
const [hubDescription, setHubDescription] = useState('');
|
||||
// 能力分类:AI验证后自动识别,可手动修改;用 ref 避免导入点击时的陈旧闭包
|
||||
const [capability, setCapability] = useState('');
|
||||
const capabilityRef = useRef('');
|
||||
const setCap = (v: string) => { setCapability(v); capabilityRef.current = v; };
|
||||
|
||||
const handleVisit = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
@@ -1085,7 +937,6 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
if (hasMinLength && hasKeywords) {
|
||||
setHubCodeVerifyResult('pass');
|
||||
setHubCodeVerifyMsg('AI验证通过:代码包含必要的传输协议和端点配置,可以导入');
|
||||
detectCap(hubName || 'MCP应用', hubCode);
|
||||
} else {
|
||||
setHubCodeVerifyResult('fail');
|
||||
setHubCodeVerifyMsg('AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性');
|
||||
@@ -1093,55 +944,11 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleHubMcpVerifyForPlatform = (platformId: string) => {
|
||||
const code = hubCodeMap[platformId] || '';
|
||||
if (!code.trim()) return;
|
||||
setHubCodeVerifyResultMap(prev => ({ ...prev, [platformId]: 'checking' }));
|
||||
setHubCodeVerifyMsgMap(prev => ({ ...prev, [platformId]: '' }));
|
||||
setTimeout(() => {
|
||||
const trimmed = code.trim();
|
||||
const hasMinLength = trimmed.length > 20;
|
||||
const hasKeywords = /(transport|endpoint|mcp|server|client|connection|url)/i.test(trimmed);
|
||||
if (hasMinLength && hasKeywords) {
|
||||
setHubCodeVerifyResultMap(prev => ({ ...prev, [platformId]: 'pass' }));
|
||||
setHubCodeVerifyMsgMap(prev => ({ ...prev, [platformId]: 'AI验证通过:代码包含必要的传输协议和端点配置,可以导入' }));
|
||||
detectCap(hubName || 'MCP应用', trimmed);
|
||||
} else {
|
||||
setHubCodeVerifyResultMap(prev => ({ ...prev, [platformId]: 'fail' }));
|
||||
setHubCodeVerifyMsgMap(prev => ({ ...prev, [platformId]: 'AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性' }));
|
||||
}
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const detectCap = (name: string, content: string, description?: string) => {
|
||||
if (!name.trim()) return;
|
||||
marketAPI.detectCapability({ name: name.trim(), description: description || '', skill_content: content })
|
||||
.then((res: any) => {
|
||||
if (res && res.code === 0 && res.data && res.data.capability) {
|
||||
setCap(res.data.capability);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const handleImport = (platformId: string) => {
|
||||
const url = importUrl[platformId];
|
||||
const isCustomImport = platformId === 'custom-import';
|
||||
const platformCode = isCustomImport ? hubCode : (hubCodeMap[platformId] || '');
|
||||
// For Skills: require URL; for MCP: require code verification pass + name
|
||||
// For Skills: require URL; for MCP: require code verification pass
|
||||
if (appType === 'skill' && !url?.trim()) return;
|
||||
if (appType === 'mcp') {
|
||||
if (isCustomImport) {
|
||||
if (hubCodeVerifyResult !== 'pass') return;
|
||||
} else {
|
||||
if (hubCodeVerifyResultMap[platformId] !== 'pass') return;
|
||||
}
|
||||
if (!hubName.trim()) return;
|
||||
}
|
||||
// Skills Hub 导入无独立AI验证步骤:在导入时触发一次能力识别(异步,导入完成前会回填到 ref)
|
||||
if (appType === 'skill' && !capabilityRef.current && url?.trim()) {
|
||||
detectCap(hubName || extractSkillName(url, platformId), url, hubDescription);
|
||||
}
|
||||
if (appType === 'mcp' && hubCodeVerifyResult !== 'pass') return;
|
||||
setImportingId(platformId);
|
||||
setImportStatus('verifying');
|
||||
setStatusMsg(appType === 'mcp' ? '正在验证代码...' : '正在验证URL链接...');
|
||||
@@ -1161,20 +968,13 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
setStatusMsg(step.msg);
|
||||
if (step.status === 'published') {
|
||||
setTimeout(() => {
|
||||
const name = appType === 'skill' ? (hubName || extractSkillName(url || '', platformId)) : hubName;
|
||||
const importPayload = appType === 'mcp' ? platformCode : (url || '');
|
||||
onImport(platformId, importPayload, name, importPrice, hubDescription, capabilityRef.current);
|
||||
onImport(platformId, appType === 'mcp' ? hubCode : url, extractSkillName(url, platformId), importPrice);
|
||||
setImportingId(null);
|
||||
setImportStatus('idle');
|
||||
setImportUrl(prev => ({ ...prev, [platformId]: '' }));
|
||||
setHubCode('');
|
||||
setHubCodeVerifyResult('idle');
|
||||
setHubCodeVerifyMsg('');
|
||||
setHubCodeMap(prev => ({ ...prev, [platformId]: '' }));
|
||||
setHubCodeVerifyResultMap(prev => ({ ...prev, [platformId]: 'idle' }));
|
||||
setHubCodeVerifyMsgMap(prev => ({ ...prev, [platformId]: '' }));
|
||||
setHubName('');
|
||||
setHubDescription('');
|
||||
}, 1000);
|
||||
}
|
||||
}, delay);
|
||||
@@ -1194,7 +994,7 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
<div className="hub-step-arrow"/>
|
||||
<div className="hub-step"><span className="hub-step-num">2</span> {appType === 'skill' ? '在平台找到目标链接并复制' : '在平台获取MCP客户端代码'}</div>
|
||||
<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 className="hub-price-section">
|
||||
<label className="form-label">定价(元)</label>
|
||||
@@ -1212,55 +1012,16 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Name & Description */}
|
||||
<div className="form-section">
|
||||
<label className="form-label">名称 <span className="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder={appType === 'skill' ? '请输入Skills工具名称' : '请输入MCP应用名称'}
|
||||
value={hubName}
|
||||
onChange={e => setHubName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">说明</label>
|
||||
<textarea
|
||||
className="form-textarea"
|
||||
placeholder="请描述功能、使用场景等..."
|
||||
value={hubDescription}
|
||||
onChange={e => setHubDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 能力分类(AI验证后自动识别,可手动修改) */}
|
||||
<div className="form-section">
|
||||
<label className="form-label">能力分类 <span className="cap-hint">(AI验证后自动识别,可修改)</span></label>
|
||||
<select className="form-input" value={capability} onChange={e => setCap(e.target.value)}>
|
||||
<option value="">未指定(由AI自动归类)</option>
|
||||
{SCENES.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* MCP Hub Import: Code Editor + AI验证 */}
|
||||
{appType === 'mcp' && (
|
||||
<div className="hub-code-section">
|
||||
<label className="form-label">MCP传输代码 <span className="required">*</span></label>
|
||||
<textarea
|
||||
className="mcp-code-editor"
|
||||
placeholder={`粘贴 MCP 传输代码(标准 JSON 配置,需通过AI验证后才能导入),例如:
|
||||
{
|
||||
"mcpServers": {
|
||||
"我的MCP服务": {
|
||||
"type": "streamable_http",
|
||||
"url": "https://your-mcp-server.com/mcp",
|
||||
"headers": { "X-API-KEY": "your-key" }
|
||||
}
|
||||
}
|
||||
}`}
|
||||
placeholder={`粘贴MCP客户端代码(需通过AI验证后才能导入),例如:
|
||||
const mcpClient = new MCPClient({
|
||||
transport: { type: "streamable-http", endpoint: "https://..." }
|
||||
});`}
|
||||
value={hubCode}
|
||||
onChange={e => { setHubCode(e.target.value); setHubCodeVerifyResult('idle'); }}
|
||||
rows={8}
|
||||
@@ -1290,33 +1051,9 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MCP 独立导入按钮:直接导入(不依赖选择平台卡片) */}
|
||||
{appType === 'mcp' && hubCodeVerifyResult === 'pass' && (
|
||||
<div className="hub-standalone-import" style={{ textAlign: 'center', marginBottom: 20, padding: '16px', background: 'var(--color-bg-secondary)', borderRadius: 8 }}>
|
||||
<button
|
||||
className="hub-import-btn"
|
||||
style={{ padding: '10px 40px', fontSize: 15, fontWeight: 600 }}
|
||||
onClick={() => handleImport('custom-import')}
|
||||
disabled={!hubName.trim()}
|
||||
>
|
||||
{importingId === 'custom-import' ? (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div className="audit-spinner tiny" />
|
||||
导入中...
|
||||
</span>
|
||||
) : '导入'}
|
||||
</button>
|
||||
{!hubName.trim() && <div style={{ color: '#f85149', fontSize: 12, marginTop: 6 }}>请先输入名称</div>}
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginTop: 6 }}>
|
||||
直接从传输代码导入,无需选择平台
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="hub-platforms">
|
||||
{platforms.map(p => (
|
||||
<div key={p.id} className="hub-platform-card">
|
||||
<div className="hub-platform-header">
|
||||
<div className="hub-platform-info">
|
||||
<div className="hub-platform-icon">
|
||||
<ImportIcon />
|
||||
@@ -1326,6 +1063,7 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
<span className="hub-platform-url">{p.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hub-platform-actions">
|
||||
<button
|
||||
className="hub-visit-btn"
|
||||
onClick={() => handleVisit(p.url)}
|
||||
@@ -1335,24 +1073,17 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
</svg>
|
||||
访问
|
||||
</button>
|
||||
</div>
|
||||
{appType === 'skill' ? (
|
||||
<>
|
||||
<div className="hub-import-col">
|
||||
<div className="hub-import-row">
|
||||
<input
|
||||
type="url"
|
||||
className="hub-url-input full"
|
||||
className="hub-url-input"
|
||||
placeholder="粘贴目标链接..."
|
||||
value={importUrl[p.id] || ''}
|
||||
onChange={e => setImportUrl(prev => ({ ...prev, [p.id]: e.target.value }))}
|
||||
disabled={importingId === p.id}
|
||||
/>
|
||||
<div className="hub-import-actions">
|
||||
{importUrl[p.id]?.trim() && (
|
||||
<span className="hub-name-preview">
|
||||
检测名称: <strong>{extractSkillName(importUrl[p.id], p.name)}</strong>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className="hub-import-btn"
|
||||
onClick={() => handleImport(p.id)}
|
||||
@@ -1366,15 +1097,18 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
) : '导入'}
|
||||
</button>
|
||||
</div>
|
||||
{importUrl[p.id]?.trim() && (
|
||||
<div className="hub-name-preview">
|
||||
检测名称: <strong>{extractSkillName(importUrl[p.id], p.name)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="hub-import-col">
|
||||
<div className="hub-import-actions">
|
||||
<div className="hub-import-row">
|
||||
<button
|
||||
className="hub-import-btn"
|
||||
onClick={() => handleImport(p.id)}
|
||||
disabled={hubCodeVerifyResultMap[p.id] !== 'pass' || importingId !== null}
|
||||
disabled={hubCodeVerifyResult !== 'pass' || importingId !== null}
|
||||
>
|
||||
{importingId === p.id ? (
|
||||
<span className="hub-importing-text">
|
||||
@@ -1384,54 +1118,8 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
) : '导入'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{appType === 'mcp' && (
|
||||
<div className="hub-platform-code-section">
|
||||
<label className="form-label">Streamable HTTP 传输代码 <span className="required">*</span></label>
|
||||
<textarea
|
||||
className="mcp-code-editor"
|
||||
placeholder={`粘贴 ${p.name} 的 MCP 传输代码(标准 JSON 配置,需通过AI验证后才能导入),例如:
|
||||
{
|
||||
"mcpServers": {
|
||||
"我的MCP服务": {
|
||||
"type": "streamable_http",
|
||||
"url": "https://your-mcp-server.com/mcp",
|
||||
"headers": { "X-API-KEY": "your-key" }
|
||||
}
|
||||
}
|
||||
}`}
|
||||
value={hubCodeMap[p.id] || ''}
|
||||
onChange={e => {
|
||||
setHubCodeMap(prev => ({ ...prev, [p.id]: e.target.value }));
|
||||
setHubCodeVerifyResultMap(prev => ({ ...prev, [p.id]: 'idle' }));
|
||||
}}
|
||||
rows={6}
|
||||
/>
|
||||
<div className="skills-upload-row">
|
||||
<span className="mcp-code-hint">请粘贴从 {p.name} 获取的 MCP 客户端代码,通过AI验证后方可导入</span>
|
||||
<button
|
||||
className="format-check-btn"
|
||||
onClick={() => handleHubMcpVerifyForPlatform(p.id)}
|
||||
disabled={!(hubCodeMap[p.id] || '').trim()}
|
||||
>
|
||||
{hubCodeVerifyResultMap[p.id] === 'checking' ? '验证中...' : 'AI验证'}
|
||||
</button>
|
||||
</div>
|
||||
{hubCodeVerifyResultMap[p.id] === 'checking' && (
|
||||
<div className="format-check-status checking">正在进行AI代码验证...</div>
|
||||
)}
|
||||
{hubCodeVerifyResultMap[p.id] === 'pass' && (
|
||||
<div className="format-check-status pass">
|
||||
<CheckCircleIcon />
|
||||
{hubCodeVerifyMsgMap[p.id]}
|
||||
</div>
|
||||
)}
|
||||
{hubCodeVerifyResultMap[p.id] === 'fail' && (
|
||||
<div className="format-check-status fail">{hubCodeVerifyMsgMap[p.id]}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1457,139 +1145,94 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
|
||||
);
|
||||
};
|
||||
|
||||
// ===== Agent 单接入 Modal(修改8:以单个Agent方式接入,含名称/介绍) =====
|
||||
// ===== Agent Platform Connect Modal (修改4) =====
|
||||
interface AgentConnectModalProps {
|
||||
onClose: () => void;
|
||||
onConnect: (platformId: string, config: Record<string, string>, price?: number, name?: string, description?: string) => void;
|
||||
onConnect: (platformId: string, config: Record<string, string>, price?: number) => void;
|
||||
}
|
||||
|
||||
const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnect }) => {
|
||||
const [platform, setPlatform] = useState<string>('dify');
|
||||
// 能力分类(可选,未指定时由后端 AI 自动归类)
|
||||
const [capability, setCapability] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [intro, setIntro] = useState('');
|
||||
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 [verifying, setVerifying] = useState(false);
|
||||
const [verifyMsg, setVerifyMsg] = useState<string>('');
|
||||
const [verifyOk, setVerifyOk] = useState<boolean | null>(null);
|
||||
const [connected, setConnected] = useState<string | null>(null);
|
||||
|
||||
const runVerify = async (): Promise<boolean | null> => {
|
||||
if (!endpoint.trim() || !apiKey.trim()) {
|
||||
setVerifyMsg('请先填写 API Endpoint 与 API Key');
|
||||
setVerifyOk(false);
|
||||
return false;
|
||||
}
|
||||
setVerifying(true);
|
||||
setVerifyMsg('正在校验连通性与密钥...');
|
||||
setVerifyOk(null);
|
||||
try {
|
||||
const res: any = await marketAPI.verifyAgent({
|
||||
platform,
|
||||
endpoint: endpoint.trim(),
|
||||
api_key: apiKey.trim(),
|
||||
});
|
||||
if (res && res.code === 0 && res.data && res.data.ok) {
|
||||
setVerifyOk(true);
|
||||
setVerifyMsg('✓ ' + (res.data.detail || '校验通过'));
|
||||
return true;
|
||||
}
|
||||
setVerifyOk(false);
|
||||
setVerifyMsg('✗ ' + ((res && res.data && res.data.detail) || '校验未通过'));
|
||||
return false;
|
||||
} catch (e: any) {
|
||||
setVerifyOk(false);
|
||||
setVerifyMsg('✗ 校验请求失败:' + (e?.message || '网络错误'));
|
||||
return false;
|
||||
} finally {
|
||||
setVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!name.trim() || !endpoint.trim() || !apiKey.trim()) return;
|
||||
// 接入前必须先通过连通性 / 鉴权校验
|
||||
const ok = await runVerify();
|
||||
if (!ok) return;
|
||||
const handleConnect = () => {
|
||||
if (!selectedPlatform || !apiKey.trim()) return;
|
||||
setConnecting(true);
|
||||
try {
|
||||
await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim(), capability);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setConnecting(false);
|
||||
}
|
||||
setConnected(selectedPlatform);
|
||||
onConnect(selectedPlatform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="market-modal-overlay" onClick={onClose}>
|
||||
<div className="market-modal agent-connect-modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="market-modal-header">
|
||||
<h2>接入单个 Agent</h2>
|
||||
<h2>对接外部Agent平台</h2>
|
||||
<button className="market-modal-close" onClick={onClose}><CloseIcon /></button>
|
||||
</div>
|
||||
<div className="market-modal-body">
|
||||
<p className="hub-import-desc">
|
||||
以单个 Agent 方式接入(例如 Dify 中每个 Agent 都有独立的访问地址与密钥)。请填写该 Agent 的名称、介绍及连接信息。
|
||||
将外部平台的Agent接入AIFlow应用市场,实现跨平台的Agent管理与调度。
|
||||
</p>
|
||||
<div className="form-section">
|
||||
<label className="form-label">所属平台</label>
|
||||
<select className="form-input" value={platform} onChange={e => setPlatform(e.target.value)}>
|
||||
|
||||
{!selectedPlatform ? (
|
||||
<div className="agent-platforms">
|
||||
{agentPlatforms.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
<div
|
||||
key={p.id}
|
||||
className={`agent-platform-card ${connected === p.id ? 'connected' : ''}`}
|
||||
onClick={() => { setSelectedPlatform(p.id); setEndpoint(p.id === 'dify' ? 'https://api.dify.ai/v1' : p.id === 'n8n' ? 'https://n8n.example.com/api/v1' : 'https://api.coze.cn/v1'); }}
|
||||
>
|
||||
<div className="agent-platform-icon">
|
||||
<ApiIcon />
|
||||
</div>
|
||||
<div className="agent-platform-info">
|
||||
<strong>{p.name}</strong>
|
||||
<span>{p.description}</span>
|
||||
</div>
|
||||
{connected === p.id ? (
|
||||
<span className="connected-badge"><CheckCircleIcon /> 已连接</span>
|
||||
) : (
|
||||
<span className="connect-indicator">配置</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-config-form">
|
||||
<button className="back-btn" onClick={() => setSelectedPlatform(null)}>
|
||||
← 返回选择
|
||||
</button>
|
||||
<div className="form-section">
|
||||
<label className="form-label">Agent 名称 <span className="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="例如:我的客服 Agent"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">Agent 介绍</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={3}
|
||||
placeholder="简要描述该 Agent 的能力与用途"
|
||||
value={intro}
|
||||
onChange={e => setIntro(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">能力分类 <span className="cap-hint">(可选,未指定由AI自动归类)</span></label>
|
||||
<select className="form-input" value={capability} onChange={e => setCapability(e.target.value)}>
|
||||
<option value="">未指定(由AI自动归类)</option>
|
||||
{SCENES.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">API Endpoint(该 Agent 专属地址)<span className="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="https://..."
|
||||
value={endpoint}
|
||||
onChange={e => setEndpoint(e.target.value)}
|
||||
/>
|
||||
<label className="form-label">平台</label>
|
||||
<input type="text" className="form-input" value={agentPlatforms.find(p => p.id === selectedPlatform)?.name || ''} disabled />
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">API Key <span className="required">*</span></label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
placeholder="请输入该 Agent 的 API Key"
|
||||
placeholder="请输入API Key"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">API Endpoint</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="https://..."
|
||||
value={endpoint}
|
||||
onChange={e => setEndpoint(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<label className="form-label">定价(元)</label>
|
||||
<input
|
||||
@@ -1601,30 +1244,23 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
|
||||
value={price}
|
||||
onChange={e => setPrice(parseFloat(e.target.value) || 0)}
|
||||
/>
|
||||
<span className="hub-price-hint">用户对接的 Agent 是否收费由您决定;管理员导入默认免费且审核通过。</span>
|
||||
</div>
|
||||
<div className="form-section agent-verify-row">
|
||||
<button
|
||||
type="button"
|
||||
className="form-test-btn"
|
||||
onClick={runVerify}
|
||||
disabled={!endpoint.trim() || !apiKey.trim() || verifying}
|
||||
>
|
||||
{verifying ? '校验中...' : '测试连接'}
|
||||
</button>
|
||||
{verifyMsg && (
|
||||
<span className={`agent-verify-msg ${verifyOk === true ? 'ok' : verifyOk === false ? 'err' : ''}`}>
|
||||
{verifyMsg}
|
||||
</span>
|
||||
)}
|
||||
<span className="hub-price-hint">用户对接的Agent:是否收费由您决定;管理员导入默认免费且审核通过。</span>
|
||||
</div>
|
||||
<button
|
||||
className="form-submit-btn"
|
||||
onClick={handleConnect}
|
||||
disabled={!name.trim() || !endpoint.trim() || !apiKey.trim() || connecting || verifying}
|
||||
disabled={!apiKey.trim() || connecting}
|
||||
>
|
||||
{connecting ? '接入中...' : '接入 Agent'}
|
||||
{connecting ? '连接验证中...' : '连接并同步Agent'}
|
||||
</button>
|
||||
{connecting && (
|
||||
<div className="hub-importing-status">
|
||||
<div className="audit-spinner small" />
|
||||
<span>正在验证连接并同步Agent列表...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1639,8 +1275,6 @@ const MarketPage: React.FC = () => {
|
||||
const [localApps, setLocalApps] = useState<MarketApp[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchTimerRef = useRef<number | null>(null);
|
||||
const [capabilityFilter, setCapabilityFilter] = useState<string | null>(null);
|
||||
const [installToast, setInstallToast] = useState<string | null>(null);
|
||||
|
||||
// Install toast
|
||||
@@ -1652,13 +1286,12 @@ const MarketPage: React.FC = () => {
|
||||
// 从后端加载应用列表
|
||||
const loadApps = (search?: string) => {
|
||||
setLoading(true);
|
||||
marketAPI.getApps({ ...(search ? { search } : {}), ...(capabilityFilter ? { capability: capabilityFilter } : {}), page_size: 100 }).then((res: any) => {
|
||||
marketAPI.getApps(search ? { search } : {}).then((res: any) => {
|
||||
if (res.code === 0 && res.data) {
|
||||
const items = (res.data.items || []).map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
capability: item.capability || null,
|
||||
source: item.source,
|
||||
publisherId: item.publisher_id,
|
||||
publisherName: item.publisher_name || '未知',
|
||||
@@ -1673,10 +1306,6 @@ const MarketPage: React.FC = () => {
|
||||
installCount: item.install_count || 0,
|
||||
installed: item.installed || false,
|
||||
createdAt: item.created_at || new Date().toISOString(),
|
||||
version: item.version || '1.0.0',
|
||||
verified: item.verified || false,
|
||||
securityScan: item.security_scan || null,
|
||||
dependencies: item.dependencies || null,
|
||||
}));
|
||||
setLocalApps(items);
|
||||
}
|
||||
@@ -1695,23 +1324,20 @@ const MarketPage: React.FC = () => {
|
||||
let base = activeTab === 'all'
|
||||
? localApps
|
||||
: localApps.filter(a => a.type === activeTab);
|
||||
if (capabilityFilter) {
|
||||
base = base.filter(a => a.capability === capabilityFilter);
|
||||
}
|
||||
return base;
|
||||
}, [activeTab, localApps, capabilityFilter]);
|
||||
}, [activeTab, localApps]);
|
||||
|
||||
const tabs: { key: AppType | 'all'; label: string }[] = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'skill', label: 'Skills工具' },
|
||||
{ key: 'agent', label: '智能Agent' },
|
||||
{ key: 'mcp', label: 'MCP应用' },
|
||||
{ key: 'mcp', label: 'MCP协议应用' },
|
||||
];
|
||||
|
||||
// Handle create tool - 调用后端 API 创建应用
|
||||
const handleCreateApp = async (data: Partial<MarketApp>) => {
|
||||
try {
|
||||
const payload: any = {
|
||||
await marketAPI.createApp({
|
||||
name: data.name,
|
||||
type: data.type || 'skill',
|
||||
description: data.description || '',
|
||||
@@ -1719,53 +1345,27 @@ const MarketPage: React.FC = () => {
|
||||
is_member_free: data.isMemberFree ?? true,
|
||||
join_promotion: data.joinPromotion ?? false,
|
||||
source: 'user',
|
||||
dependencies: (data as any).dependencies || undefined,
|
||||
};
|
||||
// 关键修复:把粘贴的 MCP 传输代码作为 download_url 传给后端,
|
||||
// 后端会据此解析出 url / headers / transport 并自动注册到 mcp_servers 表做一次连通性测试
|
||||
const mcpCode = (data as any).mcpCode as string | undefined;
|
||||
if (data.type === 'mcp' && mcpCode) {
|
||||
payload.download_url = mcpCode;
|
||||
payload.mcp_transport = (data as any).mcpTransport || 'streamable_http';
|
||||
}
|
||||
// 关键修复:手动创建的 skill 把真实 SKILL 内容作为 skill_content 传给后端落库
|
||||
const skillContent = (data as any).skillContent as string | undefined;
|
||||
if (data.type === 'skill' && skillContent) {
|
||||
payload.skill_content = skillContent;
|
||||
}
|
||||
await marketAPI.createApp(payload);
|
||||
});
|
||||
// 重新加载列表
|
||||
loadApps();
|
||||
} catch (e: any) {
|
||||
console.error('创建应用失败:', e);
|
||||
alert('创建应用失败:' + (e?.detail || e?.message || '未知错误'));
|
||||
}
|
||||
setShowCreateModal(null);
|
||||
};
|
||||
|
||||
// Handle hub import - 调用后端 API 创建应用
|
||||
const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number, description?: string, capability?: string) => {
|
||||
const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number) => {
|
||||
try {
|
||||
const isMcp = showHubImport === 'mcp';
|
||||
const desc = description || `从 ${platformName} 导入的${isMcp ? 'MCP应用' : 'Skills工具'}${!isMcp ? ` (来源: ${url})` : ''}`;
|
||||
const payload: any = {
|
||||
await marketAPI.createApp({
|
||||
name: skillName,
|
||||
type: showHubImport || 'skill',
|
||||
description: desc,
|
||||
description: `从 ${platformName} 导入的${showHubImport === 'skill' ? 'Skills工具' : 'MCP应用'} (来源: ${url})`,
|
||||
price: price || 0,
|
||||
is_member_free: (price || 0) === 0,
|
||||
join_promotion: false,
|
||||
source: 'user',
|
||||
};
|
||||
if (capability) payload.capability = capability;
|
||||
if (isMcp && url) {
|
||||
payload.download_url = url;
|
||||
}
|
||||
// skill Hub 导入:把目录 URL 作为 download_url 发后端,后端自动抓取真实 SKILL.md 回填 skill_content
|
||||
if (!isMcp && url) {
|
||||
payload.download_url = url;
|
||||
}
|
||||
await marketAPI.createApp(payload);
|
||||
});
|
||||
// 重新加载列表
|
||||
loadApps();
|
||||
} catch (e: any) {
|
||||
@@ -1774,28 +1374,21 @@ const MarketPage: React.FC = () => {
|
||||
setShowHubImport(null);
|
||||
};
|
||||
|
||||
// Handle agent connect - 调用后端 API 创建应用(单个Agent方式接入)
|
||||
const handleAgentConnect = async (platformId: string, config: Record<string, string>, price?: number, name?: string, description?: string, capability?: string) => {
|
||||
// Handle agent connect - 调用后端 API 创建应用
|
||||
const handleAgentConnect = async (platformId: string, config: Record<string, string>, price?: number) => {
|
||||
const platform = agentPlatforms.find(p => p.id === platformId);
|
||||
const finalName = (name && name.trim()) ? name.trim() : `${platform?.name || '外部'} Agent`;
|
||||
const finalDesc = (description && description.trim())
|
||||
? description.trim()
|
||||
: `从 ${platform?.name || '外部平台'} 对接导入的智能Agent`;
|
||||
if (!platform) return;
|
||||
try {
|
||||
const payload: any = {
|
||||
name: finalName,
|
||||
await marketAPI.createApp({
|
||||
name: `${platform.name} Agent`,
|
||||
type: 'agent',
|
||||
description: finalDesc,
|
||||
description: `从 ${platform.name} 对接导入的智能Agent`,
|
||||
price: price || 0,
|
||||
is_member_free: (price || 0) === 0,
|
||||
join_promotion: false,
|
||||
source: 'user',
|
||||
agent_endpoint: config.endpoint || '',
|
||||
api_key: config.apiKey || '',
|
||||
platform: platformId || 'dify',
|
||||
};
|
||||
if (capability) payload.capability = capability;
|
||||
await marketAPI.createApp(payload);
|
||||
});
|
||||
// 重新加载列表
|
||||
loadApps();
|
||||
} catch (e: any) {
|
||||
@@ -1911,22 +1504,6 @@ const MarketPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 业务场景筛选(岗位/职能视角,扁平) */}
|
||||
<div className="capability-filter">
|
||||
<button
|
||||
className={`cap-chip ${!capabilityFilter ? 'active' : ''}`}
|
||||
onClick={() => setCapabilityFilter(null)}
|
||||
>全部场景</button>
|
||||
{SCENES.map(c => (
|
||||
<button
|
||||
key={c.id}
|
||||
className={`cap-chip ${capabilityFilter === c.id ? 'active' : ''}`}
|
||||
style={capabilityFilter === c.id ? { color: '#fff', background: c.color, borderColor: c.color } : { color: c.color, borderColor: c.color + '55' }}
|
||||
onClick={() => setCapabilityFilter(capabilityFilter === c.id ? null : c.id)}
|
||||
>{c.label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)', padding: 'var(--space-md) 0', position: 'relative' }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-tertiary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ position: 'absolute', left: 10, zIndex: 1, pointerEvents: 'none' }}>
|
||||
@@ -1937,7 +1514,7 @@ const MarketPage: React.FC = () => {
|
||||
className="form-input"
|
||||
placeholder="搜索应用名称或描述..."
|
||||
value={searchQuery}
|
||||
onChange={e => { const v = e.target.value; setSearchQuery(v); if (searchTimerRef.current) clearTimeout(searchTimerRef.current); searchTimerRef.current = window.setTimeout(() => loadApps(v), 350); }}
|
||||
onChange={e => { setSearchQuery(e.target.value); loadApps(e.target.value); }}
|
||||
style={{ paddingLeft: 32, flex: 1, maxWidth: 400 }}
|
||||
/>
|
||||
{searchQuery && (
|
||||
@@ -1977,10 +1554,6 @@ const MarketPage: React.FC = () => {
|
||||
<OfficialBadge />
|
||||
</span>
|
||||
)}
|
||||
<CapabilityBadge capability={app.capability} />
|
||||
<VersionChip version={app.version} />
|
||||
{app.verified && <VerifiedBadge />}
|
||||
<SecurityBadge scan={app.securityScan} />
|
||||
</div>
|
||||
<p className="market-card-publisher">
|
||||
<UserIcon />
|
||||
@@ -1992,9 +1565,7 @@ const MarketPage: React.FC = () => {
|
||||
<div className="market-card-bottom">
|
||||
<StarRating rating={app.ratingAvg} count={app.reviewCount} />
|
||||
<div className="market-card-right">
|
||||
{app.source === 'official' && app.status === 'published' ? (
|
||||
<span className="market-card-official-badge">官方内置</span>
|
||||
) : app.installed ? (
|
||||
{app.installed ? (
|
||||
<button
|
||||
className="market-card-uninstall-btn"
|
||||
onClick={(e) => {
|
||||
|
||||
Reference in new issue
Block a user