diff --git a/web/src/app/home/add-extension/page.tsx b/web/src/app/home/add-extension/page.tsx index 42e8d148..f03acf3a 100644 --- a/web/src/app/home/add-extension/page.tsx +++ b/web/src/app/home/add-extension/page.tsx @@ -184,11 +184,12 @@ function AddExtensionContent() { const cloud = getCloudServiceClientSync(); const a = installInfo.plugin_author || ''; const n = installInfo.plugin_name || ''; - if (installExtensionType === 'mcp') - return cloud.getMCPMarketplaceIconURL(a, n); - if (installExtensionType === 'skill') - return cloud.getSkillMarketplaceIconURL(a, n); - return cloud.getPluginIconURL(a, n); + return cloud.resolveMarketplaceIconURL( + installExtensionType, + a, + n, + installInfo.plugin_icon, + ); })(); const [popoverOpen, setPopoverOpen] = useState(false); @@ -326,6 +327,7 @@ function AddExtensionContent() { plugin_version: plugin.latest_version, plugin_label: extractI18nObject(plugin.label) || plugin.name, plugin_description: extractI18nObject(plugin.description) || '', + plugin_icon: plugin.icon || '', }); setInstallExtensionType(plugin.type || 'plugin'); setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); 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 14a0b9bc..aceca127 100644 --- a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx @@ -216,12 +216,12 @@ function MarketPageContent({ const transformToVO = useCallback( (plugin: PluginV4): PluginMarketCardVO => { const cloudClient = getCloudServiceClientSync(); - 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); + const iconURL = cloudClient.resolveMarketplaceIconURL( + plugin.type, + plugin.author, + plugin.name, + plugin.icon, + ); return new PluginMarketCardVO({ pluginId: plugin.author + ' / ' + plugin.name, 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 eeb3807a..65aa7807 100644 --- a/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx +++ b/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx @@ -23,13 +23,14 @@ function pluginToVO( 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); + // Recommendation lists are mixed-type; resolve the icon per extension type, + // preferring an absolute external icon URL when the record carries one. + const iconURL = cloudClient.resolveMarketplaceIconURL( + plugin.type, + plugin.author, + plugin.name, + plugin.icon, + ); return new PluginMarketCardVO({ pluginId: plugin.author + ' / ' + plugin.name, diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts index f4890876..e1c5b33d 100644 --- a/web/src/app/infra/http/CloudServiceClient.ts +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -230,6 +230,29 @@ export class CloudServiceClient extends BaseHttpClient { return `${this.baseURL}/api/v1/marketplace/skills/${author}/${name}/resources/icon`; } + /** + * Resolve the best icon URL for a marketplace extension. + * + * Many MCP / skill records store their ``icon`` as an absolute external URL + * (e.g. simpleicons.org / iconify.design logos) rather than a file uploaded + * to Space storage. For those, the ``/resources/icon`` endpoint 404s, so we + * must use the external URL directly. Records whose ``icon`` is empty or a + * relative path fall back to the ``/resources/icon`` endpoint (real uploads). + */ + public resolveMarketplaceIconURL( + type: 'plugin' | 'mcp' | 'skill' | undefined, + author: string, + name: string, + icon?: string, + ): string { + if (icon && /^https?:\/\//i.test(icon)) { + return icon; + } + if (type === 'mcp') return this.getMCPMarketplaceIconURL(author, name); + if (type === 'skill') return this.getSkillMarketplaceIconURL(author, name); + return this.getPluginIconURL(author, name); + } + public getPluginAssetURL( author: string, pluginName: string,