From 95c09688e6490d554cf6802362b5e8c3ce34e0d1 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 1 Jun 2026 17:48:40 +0800 Subject: [PATCH] feat(web): show recommendation lists in plugin market; mixed-type icons The marketplace recommendation lists (curated rows from Space) were never mounted in the plugin market page. Wire them in: - fetch recommendation lists on mount and render them above the extension grid, only when no search/filter is active. Recommendation lists now mix plugins, MCPs and skills, so resolve each card's icon by type (plugin / mcp / skill marketplace icon URL) instead of always using the plugin icon endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugin-market/PluginMarketComponent.tsx | 29 +++++++++++++++++++ .../plugin-market/RecommendationLists.tsx | 14 ++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx index bbdbb601..d14d2e0a 100644 --- a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx @@ -27,6 +27,8 @@ import { } from 'lucide-react'; import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent'; import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO'; +import { RecommendationLists } from './RecommendationLists'; +import type { RecommendationList } from './RecommendationLists'; import { getCloudServiceClientSync } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { PluginV4, PluginV4Status } from '@/app/infra/entities/plugin'; @@ -78,6 +80,9 @@ function MarketPageContent({ const [selectedTags, setSelectedTags] = useState([]); const [availableTags, setAvailableTags] = useState([]); const [tagNames, setTagNames] = useState>({}); + const [recommendationLists, setRecommendationLists] = useState< + RecommendationList[] + >([]); const [plugins, setPlugins] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); @@ -250,9 +255,21 @@ function MarketPageContent({ useEffect(() => { fetchPlugins(1, false, true); fetchAvailableTags(); + fetchRecommendationLists(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // 获取推荐列表(精选,混合插件/MCP/Skill) + const fetchRecommendationLists = async () => { + try { + const { lists } = + await getCloudServiceClientSync().getRecommendationLists(); + setRecommendationLists(lists || []); + } catch (error) { + console.error('Failed to fetch recommendation lists:', error); + } + }; + // 获取可用标签 const fetchAvailableTags = async () => { try { @@ -701,6 +718,18 @@ function MarketPageContent({ ref={scrollContainerRef} className="flex-1 overflow-y-auto px-3 sm:px-4 pb-6 container mx-auto" > + {/* 推荐列表(仅在无搜索/筛选时展示,混合插件/MCP/Skill) */} + {!searchQuery && + typeFilter === 'all' && + componentFilter === 'all' && + selectedTags.length === 0 && ( + + )} + {isLoading ? (
diff --git a/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx b/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx index 90c0ffde..eeb3807a 100644 --- a/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx +++ b/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx @@ -22,6 +22,15 @@ function pluginToVO( plugin: PluginV4, t: (key: string) => string, ): PluginMarketCardVO { + const cloudClient = getCloudServiceClientSync(); + // Recommendation lists are mixed-type; resolve the icon per extension type. + const iconURL = + plugin.type === 'mcp' + ? cloudClient.getMCPMarketplaceIconURL(plugin.author, plugin.name) + : plugin.type === 'skill' + ? cloudClient.getSkillMarketplaceIconURL(plugin.author, plugin.name) + : cloudClient.getPluginIconURL(plugin.author, plugin.name); + return new PluginMarketCardVO({ pluginId: plugin.author + ' / ' + plugin.name, author: plugin.author, @@ -30,10 +39,7 @@ function pluginToVO( description: extractI18nObject(plugin.description) || t('market.noDescription'), installCount: plugin.install_count, - iconURL: getCloudServiceClientSync().getPluginIconURL( - plugin.author, - plugin.name, - ), + iconURL, githubURL: plugin.repository, version: plugin.latest_version, components: plugin.components,