From 7c50aabe65c375d0e9251875074f509c5b899a31 Mon Sep 17 00:00:00 2001 From: WangCham <651122857@qq.com> Date: Sat, 2 May 2026 17:38:18 +0800 Subject: [PATCH] feat: add mcp and skills --- .../plugin-installed/PluginCardVO.ts | 3 + .../PluginInstalledComponent.tsx | 9 ++- .../plugin-card/PluginCardComponent.tsx | 18 +++++ .../plugin-market/PluginMarketComponent.tsx | 81 ++++++++++++++++++- .../plugin-market/RecommendationLists.tsx | 1 + .../PluginMarketCardComponent.tsx | 18 +++++ .../plugin-market-card/PluginMarketCardVO.ts | 3 + web/src/app/infra/entities/plugin/index.ts | 1 + web/src/app/infra/http/CloudServiceClient.ts | 2 + web/src/i18n/locales/en-US.ts | 6 ++ web/src/i18n/locales/es-ES.ts | 6 ++ web/src/i18n/locales/ja-JP.ts | 8 +- web/src/i18n/locales/ru-RU.ts | 6 ++ web/src/i18n/locales/th-TH.ts | 6 ++ web/src/i18n/locales/vi-VN.ts | 6 ++ web/src/i18n/locales/zh-Hans.ts | 6 ++ web/src/i18n/locales/zh-Hant.ts | 6 ++ 17 files changed, 180 insertions(+), 6 deletions(-) diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts index 2be807c0..279161b4 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts @@ -14,6 +14,7 @@ export interface IPluginCardVO { components: PluginComponent[]; debug: boolean; hasUpdate?: boolean; + type?: 'plugin' | 'mcp' | 'skill'; } export class PluginCardVO implements IPluginCardVO { @@ -30,6 +31,7 @@ export class PluginCardVO implements IPluginCardVO { status: string; components: PluginComponent[]; hasUpdate?: boolean; + type?: 'plugin' | 'mcp' | 'skill'; constructor(prop: IPluginCardVO) { this.author = prop.author; @@ -45,5 +47,6 @@ export class PluginCardVO implements IPluginCardVO { this.install_source = prop.install_source; this.install_info = prop.install_info; this.hasUpdate = prop.hasUpdate; + this.type = prop.type; } } diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx index f4df16e1..f0ff7899 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx @@ -88,6 +88,8 @@ const PluginInstalledComponent = forwardRef( // 转换并比较版本号 const pluginCards = installedPlugins.map((plugin) => { + const marketplaceKey = `${plugin.manifest.manifest.metadata.author}/${plugin.manifest.manifest.metadata.name}`; + const marketplacePlugin = marketplacePluginMap.get(marketplaceKey); const cardVO = new PluginCardVO({ author: plugin.manifest.manifest.metadata.author ?? '', label: extractI18nObject(plugin.manifest.manifest.metadata.label), @@ -106,13 +108,12 @@ const PluginInstalledComponent = forwardRef( priority: plugin.priority, install_source: plugin.install_source, install_info: plugin.install_info, + type: marketplacePlugin?.type, }); // 检查是否来自市场且有更新 - if (cardVO.install_source === 'marketplace') { - const marketplaceKey = `${cardVO.author}/${cardVO.name}`; - const marketplacePlugin = marketplacePluginMap.get(marketplaceKey); - if (marketplacePlugin && marketplacePlugin.latest_version) { + if (cardVO.install_source === 'marketplace' && marketplacePlugin) { + if (marketplacePlugin.latest_version) { cardVO.hasUpdate = isNewerVersion( marketplacePlugin.latest_version, cardVO.version, diff --git a/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx index d24613c8..1a307e33 100644 --- a/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -60,6 +60,24 @@ export default function PluginCardComponent({ > v{cardVO.version} + {cardVO.type && ( + + {cardVO.type === 'mcp' + ? 'MCP' + : cardVO.type === 'skill' + ? t('common.skill') + : t('market.typePlugin')} + + )} {cardVO.debug && ( (() => { const category = searchParams.get('category'); @@ -63,6 +65,13 @@ function MarketPageContent({ } return 'all'; }); + const [typeFilter, setTypeFilter] = useState(() => { + const type = searchParams.get('type'); + if (type && validTypes.includes(type)) { + return type; + } + return 'all'; + }); const [selectedTags, setSelectedTags] = useState([]); const [availableTags, setAvailableTags] = useState([]); const [tagNames, setTagNames] = useState>({}); @@ -136,6 +145,7 @@ function MarketPageContent({ version: plugin.latest_version, components: plugin.components, tags: plugin.tags || [], + type: plugin.type, }); }, []); @@ -152,6 +162,7 @@ function MarketPageContent({ const { sortBy, sortOrder } = getCurrentSort(); const filterValue = componentFilter === 'all' ? undefined : componentFilter; + const typeFilterValue = typeFilter === 'all' ? undefined : typeFilter; // Always use searchMarketplacePlugins to support component filtering and tags filtering const response = @@ -163,6 +174,7 @@ function MarketPageContent({ sortOrder, filterValue, selectedTags.length > 0 ? selectedTags : undefined, + typeFilterValue, ); const data: ApiRespMarketplacePlugins = response; @@ -313,10 +325,29 @@ function MarketPageContent({ // fetchPlugins will be called by useEffect when componentFilter changes }, []); + // Handle type filter change + const handleTypeFilterChange = useCallback((value: string) => { + setTypeFilter(value); + setCurrentPage(1); + setPlugins([]); + + // Update URL query param to keep it in sync + const params = new URLSearchParams(window.location.search); + if (value === 'all') { + params.delete('type'); + } else { + params.set('type', value); + } + const newUrl = params.toString() + ? `${window.location.pathname}?${params.toString()}` + : window.location.pathname; + window.history.replaceState({}, '', newUrl); + }, []); + // 当排序选项或组件筛选变化时重新加载数据 useEffect(() => { fetchPlugins(1, !!searchQuery.trim(), true); - }, [sortOption, componentFilter]); + }, [sortOption, componentFilter, typeFilter]); // Tags 筛选变化时重新搜索 useEffect(() => { @@ -534,6 +565,54 @@ function MarketPageContent({ + {/* Type filter */} +
+ + {t('market.filterByType')}: + +
+ { + if (value) handleTypeFilterChange(value); + }} + className="justify-start flex-nowrap" + > + + {t('market.allTypes')} + + + {t('market.typePlugin')} + + + {t('market.typeMCP')} + + + {t('market.typeSkill')} + + +
+
+ {/* Sort dropdown */}
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 20eafdde..a4849bd7 100644 --- a/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx +++ b/web/src/app/home/plugins/components/plugin-market/RecommendationLists.tsx @@ -38,6 +38,7 @@ function pluginToVO( version: plugin.latest_version, components: plugin.components, tags: plugin.tags || [], + type: plugin.type, }); } diff --git a/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx b/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx index 48368621..0f7ddabf 100644 --- a/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx @@ -107,6 +107,24 @@ export default function PluginMarketCardComponent({
{cardVO.label}
+ {cardVO.type && ( + + {cardVO.type === 'mcp' + ? 'MCP' + : cardVO.type === 'skill' + ? t('common.skill') + : t('market.typePlugin')} + + )}
diff --git a/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardVO.ts b/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardVO.ts index 50f40c0f..f1d66ae2 100644 --- a/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardVO.ts +++ b/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardVO.ts @@ -10,6 +10,7 @@ export interface IPluginMarketCardVO { version: string; components?: Record; tags?: string[]; + type?: 'plugin' | 'mcp' | 'skill'; } export class PluginMarketCardVO implements IPluginMarketCardVO { @@ -24,6 +25,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO { version: string; components?: Record; tags?: string[]; + type?: 'plugin' | 'mcp' | 'skill'; constructor(prop: IPluginMarketCardVO) { this.description = prop.description; @@ -37,5 +39,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO { this.version = prop.version; this.components = prop.components; this.tags = prop.tags; + this.type = prop.type; } } diff --git a/web/src/app/infra/entities/plugin/index.ts b/web/src/app/infra/entities/plugin/index.ts index 5e133404..6b2b4355 100644 --- a/web/src/app/infra/entities/plugin/index.ts +++ b/web/src/app/infra/entities/plugin/index.ts @@ -42,6 +42,7 @@ export interface PluginV4 { latest_version: string; components: Record; status: PluginV4Status; + type?: 'plugin' | 'mcp' | 'skill'; created_at: string; updated_at: string; } diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts index 5c08e2ee..9193d9bd 100644 --- a/web/src/app/infra/http/CloudServiceClient.ts +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -38,6 +38,7 @@ export class CloudServiceClient extends BaseHttpClient { sort_order?: string, component_filter?: string, tags_filter?: string[], + type_filter?: string, ): Promise { return this.post( '/api/v1/marketplace/plugins/search', @@ -49,6 +50,7 @@ export class CloudServiceClient extends BaseHttpClient { sort_order, component_filter, tags_filter, + type_filter, }, ); } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index fe50cbf9..fa2d8f99 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -36,6 +36,7 @@ const enUS = { delete: 'Delete', add: 'Add', select: 'Select', + skill: 'Skill', cancel: 'Cancel', submit: 'Submit', error: 'Error', @@ -617,6 +618,11 @@ const enUS = { markAsReadFailed: 'Mark as read failed', filterByComponent: 'Component', allComponents: 'All Components', + filterByType: 'Type', + allTypes: 'All Types', + typePlugin: 'Plugin', + typeMCP: 'MCP', + typeSkill: 'Skill', requestPlugin: 'Request Plugin', viewDetails: 'View Details', deprecated: 'Deprecated', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 21a535d7..0ae96ecb 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -38,6 +38,7 @@ const esES = { delete: 'Eliminar', add: 'Añadir', select: 'Seleccionar', + skill: 'Habilidad', cancel: 'Cancelar', submit: 'Enviar', error: 'Error', @@ -630,6 +631,11 @@ const esES = { markAsReadFailed: 'Error al marcar como leído', filterByComponent: 'Componente', allComponents: 'Todos los componentes', + filterByType: 'Tipo', + allTypes: 'Todos los tipos', + typePlugin: 'Plugin', + typeMCP: 'MCP', + typeSkill: 'Habilidad', requestPlugin: 'Solicitar plugin', viewDetails: 'Ver detalles', deprecated: 'Obsoleto', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 38d1bac5..15f55a68 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -1,4 +1,4 @@ -const jaJP = { +const jaJP = { sidebar: { home: 'ホーム', extensions: '拡張機能', @@ -37,6 +37,7 @@ delete: '削除', add: '追加', select: '選択してください', + skill: 'スキル', cancel: 'キャンセル', submit: '送信', error: 'エラー', @@ -622,6 +623,11 @@ markAsReadFailed: '既読に設定に失敗しました', filterByComponent: 'コンポーネント', allComponents: '全部コンポーネント', + filterByType: 'タイプ', + allTypes: '全部', + typePlugin: 'プラグイン', + typeMCP: 'MCP', + typeSkill: 'スキル', requestPlugin: 'プラグインをリクエスト', tags: { filterByTags: 'タグで絞り込み', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index be12f26e..6512c9fe 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -36,6 +36,7 @@ const ruRU = { delete: 'Удалить', add: 'Добавить', select: 'Выбрать', + skill: 'Навык', cancel: 'Отмена', submit: 'Отправить', error: 'Ошибка', @@ -627,6 +628,11 @@ const ruRU = { markAsReadFailed: 'Не удалось отметить как прочитанное', filterByComponent: 'Компонент', allComponents: 'Все компоненты', + filterByType: 'Тип', + allTypes: 'Все типы', + typePlugin: 'Плагин', + typeMCP: 'MCP', + typeSkill: 'Навык', requestPlugin: 'Запросить плагин', viewDetails: 'Подробнее', deprecated: 'Устаревший', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 8e881af0..772aa4a1 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -36,6 +36,7 @@ const thTH = { delete: 'ลบ', add: 'เพิ่ม', select: 'เลือก', + skill: 'สกิล', cancel: 'ยกเลิก', submit: 'ส่ง', error: 'ข้อผิดพลาด', @@ -609,6 +610,11 @@ const thTH = { markAsReadFailed: 'ทำเครื่องหมายว่าอ่านแล้วล้มเหลว', filterByComponent: 'ส่วนประกอบ', allComponents: 'ส่วนประกอบทั้งหมด', + filterByType: 'ประเภท', + allTypes: 'ทุกประเภท', + typePlugin: 'ปลั๊กอิน', + typeMCP: 'MCP', + typeSkill: 'สกิล', requestPlugin: 'ขอปลั๊กอิน', viewDetails: 'ดูรายละเอียด', deprecated: 'เลิกใช้แล้ว', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index 0ce3b55d..9dae9356 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -36,6 +36,7 @@ const viVN = { delete: 'Xóa', add: 'Thêm', select: 'Chọn', + skill: 'Kỹ năng', cancel: 'Hủy', submit: 'Gửi', error: 'Lỗi', @@ -621,6 +622,11 @@ const viVN = { markAsReadFailed: 'Đánh dấu đã đọc thất bại', filterByComponent: 'Thành phần', allComponents: 'Tất cả thành phần', + filterByType: 'Loại', + allTypes: 'Tất cả loại', + typePlugin: 'Plugin', + typeMCP: 'MCP', + typeSkill: 'Kỹ năng', requestPlugin: 'Yêu cầu Plugin', viewDetails: 'Xem chi tiết', deprecated: 'Không còn hỗ trợ', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index ebde3e85..4e173e64 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -35,6 +35,7 @@ const zhHans = { delete: '删除', add: '添加', select: '请选择', + skill: '技能', cancel: '取消', submit: '提交', error: '错误', @@ -590,6 +591,11 @@ const zhHans = { markAsReadFailed: '标记为已读失败', filterByComponent: '组件', allComponents: '全部组件', + filterByType: '类型', + allTypes: '全部类型', + typePlugin: '插件', + typeMCP: 'MCP', + typeSkill: '技能', requestPlugin: '请求插件', tags: { filterByTags: '按标签筛选', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index b68d0b4e..9824a706 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -35,6 +35,7 @@ const zhHant = { delete: '刪除', add: '新增', select: '請選擇', + skill: '技能', cancel: '取消', submit: '提交', error: '錯誤', @@ -590,6 +591,11 @@ const zhHant = { markAsReadFailed: '標記為已讀失敗', filterByComponent: '組件', allComponents: '全部組件', + filterByType: '類型', + allTypes: '全部類型', + typePlugin: '插件', + typeMCP: 'MCP', + typeSkill: '技能', requestPlugin: '請求插件', tags: { filterByTags: '按標籤篩選',