feat: add mcp and skills

This commit is contained in:
WangCham
2026-05-02 17:38:18 +08:00
parent a8fba46040
commit 7c50aabe65
17 changed files with 180 additions and 6 deletions

View File

@@ -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;
}
}

View File

@@ -88,6 +88,8 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
// 转换并比较版本号
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<PluginInstalledComponentRef>(
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,

View File

@@ -60,6 +60,24 @@ export default function PluginCardComponent({
>
v{cardVO.version}
</Badge>
{cardVO.type && (
<Badge
variant="outline"
className={`text-[0.7rem] flex-shrink-0 ${
cardVO.type === 'mcp'
? 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'
: cardVO.type === 'skill'
? 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'
: 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'
}`}
>
{cardVO.type === 'mcp'
? 'MCP'
: cardVO.type === 'skill'
? t('common.skill')
: t('market.typePlugin')}
</Badge>
)}
{cardVO.debug && (
<Badge
variant="outline"

View File

@@ -55,6 +55,8 @@ function MarketPageContent({
'Parser',
];
const validTypes = ['plugin', 'mcp', 'skill'];
const [searchQuery, setSearchQuery] = useState('');
const [componentFilter, setComponentFilter] = useState<string>(() => {
const category = searchParams.get('category');
@@ -63,6 +65,13 @@ function MarketPageContent({
}
return 'all';
});
const [typeFilter, setTypeFilter] = useState<string>(() => {
const type = searchParams.get('type');
if (type && validTypes.includes(type)) {
return type;
}
return 'all';
});
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
const [tagNames, setTagNames] = useState<Record<string, string>>({});
@@ -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({
</div>
</div>
{/* Type filter */}
<div className="flex flex-col sm:flex-row items-center gap-2 min-w-0 max-w-full">
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
{t('market.filterByType')}:
</span>
<div className="overflow-x-auto max-w-full [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
<ToggleGroup
type="single"
spacing={2}
size="sm"
value={typeFilter}
onValueChange={(value) => {
if (value) handleTypeFilterChange(value);
}}
className="justify-start flex-nowrap"
>
<ToggleGroupItem
value="all"
aria-label="All types"
className="text-xs sm:text-sm cursor-pointer"
>
{t('market.allTypes')}
</ToggleGroupItem>
<ToggleGroupItem
value="plugin"
aria-label="Plugin"
className="text-xs sm:text-sm cursor-pointer"
>
{t('market.typePlugin')}
</ToggleGroupItem>
<ToggleGroupItem
value="mcp"
aria-label="MCP"
className="text-xs sm:text-sm cursor-pointer"
>
{t('market.typeMCP')}
</ToggleGroupItem>
<ToggleGroupItem
value="skill"
aria-label="Skill"
className="text-xs sm:text-sm cursor-pointer"
>
{t('market.typeSkill')}
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
{/* Sort dropdown */}
<div className="flex items-center gap-2 sm:gap-3">
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">

View File

@@ -38,6 +38,7 @@ function pluginToVO(
version: plugin.latest_version,
components: plugin.components,
tags: plugin.tags || [],
type: plugin.type,
});
}

View File

@@ -107,6 +107,24 @@ export default function PluginMarketCardComponent({
<div className="text-base sm:text-[1.2rem] text-black dark:text-[#f0f0f0] truncate">
{cardVO.label}
</div>
{cardVO.type && (
<Badge
variant="outline"
className={`text-[0.6rem] px-1.5 py-0 h-4 flex-shrink-0 gap-0.5 ${
cardVO.type === 'mcp'
? 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'
: cardVO.type === 'skill'
? 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'
: 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'
}`}
>
{cardVO.type === 'mcp'
? 'MCP'
: cardVO.type === 'skill'
? t('common.skill')
: t('market.typePlugin')}
</Badge>
)}
</div>
</div>

View File

@@ -10,6 +10,7 @@ export interface IPluginMarketCardVO {
version: string;
components?: Record<string, number>;
tags?: string[];
type?: 'plugin' | 'mcp' | 'skill';
}
export class PluginMarketCardVO implements IPluginMarketCardVO {
@@ -24,6 +25,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
version: string;
components?: Record<string, number>;
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;
}
}

View File

@@ -42,6 +42,7 @@ export interface PluginV4 {
latest_version: string;
components: Record<string, number>;
status: PluginV4Status;
type?: 'plugin' | 'mcp' | 'skill';
created_at: string;
updated_at: string;
}

View File

@@ -38,6 +38,7 @@ export class CloudServiceClient extends BaseHttpClient {
sort_order?: string,
component_filter?: string,
tags_filter?: string[],
type_filter?: string,
): Promise<ApiRespMarketplacePlugins> {
return this.post<ApiRespMarketplacePlugins>(
'/api/v1/marketplace/plugins/search',
@@ -49,6 +50,7 @@ export class CloudServiceClient extends BaseHttpClient {
sort_order,
component_filter,
tags_filter,
type_filter,
},
);
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: 'タグで絞り込み',

View File

@@ -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: 'Устаревший',

View File

@@ -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: 'เลิกใช้แล้ว',

View File

@@ -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ợ',

View File

@@ -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: '按标签筛选',

View File

@@ -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: '按標籤篩選',