import React, { useState, useMemo, useEffect } from 'react'; import { marketAPI } from '@/api'; import type { MarketApp, AppType, AppReview } from '@/types'; import './MarketPage.css'; // ===== SVG Icons ===== const StarFilledIcon = ({ size = 14 }: { size?: number }) => ( ); const StarEmptyIcon = ({ size = 14 }: { size?: number }) => ( ); const DownloadIcon = () => ( ); const CloseIcon = () => ( ); const SkillIcon = () => ( ); const AgentIcon = () => ( ); const McpIcon = () => ( ); const UserIcon = () => ( ); const CheckCircleIcon = () => ( ); const OfficialBadge = () => ( ); const MessageSquareIcon = () => ( ); const PlusIcon = () => ( ); const ImportIcon = () => ( ); const ApiIcon = () => ( ); const ServerIcon = () => ( ); // ===== Color palette for avatar icons ===== const AVATAR_COLORS = [ { bg: '#EEEDFF', fg: '#5B5FED' }, { bg: '#E6F7EE', fg: '#219B5E' }, { bg: '#FEF4E8', fg: '#E8922C' }, { bg: '#EBF2FE', fg: '#3B82F6' }, { bg: '#FCE7F3', fg: '#EC4899' }, { bg: '#E0F2FE', fg: '#0EA5E9' }, { bg: '#FEF3C7', fg: '#D97706' }, { bg: '#EDE9FE', fg: '#8B5CF6' }, ]; function getAvatarFromName(name: string): { letter: string; bg: string; fg: string } { const letter = (name || '?').charAt(0).toUpperCase(); let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } const idx = Math.abs(hash) % AVATAR_COLORS.length; return { letter, ...AVATAR_COLORS[idx] }; } function renderAppIcon(app: MarketApp): React.ReactNode { const icon = app.icon; // 如果icon为空或就是问号,生成首字母头像 if (!icon || icon === '?') { const avatar = getAvatarFromName(app.name); return ( {avatar.letter} ); } // 如果icon是单个字符,生成头像 if (icon.length === 1) { const avatar = getAvatarFromName(app.name); return ( {app.name.charAt(0).toUpperCase()} ); } // 否则直接渲染icon文本(可能是emoji或其他) return {icon}; } function renderAppIconLarge(app: MarketApp): React.ReactNode { const icon = app.icon; if (!icon || icon === '?' || icon.length === 1) { const avatar = getAvatarFromName(app.name); return ( {app.name.charAt(0).toUpperCase()} ); } return {icon}; } const typeLabels: Record = { skill: 'Skills工具', agent: '智能Agent', mcp: 'MCP应用', }; const typeIcons: Record = { skill: , agent: , mcp: , }; // ===== Hub Platforms (修改3 & 修改5) ===== const skillsHubPlatforms = [ { id: 'skills.sh', name: 'skills.sh', url: 'https://www.skills.sh' }, { id: 'clawhub', name: 'ClawHub', url: 'https://clawhub.ai' }, { id: 'github', name: 'GitHub', url: 'https://github.com' }, { id: 'modelscope', name: 'ModelScope', url: 'https://modelscope.cn/skills' }, { id: 'skillsmp', name: 'SkillsMP', url: 'https://skillsmp.com' }, ]; 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) ===== const agentPlatforms = [ { id: 'dify', name: 'Dify', description: '开源LLMOps平台,快速构建AI应用', type: 'external' }, { id: 'n8n', name: 'n8n', description: '开源自动化工作流平台', type: 'external' }, { id: 'coze', name: 'Coze', description: '字节跳动AI Bot开发平台', type: 'external' }, ]; // MCP传输类型 type MCPTransportType = 'streamable-http' | 'sse'; // ===== Star Rating ===== const StarRating: React.FC<{ rating: number; count?: number; size?: number }> = ({ rating, count, size = 14 }) => { const stars = []; for (let i = 1; i <= 5; i++) { if (i <= Math.floor(rating)) { stars.push(); } else if (i - 0.5 <= rating) { stars.push(); } else { stars.push(); } } return ( {stars} {count !== undefined && ({count})} ); }; // ===== Helpers ===== function formatInstallCount(n: number): string { if (n >= 10000) return (n / 10000).toFixed(1) + '万'; if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return String(n); } // ===== App Detail Modal ===== interface AppDetailModalProps { app: MarketApp; onClose: () => void; onAppChange?: () => void; } // 全局toast函数引用 let globalShowToast: ((msg: string) => void) | null = null; const AppDetailModal: React.FC = ({ app, onClose, onAppChange }) => { const [toastMsg, setToastMsg] = useState(null); useEffect(() => { globalShowToast = setToastMsg; return () => { globalShowToast = null; }; }, []); const showToast = (msg: string) => { setToastMsg(msg); setTimeout(() => setToastMsg(null), 3000); }; const [installed, setInstalled] = useState(!!app.installed); const [reviews, setReviews] = useState([]); const [installing, setInstalling] = useState(false); const [uninstalling, setUninstalling] = useState(false); // 当app prop变化时同步installed状态 useEffect(() => { setInstalled(!!app.installed); }, [app.id, app.installed]); useEffect(() => { // 加载评论 marketAPI.getReviews(app.id).then((res: any) => { if (res.code === 0 && res.data) { const items = res.data.items || []; setReviews(items.map((r: any) => ({ id: r.id, userId: r.user_id, userName: r.username, appId: app.id, rating: r.rating, content: r.content, createdAt: r.created_at, }))); } }).catch(() => {}); }, [app.id]); const handleInstall = async () => { if (installing) return; // 检查是否已安装 if (installed || app.installed) { showToast(`"${app.name}" 已经安装,无需重复安装`); return; } setInstalling(true); try { const res: any = await marketAPI.installApp(app.id); if (res.code === 0) { setInstalled(true); showToast(`"${app.name}" 安装成功`); onAppChange?.(); } } catch (e: any) { const msg = e.detail || e.message || '安装失败'; showToast(`安装失败:${msg}`); } finally { setInstalling(false); } }; const handleUninstall = async () => { if (uninstalling) return; if (!window.confirm(`确定要卸载 "${app.name}" 吗?`)) return; setUninstalling(true); try { const res: any = await marketAPI.uninstallApp(app.id); if (res.code === 0) { setInstalled(false); showToast(`"${app.name}" 已卸载`); onAppChange?.(); } } catch (e: any) { const msg = e.detail || e.message || '卸载失败'; showToast(`卸载失败:${msg}`); } finally { setUninstalling(false); } }; return (
e.stopPropagation()}>
{/* App Info */}
{renderAppIconLarge(app)}

