diff --git a/README_CN.md b/README_CN.md index 7dcdd4ca..2d23decb 100644 --- a/README_CN.md +++ b/README_CN.md @@ -25,7 +25,7 @@ 文档APICloud | -插件市场 | +拓展市场路线图 diff --git a/web/src/app/home/market/page.tsx b/web/src/app/home/market/page.tsx index e2158c53..3b093520 100644 --- a/web/src/app/home/market/page.tsx +++ b/web/src/app/home/market/page.tsx @@ -48,6 +48,7 @@ function MarketplaceContent() { } = usePluginInstallTasks(); const [modalOpen, setModalOpen] = useState(false); const [installInfo, setInstallInfo] = useState>({}); + const [installExtensionType, setInstallExtensionType] = useState<'plugin' | 'mcp' | 'skill'>('plugin'); const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.ASK_CONFIRM); const [installError, setInstallError] = useState(null); @@ -96,6 +97,7 @@ function MarketplaceContent() { plugin_name: plugin.name, plugin_version: plugin.latest_version, }); + setInstallExtensionType(plugin.type || 'plugin'); setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); setInstallError(null); setModalOpen(true); @@ -119,6 +121,7 @@ function MarketplaceContent() { taskId, pluginName: pluginDisplayName, source: 'marketplace', + extensionType: installExtensionType, }); setSelectedTaskId(taskKey); setModalOpen(false); diff --git a/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskContext.tsx b/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskContext.tsx index c7463b4b..a3f774e2 100644 --- a/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskContext.tsx +++ b/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskContext.tsx @@ -28,6 +28,7 @@ export interface PluginInstallTask { source: 'github' | 'marketplace' | 'local'; stage: InstallStage; overallProgress: number; // 0-100 + extensionType: 'plugin' | 'mcp' | 'skill'; // type of extension being installed fileSize?: number; // bytes, if known // Download progress downloadCurrent?: number; // bytes downloaded so far @@ -53,6 +54,7 @@ interface PluginInstallTaskContextValue { taskId: number; pluginName: string; source: 'github' | 'marketplace' | 'local'; + extensionType: 'plugin' | 'mcp' | 'skill'; fileSize?: number; }) => void; removeTask: (id: string) => void; @@ -131,7 +133,7 @@ function extractSourceFromName( * Check if a backend task name is a plugin install task. */ function isPluginInstallTask(name: string): boolean { - return name.startsWith('plugin-install-'); + return name.startsWith('plugin-install-') || name.startsWith('mcp-install-') || name.startsWith('skill-install-'); } /** @@ -165,13 +167,21 @@ function asyncTaskToPluginInstallTask(task: AsyncTask): PluginInstallTask { overallProgress = Math.min(95, stageToProgress(stage)); } - const pluginName = str(md.plugin_name) || task.label || `${source} plugin`; + const pluginName = str(md.plugin_name) || task.label || `${source} extension`; + + let extensionType: 'plugin' | 'mcp' | 'skill' = 'plugin'; + if (task.name.startsWith('mcp-install-')) { + extensionType = 'mcp'; + } else if (task.name.startsWith('skill-install-')) { + extensionType = 'skill'; + } return { id: `${source}-${task.id}`, taskId: task.id, pluginName, source, + extensionType, stage, overallProgress, downloadCurrent: num(md.download_current), @@ -388,6 +398,7 @@ export function PluginInstallTaskProvider({ converted.startedAt = existing.startedAt; converted.pluginName = existing.pluginName; converted.fileSize = existing.fileSize; + converted.extensionType = existing.extensionType; updatedTasks[idx] = converted; } } @@ -433,6 +444,7 @@ export function PluginInstallTaskProvider({ taskId: number; pluginName: string; source: 'github' | 'marketplace' | 'local'; + extensionType: 'plugin' | 'mcp' | 'skill'; fileSize?: number; }) => { const taskKey = `${params.source}-${params.taskId}`; @@ -445,6 +457,7 @@ export function PluginInstallTaskProvider({ taskId: params.taskId, pluginName: params.pluginName, source: params.source, + extensionType: params.extensionType, stage: InstallStage.DOWNLOADING, overallProgress: 5, fileSize: params.fileSize, diff --git a/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskQueue.tsx b/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskQueue.tsx index 5aa95c19..27e5be0d 100644 --- a/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskQueue.tsx +++ b/web/src/app/home/plugins/components/plugin-install-task/PluginInstallTaskQueue.tsx @@ -11,6 +11,9 @@ import { Loader2, X, ListTodo, + Wrench, + AudioWaveform, + Book, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { @@ -35,6 +38,12 @@ const STAGE_ICONS: Record = { [InstallStage.ERROR]: XCircle, }; +const EXTENSION_TYPE_ICONS: Record = { + plugin: Wrench, + mcp: AudioWaveform, + skill: Book, +}; + function TaskQueueItem({ task, onClick, @@ -49,6 +58,40 @@ function TaskQueueItem({ const isError = task.stage === InstallStage.ERROR; const isRunning = !isDone && !isError; const StageIcon = STAGE_ICONS[task.stage] || Download; + const TypeIcon = EXTENSION_TYPE_ICONS[task.extensionType] || Wrench; + + const getTypeBadgeClass = () => { + switch (task.extensionType) { + case 'mcp': + return 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'; + case 'skill': + return 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'; + default: + return 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'; + } + }; + + const getTypeLabel = () => { + switch (task.extensionType) { + case 'mcp': + return 'MCP'; + case 'skill': + return t('common.skill'); + default: + return t('market.typePlugin'); + } + }; + + const getInstallCompleteMessage = () => { + switch (task.extensionType) { + case 'mcp': + return t('plugins.installProgress.installCompleteMCP'); + case 'skill': + return t('plugins.installProgress.installCompleteSkill'); + default: + return t('plugins.installProgress.installCompletePlugin'); + } + }; const stageLabel = (() => { switch (task.stage) { @@ -61,7 +104,7 @@ function TaskQueueItem({ case InstallStage.LAUNCHING: return t('plugins.installProgress.launching'); case InstallStage.DONE: - return t('plugins.installProgress.completed'); + return isDone ? getInstallCompleteMessage() : t('plugins.installProgress.completed'); case InstallStage.ERROR: return t('plugins.installProgress.failed'); default: @@ -93,7 +136,16 @@ function TaskQueueItem({
-
{task.pluginName}
+
+
{task.pluginName}
+ + + {getTypeLabel()} + +
{stageLabel} {isRunning && ( diff --git a/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx new file mode 100644 index 00000000..871fe0fe --- /dev/null +++ b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx @@ -0,0 +1,317 @@ +import { ExtensionCardVO, ExtensionType } from './ExtensionCardVO'; +import { useState } from 'react'; +import { Badge } from '@/components/ui/badge'; +import { useTranslation } from 'react-i18next'; +import { BugIcon, ExternalLink, Ellipsis, Trash, ArrowUp } from 'lucide-react'; +import { getCloudServiceClientSync, systemInfo } from '@/app/infra/http'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + MCPSessionStatus, +} from '@/app/infra/entities/api'; + +type ExtensionCardComponentProps = { + cardVO: ExtensionCardVO; + onCardClick: () => void; + onDeleteClick: (cardVO: ExtensionCardVO) => void; + onUpgradeClick?: (cardVO: ExtensionCardVO) => void; +}; + +export default function ExtensionCardComponent({ + cardVO, + onCardClick, + onDeleteClick, + onUpgradeClick, +}: ExtensionCardComponentProps) { + const { t } = useTranslation(); + const [dropdownOpen, setDropdownOpen] = useState(false); + + const getTypeBadgeColor = (type: ExtensionType) => { + switch (type) { + case 'mcp': + return 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'; + case 'skill': + return 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'; + default: + return 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'; + } + }; + + const getTypeLabel = (type: ExtensionType) => { + switch (type) { + case 'mcp': + return 'MCP'; + case 'skill': + return t('common.skill'); + default: + return t('market.typePlugin'); + } + }; + + const getStatusColor = (status?: string) => { + switch (status) { + case MCPSessionStatus.CONNECTED: + return 'text-green-600'; + case MCPSessionStatus.CONNECTING: + return 'text-yellow-600'; + case MCPSessionStatus.ERROR: + return 'text-red-600'; + default: + return 'text-gray-600'; + } + }; + + const renderPluginContent = () => ( + <> +
+ {cardVO.author} / {cardVO.name} +
+
+
+ {cardVO.label} +
+ + v{cardVO.version} + + + {getTypeLabel(cardVO.type)} + + {cardVO.debug && ( + + + {t('plugins.debugging')} + + )} + {!cardVO.debug && ( + <> + {cardVO.install_source === 'github' && ( + + {t('plugins.fromGithub')} + + )} + {cardVO.install_source === 'local' && ( + + {t('plugins.fromLocal')} + + )} + {cardVO.install_source === 'marketplace' && ( + + {t('plugins.fromMarketplace')} + + )} + + )} +
+
+ {cardVO.description} +
+ + ); + + const renderMCPContent = () => ( + <> +
+ MCP Server +
+
+
+ {cardVO.label} +
+ + MCP + + {cardVO.mode && ( + + {cardVO.mode.toUpperCase()} + + )} + + {cardVO.enabled ? t('mcp.statusConnected') : t('mcp.statusDisabled')} + +
+
+ {cardVO.description || t('mcp.noToolsFound')} + {cardVO.tools !== undefined && cardVO.tools > 0 && ( + {t('mcp.toolCount', { count: cardVO.tools })} + )} +
+ + ); + + const renderSkillContent = () => ( + <> +
+ Skill +
+
+
+ {cardVO.label} +
+ + {t('common.skill')} + +
+
+ {cardVO.description} +
+ + ); + + return ( + <> +
onCardClick()} + > +
+ extension icon + +
+
+ {cardVO.type === 'plugin' && renderPluginContent()} + {cardVO.type === 'mcp' && renderMCPContent()} + {cardVO.type === 'skill' && renderSkillContent()} +
+
+ +
e.stopPropagation()} + > +
+ +
+ { + setDropdownOpen(open); + }} + > + +
+ + {cardVO.hasUpdate && ( +
+ )} +
+
+ + {cardVO.type === 'plugin' && cardVO.install_source === 'marketplace' && ( + { + e.stopPropagation(); + if (onUpgradeClick) { + onUpgradeClick(cardVO); + } + setDropdownOpen(false); + }} + > + + {t('plugins.update')} + {cardVO.hasUpdate && ( + + {t('plugins.new')} + + )} + + )} + {cardVO.type === 'plugin' && (cardVO.install_source === 'github' || cardVO.install_source === 'marketplace') && ( + { + e.stopPropagation(); + if (cardVO.install_source === 'github') { + window.open(cardVO.install_info?.github_url as string, '_blank'); + } else if (cardVO.install_source === 'marketplace') { + window.open( + getCloudServiceClientSync().getPluginMarketplaceURL( + systemInfo.cloud_service_url, + cardVO.author, + cardVO.name, + ), + '_blank', + ); + } + setDropdownOpen(false); + }} + > + + {t('plugins.viewSource')} + + )} + { + e.stopPropagation(); + onDeleteClick(cardVO); + setDropdownOpen(false); + }} + > + + + {cardVO.type === 'mcp' + ? t('mcp.deleteServer') + : cardVO.type === 'skill' + ? t('skills.delete') + : t('plugins.delete')} + + + +
+
+
+
+
+ + ); +} \ No newline at end of file diff --git a/web/src/app/home/plugins/components/plugin-installed/ExtensionCardVO.ts b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardVO.ts new file mode 100644 index 00000000..19511982 --- /dev/null +++ b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardVO.ts @@ -0,0 +1,61 @@ +export type ExtensionType = 'plugin' | 'mcp' | 'skill'; + +export interface IExtensionCardVO { + id: string; + author: string; + label: string; + name: string; + description: string; + version: string; + enabled: boolean; + type: ExtensionType; + iconURL?: string; + install_source?: string; + install_info?: Record; + status?: string; + debug?: boolean; + hasUpdate?: boolean; + runtimeStatus?: 'connecting' | 'connected' | 'error' | 'disabled'; + tools?: number; + mode?: 'stdio' | 'sse' | 'http'; +} + +export class ExtensionCardVO implements IExtensionCardVO { + id: string; + author: string; + label: string; + name: string; + description: string; + version: string; + enabled: boolean; + type: ExtensionType; + iconURL?: string; + install_source?: string; + install_info?: Record; + status?: string; + debug?: boolean; + hasUpdate?: boolean; + runtimeStatus?: 'connecting' | 'connected' | 'error' | 'disabled'; + tools?: number; + mode?: 'stdio' | 'sse' | 'http'; + + constructor(prop: IExtensionCardVO) { + this.id = prop.id; + this.author = prop.author; + this.label = prop.label; + this.name = prop.name; + this.description = prop.description; + this.version = prop.version; + this.enabled = prop.enabled; + this.type = prop.type; + this.iconURL = prop.iconURL; + this.install_source = prop.install_source; + this.install_info = prop.install_info; + this.status = prop.status; + this.debug = prop.debug; + this.hasUpdate = prop.hasUpdate; + this.runtimeStatus = prop.runtimeStatus; + this.tools = prop.tools; + this.mode = prop.mode; + } +} \ No newline at end of file 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 e82f921e..8cb21894 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useNavigate } from 'react-router-dom'; -import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO'; -import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent'; +import { ExtensionCardVO, ExtensionType } from './ExtensionCardVO'; +import ExtensionCardComponent from './ExtensionCardComponent'; import styles from '@/app/home/plugins/plugins.module.css'; import { httpClient } from '@/app/infra/http/HttpClient'; import { getCloudServiceClientSync } from '@/app/infra/http'; @@ -22,42 +22,59 @@ import { toast } from 'sonner'; import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { Puzzle } from 'lucide-react'; +import { + ToggleGroup, + ToggleGroupItem, +} from '@/components/ui/toggle-group'; +import { Wrench, AudioWaveform, Book } from 'lucide-react'; +import { MCPSessionStatus } from '@/app/infra/entities/api'; export interface PluginInstalledComponentRef { refreshPluginList: () => void; } -enum PluginOperationType { +enum ExtensionOperationType { DELETE = 'DELETE', UPDATE = 'UPDATE', } +type FilterType = 'all' | ExtensionType; + +const FilterOptions = [ + { value: 'all' as FilterType, labelKey: 'market.filters.allFormats', icon: null }, + { value: 'plugin' as FilterType, labelKey: 'market.typePlugin', icon: Wrench }, + { value: 'mcp' as FilterType, labelKey: 'market.typeMCP', icon: AudioWaveform }, + { value: 'skill' as FilterType, labelKey: 'market.typeSkill', icon: Book }, +]; + const PluginInstalledComponent = forwardRef( (props, ref) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { refreshPlugins } = useSidebarData(); - const [pluginList, setPluginList] = useState([]); + const { refreshPlugins, refreshMCPServers, refreshSkills } = useSidebarData(); + const [extensionList, setExtensionList] = useState([]); + const [filterType, setFilterType] = useState('all'); const [showOperationModal, setShowOperationModal] = useState(false); - const [operationType, setOperationType] = useState( - PluginOperationType.DELETE, + const [operationType, setOperationType] = useState( + ExtensionOperationType.DELETE, ); - const [targetPlugin, setTargetPlugin] = useState(null); + const [targetExtension, setTargetExtension] = useState(null); const [deleteData, setDeleteData] = useState(false); const asyncTask = useAsyncTask({ onSuccess: () => { const successMessage = - operationType === PluginOperationType.DELETE + operationType === ExtensionOperationType.DELETE ? t('plugins.deleteSuccess') : t('plugins.updateSuccess'); toast.success(successMessage); setShowOperationModal(false); - getPluginList(); + getExtensionList(); refreshPlugins(); + refreshMCPServers(); + refreshSkills(); }, onError: () => { - // Error is already handled in the hook state }, }); @@ -66,131 +83,171 @@ const PluginInstalledComponent = forwardRef( }, []); function initData() { - getPluginList(); + getExtensionList(); } - async function getPluginList() { + async function getExtensionList() { try { - // 获取已安装插件列表 - const installedPluginsResp = await httpClient.getPlugins(); - const installedPlugins = installedPluginsResp.plugins; - - // 获取市场插件列表 const client = getCloudServiceClientSync(); - const marketplaceResp = await client.getMarketplacePlugins(1, 100); - const marketplacePlugins = marketplaceResp.plugins; - // 创建市场插件映射,便于快速查找 - const marketplacePluginMap = new Map(); - marketplacePlugins.forEach((plugin) => { + const [installedPluginsResp, marketplaceResp, mcpResp, skillsResp] = await Promise.all([ + httpClient.getPlugins().catch(() => ({ plugins: [] })), + client.getMarketplacePlugins(1, 100).catch(() => ({ plugins: [] })), + httpClient.getMCPServers().catch(() => ({ servers: [] })), + httpClient.getSkills().catch(() => ({ skills: [] })), + ]); + + const marketplacePluginMap = new Map(); + marketplaceResp.plugins.forEach((plugin: any) => { const key = `${plugin.author}/${plugin.name}`; marketplacePluginMap.set(key, plugin); }); - // 转换并比较版本号 - 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), - description: extractI18nObject( - plugin.manifest.manifest.metadata.description ?? { - en_US: '', - zh_Hans: '', - }, - ), - debug: plugin.debug, - enabled: plugin.enabled, - name: plugin.manifest.manifest.metadata.name, - version: plugin.manifest.manifest.metadata.version ?? '', - status: plugin.status, - components: plugin.components, - priority: plugin.priority, - install_source: plugin.install_source, - install_info: plugin.install_info, - type: marketplacePlugin?.type, - }); + const extensions: ExtensionCardVO[] = []; - // 检查是否来自市场且有更新 - if (cardVO.install_source === 'marketplace' && marketplacePlugin) { + for (const plugin of installedPluginsResp.plugins) { + const meta = plugin.manifest.manifest.metadata; + const author = meta.author ?? ''; + const name = meta.name; + const marketplaceKey = `${author}/${name}`; + const marketplacePlugin = marketplacePluginMap.get(marketplaceKey); + + let hasUpdate = false; + if (plugin.install_source === 'marketplace' && marketplacePlugin) { if (marketplacePlugin.latest_version) { - cardVO.hasUpdate = isNewerVersion( + hasUpdate = isNewerVersion( marketplacePlugin.latest_version, - cardVO.version, + meta.version ?? '', ); } } - return cardVO; - }); + extensions.push(new ExtensionCardVO({ + id: marketplaceKey, + author, + label: extractI18nObject(meta.label) || name, + name, + description: extractI18nObject(meta.description ?? { en_US: '', zh_Hans: '' }), + version: meta.version ?? '', + enabled: plugin.enabled, + type: marketplacePlugin?.type || 'plugin', + iconURL: httpClient.getPluginIconURL(author, name), + install_source: plugin.install_source, + install_info: plugin.install_info, + status: plugin.status, + debug: plugin.debug, + hasUpdate, + })); + } - setPluginList(pluginCards); + for (const server of mcpResp.servers) { + extensions.push(new ExtensionCardVO({ + id: `mcp-${server.name}`, + author: '', + label: server.name.replace(/__/g, '/'), + name: server.name, + description: '', + version: '', + enabled: server.enable, + type: 'mcp', + iconURL: httpClient.getPluginIconURL('mcp', server.name), + status: server.runtime_info?.status, + runtimeStatus: server.runtime_info?.status, + tools: server.runtime_info?.tool_count || 0, + mode: server.mode, + })); + } + + for (const skill of skillsResp.skills) { + extensions.push(new ExtensionCardVO({ + id: `skill-${skill.name}`, + author: '', + label: skill.display_name || skill.name, + name: skill.name, + description: skill.description || '', + version: '', + enabled: true, + type: 'skill', + iconURL: httpClient.getPluginIconURL('skill', skill.name), + })); + } + + setExtensionList(extensions); } catch (error) { - console.error('获取插件列表失败:', error); - // 失败时仍显示已安装插件,不影响用户体验 - const installedPluginsResp = await httpClient.getPlugins(); - setPluginList( - installedPluginsResp.plugins.map((plugin) => { - return new PluginCardVO({ - author: plugin.manifest.manifest.metadata.author ?? '', - label: extractI18nObject(plugin.manifest.manifest.metadata.label), - description: extractI18nObject( - plugin.manifest.manifest.metadata.description ?? { - en_US: '', - zh_Hans: '', - }, - ), - debug: plugin.debug, - enabled: plugin.enabled, - name: plugin.manifest.manifest.metadata.name, - version: plugin.manifest.manifest.metadata.version ?? '', - status: plugin.status, - components: plugin.components, - priority: plugin.priority, - install_source: plugin.install_source, - install_info: plugin.install_info, - }); - }), - ); + console.error('Failed to fetch extension list:', error); + setExtensionList([]); } } useImperativeHandle(ref, () => ({ - refreshPluginList: getPluginList, + refreshPluginList: getExtensionList, })); - function handlePluginClick(plugin: PluginCardVO) { - const pluginId = `${plugin.author}/${plugin.name}`; - navigate(`/home/plugins?id=${encodeURIComponent(pluginId)}`); + function handleExtensionClick(extension: ExtensionCardVO) { + if (extension.type === 'mcp') { + navigate(`/home/mcp`); + } else if (extension.type === 'skill') { + navigate(`/home/skills`); + } else { + const extensionId = `${extension.author}/${extension.name}`; + navigate(`/home/plugins?id=${encodeURIComponent(extensionId)}`); + } } - function handlePluginDelete(plugin: PluginCardVO) { - setTargetPlugin(plugin); - setOperationType(PluginOperationType.DELETE); + function handleExtensionDelete(extension: ExtensionCardVO) { + setTargetExtension(extension); + setOperationType(ExtensionOperationType.DELETE); setShowOperationModal(true); setDeleteData(false); asyncTask.reset(); } - function handlePluginUpdate(plugin: PluginCardVO) { - setTargetPlugin(plugin); - setOperationType(PluginOperationType.UPDATE); + function handleExtensionUpdate(extension: ExtensionCardVO) { + setTargetExtension(extension); + setOperationType(ExtensionOperationType.UPDATE); setShowOperationModal(true); asyncTask.reset(); } function executeOperation() { - if (!targetPlugin) return; + if (!targetExtension) return; + + if (targetExtension.type === 'mcp') { + httpClient.deleteMCPServer(targetExtension.name) + .then(() => { + toast.success(t('mcp.deleteSuccess')); + setShowOperationModal(false); + getExtensionList(); + refreshMCPServers(); + }) + .catch((error) => { + toast.error(t('mcp.deleteError') + error.message); + }); + return; + } + + if (targetExtension.type === 'skill') { + httpClient.deleteSkill(targetExtension.name) + .then(() => { + toast.success(t('skills.deleteSuccess')); + setShowOperationModal(false); + getExtensionList(); + refreshSkills(); + }) + .catch((error) => { + toast.error(t('skills.deleteError') + error.message); + }); + return; + } const apiCall = - operationType === PluginOperationType.DELETE + operationType === ExtensionOperationType.DELETE ? httpClient.removePlugin( - targetPlugin.author, - targetPlugin.name, + targetExtension.author, + targetExtension.name, deleteData, ) - : httpClient.upgradePlugin(targetPlugin.author, targetPlugin.name); + : httpClient.upgradePlugin(targetExtension.author, targetExtension.name); apiCall .then((res) => { @@ -198,13 +255,32 @@ const PluginInstalledComponent = forwardRef( }) .catch((error) => { const errorMessage = - operationType === PluginOperationType.DELETE + operationType === ExtensionOperationType.DELETE ? t('plugins.deleteError') + error.message : t('plugins.updateError') + error.message; toast.error(errorMessage); }); } + const filteredExtensions = extensionList.filter((ext) => { + if (filterType === 'all') return true; + return ext.type === filterType; + }); + + const getDeleteConfirmMessage = () => { + if (!targetExtension) return ''; + if (targetExtension.type === 'mcp') { + return t('mcp.confirmDeleteServer'); + } + if (targetExtension.type === 'skill') { + return t('skills.deleteConfirmation'); + } + return t('plugins.confirmDeletePlugin', { + author: targetExtension.author, + name: targetExtension.name, + }); + }; + return ( <> ( onOpenChange={(open) => { if (!open) { setShowOperationModal(false); - setTargetPlugin(null); + setTargetExtension(null); asyncTask.reset(); } }} @@ -220,7 +296,7 @@ const PluginInstalledComponent = forwardRef( - {operationType === PluginOperationType.DELETE + {operationType === ExtensionOperationType.DELETE ? t('plugins.deleteConfirm') : t('plugins.updateConfirm')} @@ -228,18 +304,8 @@ const PluginInstalledComponent = forwardRef( {asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
-
- {operationType === PluginOperationType.DELETE - ? t('plugins.confirmDeletePlugin', { - author: targetPlugin?.author ?? '', - name: targetPlugin?.name ?? '', - }) - : t('plugins.confirmUpdatePlugin', { - author: targetPlugin?.author ?? '', - name: targetPlugin?.name ?? '', - })} -
- {operationType === PluginOperationType.DELETE && ( +
{getDeleteConfirmMessage()}
+ {operationType === ExtensionOperationType.DELETE && targetExtension?.type === 'plugin' && (
( )} {asyncTask.status === AsyncTaskStatus.RUNNING && (
- {operationType === PluginOperationType.DELETE + {operationType === ExtensionOperationType.DELETE ? t('plugins.deleting') : t('plugins.updating')}
)} {asyncTask.status === AsyncTaskStatus.ERROR && (
- {operationType === PluginOperationType.DELETE + {operationType === ExtensionOperationType.DELETE ? t('plugins.deleteError') : t('plugins.updateError')}
{asyncTask.error}
@@ -280,7 +346,7 @@ const PluginInstalledComponent = forwardRef( variant="outline" onClick={() => { setShowOperationModal(false); - setTargetPlugin(null); + setTargetExtension(null); asyncTask.reset(); }} > @@ -290,7 +356,7 @@ const PluginInstalledComponent = forwardRef( {asyncTask.status === AsyncTaskStatus.WAIT_INPUT && ( @@ -306,13 +372,13 @@ const PluginInstalledComponent = forwardRef( {asyncTask.status === AsyncTaskStatus.RUNNING && ( @@ -332,21 +398,44 @@ const PluginInstalledComponent = forwardRef(
- {pluginList.length === 0 ? ( +
+ { + if (value) setFilterType(value as FilterType); + }} + className="justify-start" + > + {FilterOptions.map((option) => ( + + {option.icon && } + {t(option.labelKey)} + + ))} + +
+ + {filteredExtensions.length === 0 ? (
{t('plugins.noPluginInstalled')}
) : (
- {pluginList.map((vo, index) => { + {filteredExtensions.map((vo, index) => { return ( -
- + handlePluginClick(vo)} - onDeleteClick={() => handlePluginDelete(vo)} - onUpgradeClick={() => handlePluginUpdate(vo)} + onCardClick={() => handleExtensionClick(vo)} + onDeleteClick={() => handleExtensionDelete(vo)} + onUpgradeClick={vo.type === 'plugin' ? () => handleExtensionUpdate(vo) : undefined} />
); @@ -358,4 +447,4 @@ const PluginInstalledComponent = forwardRef( }, ); -export default PluginInstalledComponent; +export default PluginInstalledComponent; \ No newline at end of file diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 05099871..347f7d92 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -315,6 +315,7 @@ function PluginListView() { taskId, pluginName: pluginDisplayName, source: 'github', + extensionType: 'plugin', fileSize: assetSize, }); setSelectedTaskId(taskKey); @@ -337,6 +338,7 @@ function PluginListView() { taskId, pluginName: fileName, source: 'local', + extensionType: 'plugin', fileSize: fileSize, }); setSelectedTaskId(taskKey); diff --git a/web/src/app/infra/http/README.md b/web/src/app/infra/http/README.md index 2a2e976b..ff7b2842 100644 --- a/web/src/app/infra/http/README.md +++ b/web/src/app/infra/http/README.md @@ -8,7 +8,7 @@ HTTP Client 已经重构为更清晰的架构,将通用方法与业务逻辑 - **BaseHttpClient.ts** - 基础 HTTP 客户端类,包含所有通用的 HTTP 方法和拦截器配置 - **BackendClient.ts** - 后端服务客户端,处理与后端 API 的所有交互 -- **CloudServiceClient.ts** - 云服务客户端,处理与 cloud service 的交互(如插件市场) +- **CloudServiceClient.ts** - 云服务客户端,处理与 cloud service 的交互(如拓展市场) - **index.ts** - 主入口文件,管理客户端实例的创建和导出 - **HttpClient.ts** - 仅用于向后兼容的文件(已废弃) diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index e3fe15c6..d985f36c 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -2,8 +2,8 @@ const enUS = { sidebar: { home: 'Home', extensions: 'Extensions', - installedPlugins: 'Installed Plugins', - pluginMarket: 'Marketplace', + installedPlugins: 'Installed Extensions', + pluginMarket: 'Extension Market', mcpServers: 'MCP Servers', pluginPages: 'Plugin Pages', pluginPagesTooltip: 'Visual pages provided by installed plugins', @@ -429,7 +429,7 @@ const enUS = { createPlugin: 'Create Plugin', editPlugin: 'Edit Plugin', installed: 'Installed', - marketplace: 'Marketplace', + marketplace: 'Extension Market', arrange: 'Sort Plugins', install: 'Install', installPlugin: 'Install Plugin', @@ -571,22 +571,28 @@ const enUS = { assetSize: 'Size: {{size}}', confirmInstall: 'Confirm Install', installFromGithubDesc: 'Install plugin from GitHub Release', - goToMarketplace: 'Go to Marketplace', + goToMarketplace: 'Go to Extension Market', installProgress: { title: 'Installing {{name}}', - titleGeneric: 'Plugin Installation', + titleGeneric: 'Extension Installation', + titlePlugin: 'Installing Plugin {{name}}', + titleMCP: 'Installing MCP Server {{name}}', + titleSkill: 'Installing Skill {{name}}', overallProgress: 'Overall Progress', - downloading: 'Downloading Plugin', + downloading: 'Downloading', installingDeps: 'Installing Dependencies', initializing: 'Initializing Settings', - launching: 'Launching Plugin', + launching: 'Launching', completed: 'Completed', failed: 'Failed', downloadSize: 'Package size: {{size}}', depsInfo: '{{count}} dependencies to install', depsProgress: '{{installed}}/{{total}} installed · {{remaining}} remaining', - installComplete: 'Plugin installed successfully', + installComplete: 'Installation successful', + installCompletePlugin: 'Plugin installed successfully', + installCompleteMCP: 'MCP Server installed successfully', + installCompleteSkill: 'Skill installed successfully', dismiss: 'Dismiss', background: 'Run in Background', taskQueue: 'Install Tasks', @@ -935,7 +941,7 @@ const enUS = { builtInParser: 'Provided by Knowledge engine', noParserAvailable: 'No parser supports this file type. Please install a parser plugin that can handle this format.', - installParserHint: 'Browse parser plugins in Marketplace →', + installParserHint: 'Browse parser plugins in Extension Market →', confirmUpload: 'Upload', cancelUpload: 'Cancel', }, diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 300a3c71..9955e58e 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -2,8 +2,8 @@ const zhHans = { sidebar: { home: '首页', extensions: '扩展', - installedPlugins: '已安装插件', - pluginMarket: '插件市场', + installedPlugins: '已安装扩展', + pluginMarket: '拓展市场', mcpServers: 'MCP 服务器', pluginPages: '插件页面', pluginPagesTooltip: '由已安装的插件提供的可视化页面', @@ -412,7 +412,7 @@ const zhHans = { createPlugin: '创建插件', editPlugin: '编辑插件', installed: '已安装', - marketplace: '插件市场', + marketplace: '拓展市场', arrange: '编排', install: '安装', installPlugin: '安装插件', @@ -546,21 +546,27 @@ const zhHans = { assetSize: '大小: {{size}}', confirmInstall: '确认安装', installFromGithubDesc: '从 GitHub Release 安装插件', - goToMarketplace: '前往插件市场', + goToMarketplace: '前往拓展市场', installProgress: { title: '正在安装 {{name}}', - titleGeneric: '插件安装', + titleGeneric: '扩展安装', + titlePlugin: '正在安装插件 {{name}}', + titleMCP: '正在安装 MCP 服务器 {{name}}', + titleSkill: '正在安装技能 {{name}}', overallProgress: '总体进度', - downloading: '下载插件', + downloading: '下载中', installingDeps: '安装依赖', initializing: '初始化配置', - launching: '启动插件', + launching: '启动中', completed: '已完成', failed: '安装失败', downloadSize: '包大小: {{size}}', depsInfo: '共 {{count}} 个依赖需要安装', depsProgress: '已安装 {{installed}}/{{total}} · 剩余 {{remaining}} 个', - installComplete: '插件安装成功', + installComplete: '安装成功', + installCompletePlugin: '插件安装成功', + installCompleteMCP: 'MCP 服务器安装成功', + installCompleteSkill: '技能安装成功', dismiss: '关闭', background: '后台运行', taskQueue: '安装任务', @@ -895,7 +901,7 @@ const zhHans = { builtInParser: '由知识引擎提供', noParserAvailable: '没有解析器支持此文件类型,请安装支持该格式的解析器插件。', - installParserHint: '前往插件市场安装解析器 →', + installParserHint: '前往拓展市场安装解析器 →', confirmUpload: '上传', cancelUpload: '取消', },