feat(market): skills/mcp 能力分类 + AI验证期自动识别 + 导入自动入座

This commit is contained in:
QihuAI Design committed 2026-07-17 12:09:42 +08:00
1 parent 2ae5d9f9b5
commit 6f3fb400a4
7 files changed
+1407 -1140

No files matched your search

+5 -1
View File
@@ -289,7 +289,7 @@ export const knowledgeAPI = {
export const marketAPI = {
getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => {
getApps: (params?: { type?: string; source?: string; capability?: string; search?: string; page?: number; page_size?: number }) => {
const searchParams = new URLSearchParams();
@@ -297,6 +297,8 @@ export const marketAPI = {
if (params?.source) searchParams.set('source', params.source);
if (params?.capability) searchParams.set('capability', params.capability);
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
@@ -315,6 +317,8 @@ export const marketAPI = {
verifyAgent: (data: any) => post<any>('/market/apps/verify-agent', data),
detectCapability: (data: any) => post<any>('/market/apps/detect-capability', data),
purchaseApp: (id: string) => post<any>(`/market/apps/${id}/purchase`),
installApp: (id: string) => post<any>(`/market/apps/${id}/install`),
+49
View File
@@ -1739,3 +1739,52 @@
border: 1px solid var(--color-border-light);
border-radius: var(--radius-md);
}
/* ===== 能力分类筛选 & 徽章 ===== */
.capability-filter {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
padding: var(--space-sm) 0 0;
}
.cap-chip {
padding: 4px 12px;
font-size: var(--text-xs);
font-weight: 500;
border-radius: 999px;
border: 1px solid var(--color-border-light);
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.cap-chip:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.cap-chip.active {
color: #fff;
border-color: transparent;
}
.capability-badge {
display: inline-flex;
align-items: center;
padding: 1px 8px;
font-size: 11px;
font-weight: 600;
line-height: 18px;
border-radius: 6px;
border: 1px solid transparent;
white-space: nowrap;
}
.cap-hint {
font-size: 11px;
font-weight: 400;
color: var(--color-text-tertiary);
}
+138 -10
View File
@@ -1,4 +1,4 @@
import React, { useState, useMemo, useEffect } from 'react';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { marketAPI } from '@/api';
import type { MarketApp, AppType, AppReview } from '@/types';
import './MarketPage.css';
@@ -182,6 +182,36 @@ const typeIcons: Record<AppType, React.ReactNode> = {
mcp: <McpIcon />,
};
// ===== 能力分类(与后端 11 种能力保持一致) =====
const CAPABILITIES: { id: string; label: string; color: string }[] = [
{ id: 'text', label: '文本对话', color: '#5B5FED' },
{ id: 'thinking', label: '推理思考', color: '#8B5CF6' },
{ id: 'vision', label: '视觉理解', color: '#219B5E' },
{ id: 'tool', label: '工具调用', color: '#3B82F6' },
{ id: 'image', label: '图像生成', color: '#D9383A' },
{ id: 'audio', label: '语音合成', color: '#E8922C' },
{ id: 'asr', label: '语音识别', color: '#0EA5E9' },
{ id: 'ocr', label: 'OCR识别', color: '#F59E0B' },
{ id: 'embedding', label: '嵌入向量', color: '#10B981' },
{ id: 'rerank', label: '重排序', color: '#EC4899' },
{ id: 'video', label: '视频生成', color: '#EF4444' },
];
function CapabilityBadge({ capability }: { capability?: string | null }) {
if (!capability) return null;
const cap = CAPABILITIES.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>
);
}
// ===== Hub Platforms (修改3 & 修改5) =====
const skillsHubPlatforms = [
{ id: 'skills.sh', name: 'skills.sh', url: 'https://www.skills.sh' },
@@ -351,6 +381,7 @@ const AppDetailModal: React.FC<AppDetailModalProps> = ({ app, onClose, onAppChan
{typeIcons[app.type]}
{typeLabels[app.type]}
</span>
<CapabilityBadge capability={app.capability} />
<span className="modal-app-publisher">
<UserIcon />
{app.publisherName}
@@ -470,6 +501,8 @@ 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('');
const handleCheckFormat = () => {
const content = skillsFile || skillsContent;
@@ -483,6 +516,7 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
if (hasMinLength && (hasMarkdownHeader || hasValidStructure)) {
setFormatCheckResult('pass');
setFormatCheckMsg('格式检测通过:符合Skills标准规范(工具定义 + 使用说明 + 参数配置)');
detectCap(content);
} else {
setFormatCheckResult('fail');
setFormatCheckMsg('格式不符合Skills标准:请确保包含工具定义、功能描述、使用说明和参数配置');
@@ -500,6 +534,7 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
if (hasMinLength && hasKeywords) {
setMcpCodeVerifyResult('pass');
setMcpCodeVerifyMsg('AI验证通过:代码包含必要的传输协议和端点配置');
detectCap(mcpCode);
} else {
setMcpCodeVerifyResult('fail');
setMcpCodeVerifyMsg('AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性');
@@ -519,6 +554,18 @@ 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
@@ -532,6 +579,7 @@ 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,
@@ -697,6 +745,19 @@ const CreateToolModal: React.FC<CreateToolModalProps> = ({ appType, onClose, onS
/>
</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>
{CAPABILITIES.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">
@@ -879,7 +940,7 @@ interface HubImportModalProps {
appType: 'skill' | 'mcp';
platforms: typeof skillsHubPlatforms;
onClose: () => void;
onImport: (platformName: string, url: string, skillName: string, price?: number, description?: string) => void;
onImport: (platformName: string, url: string, skillName: string, price?: number, description?: string, capability?: string) => void;
}
const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onClose, onImport }) => {
@@ -900,6 +961,10 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
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');
@@ -915,6 +980,7 @@ 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),请检查代码完整性');
@@ -934,6 +1000,7 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
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),请检查代码完整性' }));
@@ -941,6 +1008,17 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
}, 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';
@@ -955,6 +1033,10 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
}
if (!hubName.trim()) return;
}
// Skills Hub 导入无独立AI验证步骤:在导入时触发一次能力识别(异步,导入完成前会回填到 ref)
if (appType === 'skill' && !capabilityRef.current && url?.trim()) {
detectCap(hubName || extractSkillName(url, platformId), url, hubDescription);
}
setImportingId(platformId);
setImportStatus('verifying');
setStatusMsg(appType === 'mcp' ? '正在验证代码...' : '正在验证URL链接...');
@@ -976,7 +1058,7 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
setTimeout(() => {
const name = appType === 'skill' ? (hubName || extractSkillName(url || '', platformId)) : hubName;
const importPayload = appType === 'mcp' ? platformCode : (url || '');
onImport(platformId, importPayload, name, importPrice, hubDescription);
onImport(platformId, importPayload, name, importPrice, hubDescription, capabilityRef.current);
setImportingId(null);
setImportStatus('idle');
setImportUrl(prev => ({ ...prev, [platformId]: '' }));
@@ -1047,6 +1129,17 @@ const HubImportModal: React.FC<HubImportModalProps> = ({ appType, platforms, onC
/>
</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>
{CAPABILITIES.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">
@@ -1267,6 +1360,8 @@ interface AgentConnectModalProps {
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 [apiKey, setApiKey] = useState('');
@@ -1316,7 +1411,7 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
if (!ok) return;
setConnecting(true);
try {
await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim());
await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim(), capability);
} finally {
setConnecting(false);
}
@@ -1361,6 +1456,15 @@ const AgentConnectModal: React.FC<AgentConnectModalProps> = ({ onClose, onConnec
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>
{CAPABILITIES.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
@@ -1430,6 +1534,7 @@ const MarketPage: React.FC = () => {
const [localApps, setLocalApps] = useState<MarketApp[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [capabilityFilter, setCapabilityFilter] = useState<string | null>(null);
const [installToast, setInstallToast] = useState<string | null>(null);
// Install toast
@@ -1441,7 +1546,7 @@ const MarketPage: React.FC = () => {
// 从后端加载应用列表
const loadApps = (search?: string) => {
setLoading(true);
marketAPI.getApps(search ? { search, page_size: 100 } : { page_size: 100 }).then((res: any) => {
marketAPI.getApps({ ...(search ? { search } : {}), ...(capabilityFilter ? { capability: capabilityFilter } : {}), page_size: 100 }).then((res: any) => {
if (res.code === 0 && res.data) {
const items = (res.data.items || []).map((item: any) => ({
id: item.id,
@@ -1479,8 +1584,11 @@ 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]);
}, [activeTab, localApps, capabilityFilter]);
const tabs: { key: AppType | 'all'; label: string }[] = [
{ key: 'all', label: '全部' },
@@ -1524,7 +1632,7 @@ const MarketPage: React.FC = () => {
};
// Handle hub import - 调用后端 API 创建应用
const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number, description?: string) => {
const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number, description?: string, capability?: string) => {
try {
const isMcp = showHubImport === 'mcp';
const desc = description || `从 ${platformName} 导入的${isMcp ? 'MCP应用' : 'Skills工具'}${!isMcp ? ` (来源: ${url})` : ''}`;
@@ -1537,6 +1645,7 @@ const MarketPage: React.FC = () => {
join_promotion: false,
source: 'user',
};
if (capability) payload.capability = capability;
if (isMcp && url) {
payload.download_url = url;
}
@@ -1554,14 +1663,14 @@ const MarketPage: React.FC = () => {
};
// Handle agent connect - 调用后端 API 创建应用(单个Agent方式接入)
const handleAgentConnect = async (platformId: string, config: Record<string, string>, price?: number, name?: string, description?: string) => {
const handleAgentConnect = async (platformId: string, config: Record<string, string>, price?: number, name?: string, description?: string, capability?: string) => {
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`;
try {
await marketAPI.createApp({
const payload: any = {
name: finalName,
type: 'agent',
description: finalDesc,
@@ -1572,7 +1681,9 @@ const MarketPage: React.FC = () => {
agent_endpoint: config.endpoint || '',
api_key: config.apiKey || '',
platform: platformId || 'dify',
});
};
if (capability) payload.capability = capability;
await marketAPI.createApp(payload);
// 重新加载列表
loadApps();
} catch (e: any) {
@@ -1688,6 +1799,22 @@ const MarketPage: React.FC = () => {
</div>
)}
{/* 能力分类筛选 */}
<div className="capability-filter">
<button
className={`cap-chip ${!capabilityFilter ? 'active' : ''}`}
onClick={() => setCapabilityFilter(null)}
>全部能力</button>
{CAPABILITIES.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' }}>
@@ -1738,6 +1865,7 @@ const MarketPage: React.FC = () => {
<OfficialBadge />
</span>
)}
<CapabilityBadge capability={app.capability} />
</div>
<p className="market-card-publisher">
<UserIcon />
+1
View File
@@ -107,6 +107,7 @@ export interface MarketApp {
reviewCount: number;
installCount: number;
installed?: boolean;
capability?: string;
createdAt: string;
}