{app.name}

{app.source === 'official' && ( )}
{typeIcons[app.type]} {typeLabels[app.type]} {app.publisherName}
{/* Rating & Price */}
安装量 {formatInstallCount(app.installCount)}
{/* Description */}

应用描述

{app.description}

{/* Price & Install */}
{app.isMemberFree ? ( <> 会员免费 原价 {app.price}元/月 ) : ( {app.price} 元/月 )}
{installed ? ( ) : ( )}
{/* Reviews */} {reviews.length > 0 && (

用户评价 ({reviews.length})

{reviews.map(r => (
{r.userName.charAt(0).toUpperCase()}
{r.userName}

{r.content}

))}
)} {/* Toast notification */} {toastMsg && (
{toastMsg}
)}
); }; // ===== Create Tool Modal (修改2: 用户自定义, 修改3: Skills特殊逻辑) ===== interface CreateToolModalProps { appType: AppType; onClose: () => void; onSubmit: (app: Partial) => void; } const CreateToolModal: React.FC = ({ appType, onClose, onSubmit }) => { const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [isPaid, setIsPaid] = useState(false); const [price, setPrice] = useState(''); const [joinPromotion, setJoinPromotion] = useState(false); const [creationMethod, setCreationMethod] = useState<'manual' | 'ai'>('manual'); const [mcpTransport, setMcpTransport] = useState('streamable-http'); const [step, setStep] = useState<'form' | 'audit' | 'done'>('form'); // Skills-specific: manual creation const [skillsContent, setSkillsContent] = useState(''); const [skillsFile, setSkillsFile] = useState(null); const [formatCheckResult, setFormatCheckResult] = useState<'idle' | 'checking' | 'pass' | 'fail'>('idle'); const [formatCheckMsg, setFormatCheckMsg] = useState(''); // MCP code editor state const [mcpCode, setMcpCode] = useState(''); const [mcpCodeVerifyResult, setMcpCodeVerifyResult] = useState<'idle' | 'checking' | 'pass' | 'fail'>('idle'); const [mcpCodeVerifyMsg, setMcpCodeVerifyMsg] = useState(''); const handleCheckFormat = () => { const content = skillsFile || skillsContent; if (!content.trim()) return; setFormatCheckResult('checking'); // Simulate AI format check setTimeout(() => { const hasMarkdownHeader = content.includes('#') || content.includes('##'); const hasMinLength = content.trim().length > 50; const hasValidStructure = /(description|功能|描述|usage|用法|tool|skill)/i.test(content); if (hasMinLength && (hasMarkdownHeader || hasValidStructure)) { setFormatCheckResult('pass'); setFormatCheckMsg('格式检测通过:符合Skills标准规范(工具定义 + 使用说明 + 参数配置)'); } else { setFormatCheckResult('fail'); setFormatCheckMsg('格式不符合Skills标准:请确保包含工具定义、功能描述、使用说明和参数配置'); } }, 1500); }; const handleMcpVerify = () => { if (!mcpCode.trim()) return; setMcpCodeVerifyResult('checking'); setTimeout(() => { const code = mcpCode.trim(); const hasMinLength = code.length > 20; const hasKeywords = /(transport|endpoint|mcp|server|client|connection|url)/i.test(code); if (hasMinLength && hasKeywords) { setMcpCodeVerifyResult('pass'); setMcpCodeVerifyMsg('AI验证通过:代码包含必要的传输协议和端点配置'); } else { setMcpCodeVerifyResult('fail'); setMcpCodeVerifyMsg('AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性'); } }, 1200); }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (ev) => { const content = ev.target?.result as string; setSkillsFile(content); }; reader.readAsText(file); } }; const handleSubmit = () => { if (!name.trim() || !description.trim()) return; // Skills requires format check pass if (appType === 'skill' && creationMethod === 'manual' && formatCheckResult !== 'pass') return; // MCP requires code verification pass if (appType === 'mcp' && mcpCodeVerifyResult !== 'pass') return; setStep('audit'); setTimeout(() => { setStep('done'); setTimeout(() => { onSubmit({ name: name.trim(), type: appType, description: description.trim(), price: isPaid ? parseFloat(price) || 0 : 0, isMemberFree: !isPaid, joinPromotion, source: 'user', publisherId: 'u1', publisherName: 'demo_user', status: 'published' as const, icon: appType === 'skill' ? 'S' : appType === 'agent' ? 'A' : 'M', }); }, 1500); }, 2000); }; return (
e.stopPropagation()}>

创建{typeLabels[appType]}

{step === 'form' && (
{/* Creation Method */}
{/* AI Chat Method - Skills only */} {creationMethod === 'ai' && appType === 'skill' && (
即将跳转到AI聊天创建工具

系统将为你在AI聊天中创建一个临时新对话窗口。接下来的所有对话内容都将转化为该Skills工具的核心定义——包括功能描述、使用方式、参数配置和输出格式。

)} {creationMethod === 'ai' && appType !== 'skill' && (