mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-07 22:36:02 +00:00
feat: add mcp and skills
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -38,6 +38,7 @@ function pluginToVO(
|
||||
version: plugin.latest_version,
|
||||
components: plugin.components,
|
||||
tags: plugin.tags || [],
|
||||
type: plugin.type,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'タグで絞り込み',
|
||||
|
||||
@@ -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: 'Устаревший',
|
||||
|
||||
@@ -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: 'เลิกใช้แล้ว',
|
||||
|
||||
@@ -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ợ',
|
||||
|
||||
@@ -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: '按标签筛选',
|
||||
|
||||
@@ -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: '按標籤篩選',
|
||||
|
||||
Reference in New Issue
Block a user