diff --git a/backend/python/app/api/chat.py b/backend/python/app/api/chat.py index 1624b4f..0018b65 100644 --- a/backend/python/app/api/chat.py +++ b/backend/python/app/api/chat.py @@ -5376,7 +5376,7 @@ def _build_system_prompt( - system_parts.append("回复格式要求:你必须使用Markdown格式回复,确保层次分明、条理清晰。使用##和###做标题、**加粗**强调重点、-或1.做列表、```代码块包裹代码或配置、表格展示对比数据。禁止回复纯文本长段落。") + system_parts.append("回复格式要求:使用 Markdown 格式,层次分明即可。对于简短问题,用 1-2 段自然文字直接回答,不必强行分点、加标题或表格;仅在确实需要分步骤或列举时才使用列表,代码/配置用 ``` 代码块包裹。避免冗余空行,保持紧凑、美观、易读。") @@ -5528,7 +5528,13 @@ def _build_system_prompt( - system_parts.append("重要规则: 当用户请求涉及上述技能工具的处理范围时, 你必须优先通过function calling调用对应工具, 而不是直接回复文字。调用后将获得工具执行结果, 基于结果生成最终回复。") + system_parts.append( + "重要规则:\n" + "1) 当用户请求需要由该技能【执行】某项任务时,优先通过 function calling 调用对应工具;调用后基于工具返回结果生成回复。\n" + "2) 当用户询问所选技能【本身的介绍、功能、用途、用法、特点】(例如“介绍一下这个技能”“它是什么”“怎么用”“能做什么”)时," + "必须基于下方【已选技能资料】中的名称/描述/指令直接作答,不要泛泛介绍平台整体,也不要说“不知道这个技能”。\n" + "3) 若用户问题既不执行也不询问该技能本身,则按常规方式回答。" + ) @@ -5552,11 +5558,18 @@ def _build_system_prompt( - if instructions and len(instructions) < 800: - - - - system_parts.append(f"技能「{s['name']}」的详细指令: {instructions}") + name = s.get("name", "") + if not auto_discovered: + # 显式选中技能:注入可引用的技能资料,便于“介绍/说明”类问题据此作答 + ref_parts = [f"【已选技能资料】名称:{name}"] + desc = s.get("description", "") + if desc: + ref_parts.append(f"简介:{desc}") + if instructions and len(instructions) < 1200: + ref_parts.append(f"使用指令:{instructions}") + system_parts.append("\n".join(ref_parts)) + elif instructions and len(instructions) < 800: + system_parts.append(f"技能「{name}」的详细指令: {instructions}") diff --git a/frontend/src/pages/front/MarketPage.css b/frontend/src/pages/front/MarketPage.css index a166998..098a470 100644 --- a/frontend/src/pages/front/MarketPage.css +++ b/frontend/src/pages/front/MarketPage.css @@ -1778,13 +1778,14 @@ } .cap-chip:hover { - border-color: var(--color-primary); - color: var(--color-primary); + border-color: var(--color-accent); + color: var(--color-accent); } .cap-chip.active { color: #fff; - border-color: transparent; + background: var(--color-accent); + border-color: var(--color-accent); } .capability-badge { diff --git a/frontend/src/pages/front/MarketPage.tsx b/frontend/src/pages/front/MarketPage.tsx index e3f3bff..4c23f39 100644 --- a/frontend/src/pages/front/MarketPage.tsx +++ b/frontend/src/pages/front/MarketPage.tsx @@ -1,1963 +1,1964 @@ -import React, { useState, useMemo, useEffect, useRef } 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: , -}; - -// ===== 业务场景分类(面向用户的技能/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 ( - - {cap.label} - - ); -} - -// ===== 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(''); - // 能力分类:AI验证后自动识别,可手动修改;空字符串表示交由后端 AI 自动归类 - const [capability, setCapability] = 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标准规范(工具定义 + 使用说明 + 参数配置)'); - detectCap(content); - } 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验证通过:代码包含必要的传输协议和端点配置'); - detectCap(mcpCode); - } 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); - } - }; - - // 调用后端 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 - 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, - capability: capability || undefined, - 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', - // 关键修复:把粘贴的 MCP 传输代码随提交一起发给后端,否则后端拿不到代码、URL 为空、永远连不上 - mcpCode: appType === 'mcp' ? mcpCode.trim() : undefined, - mcpTransport: appType === 'mcp' ? mcpTransport : undefined, - // 关键修复:把粘贴/上传的 SKILL 内容随提交一起发给后端,否则 skill_content 为空、技能在对话里失效 - skillContent: appType === 'skill' ? (skillsContent.trim() || skillsFile || '') : undefined, - } as any); - }, 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' && ( -
- -