From fffc862fe6dd69ccdec46c320fefa75fe3408838 Mon Sep 17 00:00:00 2001 From: WangCham <651122857@qq.com> Date: Sat, 9 May 2026 11:49:44 +0800 Subject: [PATCH] feat: refactor market --- src/langbot/pkg/plugin/connector.py | 48 +- web/src/app/home/add-extension/page.tsx | 239 +++++++ web/src/app/home/add-plugin/page.tsx | 586 ++++++++++++++++++ .../components/home-sidebar/HomeSidebar.tsx | 1 - .../home-sidebar/sidbarConfigList.tsx | 39 +- web/src/app/home/mcp/MCPDetailContent.tsx | 7 + .../PluginInstalledComponent.tsx | 8 +- web/src/app/home/plugins/page.tsx | 48 -- web/src/app/home/skills/page.tsx | 42 +- web/src/i18n/locales/en-US.ts | 8 +- web/src/i18n/locales/es-ES.ts | 3 + web/src/i18n/locales/ja-JP.ts | 3 + web/src/i18n/locales/ru-RU.ts | 3 + web/src/i18n/locales/th-TH.ts | 3 + web/src/i18n/locales/vi-VN.ts | 3 + web/src/i18n/locales/zh-Hans.ts | 10 +- web/src/i18n/locales/zh-Hant.ts | 64 ++ web/src/router.tsx | 22 + 18 files changed, 1021 insertions(+), 116 deletions(-) create mode 100644 web/src/app/home/add-extension/page.tsx create mode 100644 web/src/app/home/add-plugin/page.tsx diff --git a/src/langbot/pkg/plugin/connector.py b/src/langbot/pkg/plugin/connector.py index 4e9e8f8e..1008ec91 100644 --- a/src/langbot/pkg/plugin/connector.py +++ b/src/langbot/pkg/plugin/connector.py @@ -316,13 +316,17 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): raise Exception(f'MCP {plugin_author}/{plugin_name} has no config') elif mcp_resp.status_code == 404: # Try skill endpoint - download ZIP and install - self.ap.logger.info(f'Installing skill from marketplace: {plugin_author}/{plugin_name}') + self.ap.logger.info(f'Trying skill endpoint for: {plugin_author}/{plugin_name}') if task_context: - task_context.set_current_action('installing skill from marketplace') + task_context.set_current_action('checking skill marketplace') # Get skill detail to find version skill_resp = await client.get(f'{space_url}/api/v1/marketplace/skills/{plugin_author}/{plugin_name}') if skill_resp.status_code == 200: + self.ap.logger.info(f'Installing skill from marketplace: {plugin_author}/{plugin_name}') + if task_context: + task_context.set_current_action('installing skill from marketplace') + # Download the skill ZIP (no version needed - uses latest) if task_context: task_context.set_current_action('downloading skill package') @@ -340,8 +344,46 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): # Install skill from ZIP using skill service await self._install_skill_from_zip(file_bytes, f'{plugin_author}-{plugin_name}', task_context) return + elif skill_resp.status_code == 404: + # Try plugin endpoint - get versions and download + self.ap.logger.info(f'Trying plugin endpoint for: {plugin_author}/{plugin_name}') + if task_context: + task_context.set_current_action('checking plugin marketplace') + + # Get plugin versions to find latest + versions_resp = await client.get( + f'{space_url}/api/v1/marketplace/plugins/{plugin_author}/{plugin_name}/versions' + ) + if versions_resp.status_code == 200: + versions_data = versions_resp.json().get('data', {}).get('versions', []) + if versions_data: + latest_version = versions_data[0].get('version', '') + if latest_version: + self.ap.logger.info(f'Installing plugin from marketplace: {plugin_author}/{plugin_name} v{latest_version}') + if task_context: + task_context.set_current_action('downloading plugin package') + + download_resp = await client.get( + f'{space_url}/api/v1/marketplace/plugins/download/{plugin_author}/{plugin_name}/{latest_version}' + ) + if download_resp.status_code != 200: + raise Exception(f'Failed to download plugin {plugin_author}/{plugin_name}: {download_resp.status_code}') + + file_bytes = download_resp.content + self._extract_deps_metadata(file_bytes, task_context) + file_key = await self.handler.send_file(file_bytes, 'lbpkg') + install_info['plugin_file_key'] = file_key + self.ap.logger.info(f'Transfered file {file_key} to plugin runtime') + # Continue to install via runtime + else: + raise Exception(f'No version found for plugin {plugin_author}/{plugin_name}') + else: + raise Exception(f'Plugin {plugin_author}/{plugin_name} has no versions') + else: + raise Exception(f'Plugin {plugin_author}/{plugin_name} not found in marketplace') else: - raise Exception(f'Skill {plugin_author}/{plugin_name} not found in marketplace') + skill_resp.raise_for_status() + raise Exception(f'Failed to get skill {plugin_author}/{plugin_name}') else: mcp_resp.raise_for_status() raise Exception(f'Failed to get MCP {plugin_author}/{plugin_name}') diff --git a/web/src/app/home/add-extension/page.tsx b/web/src/app/home/add-extension/page.tsx new file mode 100644 index 00000000..d4d2e7bf --- /dev/null +++ b/web/src/app/home/add-extension/page.tsx @@ -0,0 +1,239 @@ +import MarketPage from '@/app/home/plugins/components/plugin-market/PluginMarketComponent'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Download, PlusIcon, ChevronDownIcon } from 'lucide-react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; +import { toast } from 'sonner'; +import { useTranslation } from 'react-i18next'; +import { PluginV4 } from '@/app/infra/entities/plugin'; +import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; +import { usePluginInstallTasks } from '@/app/home/plugins/components/plugin-install-task'; + +enum PluginInstallStatus { + ASK_CONFIRM = 'ask_confirm', + INSTALLING = 'installing', + ERROR = 'error', +} + +export default function AddExtensionPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + + if (!systemInfo?.enable_marketplace) { + return ( +
+

{t('plugins.marketplace')}

+
+ ); + } + + return ; +} + +function AddExtensionContent() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { refreshPlugins } = useSidebarData(); + const { + addTask, + setSelectedTaskId, + registerOnTaskComplete, + unregisterOnTaskComplete, + } = 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); + + useEffect(() => { + const onComplete = (_taskId: number, success: boolean) => { + if (success) { + toast.success(t('plugins.installSuccess')); + refreshPlugins(); + } + }; + registerOnTaskComplete(onComplete); + return () => { + unregisterOnTaskComplete(onComplete); + }; + }, [registerOnTaskComplete, unregisterOnTaskComplete, refreshPlugins, t]); + + const handleInstallPlugin = useCallback( + async (plugin: PluginV4) => { + setInstallInfo({ + plugin_author: plugin.author, + plugin_name: plugin.name, + plugin_version: plugin.latest_version, + }); + setInstallExtensionType(plugin.type || 'plugin'); + setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); + setInstallError(null); + setModalOpen(true); + }, + [t], + ); + + function handleModalConfirm() { + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + const pluginDisplayName = `${installInfo.plugin_author}/${installInfo.plugin_name}`; + httpClient + .installPluginFromMarketplace( + installInfo.plugin_author, + installInfo.plugin_name, + installInfo.plugin_version, + ) + .then((resp: { task_id: number }) => { + const taskId = resp.task_id; + const taskKey = `marketplace-${taskId}`; + addTask({ + taskId, + pluginName: pluginDisplayName, + source: 'marketplace', + extensionType: installExtensionType, + }); + setSelectedTaskId(taskKey); + setModalOpen(false); + }) + .catch((err: { msg?: string }) => { + setInstallError(err.msg || null); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } + + return ( + <> +
+
+ + + + + + + + navigate('/home/skills?action=create')}> + {t('skills.createManually')} + + navigate('/home/skills?action=upload')}> + {t('skills.uploadZip')} + + navigate('/home/skills?action=github')}> + {t('skills.importFromGithub')} + + + + + + + + + + navigate('/home/add-plugin?action=github')}> + {t('plugins.installFromGithub')} + + navigate('/home/add-plugin?action=upload')}> + {t('plugins.uploadLocal')} + + + +
+ +
+ +
+
+ + { + setModalOpen(open); + if (!open) { + setInstallError(null); + } + }} + > + + + + + {t('plugins.installPlugin')} + + + + {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && ( +
+

+ {t('plugins.askConfirm', { + name: installInfo.plugin_name, + version: installInfo.plugin_version, + })} +

+
+ )} + + {pluginInstallStatus === PluginInstallStatus.INSTALLING && ( +
+

{t('plugins.installing')}

+
+ )} + + {pluginInstallStatus === PluginInstallStatus.ERROR && ( +
+

{t('plugins.installFailed')}

+

{installError}

+
+ )} + + + {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && ( + <> + + + + )} + {pluginInstallStatus === PluginInstallStatus.ERROR && ( + + )} + +
+
+ + ); +} \ No newline at end of file diff --git a/web/src/app/home/add-plugin/page.tsx b/web/src/app/home/add-plugin/page.tsx new file mode 100644 index 00000000..f9cf95bc --- /dev/null +++ b/web/src/app/home/add-plugin/page.tsx @@ -0,0 +1,586 @@ +import { useEffect, useState, useRef } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Github, Upload as UploadIcon, ChevronLeft } from 'lucide-react'; +import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; +import { toast } from 'sonner'; +import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; +import { usePluginInstallTasks } from '@/app/home/plugins/components/plugin-install-task'; + +enum PluginInstallStatus { + WAIT_INPUT = 'wait_input', + SELECT_RELEASE = 'select_release', + SELECT_ASSET = 'select_asset', + ASK_CONFIRM = 'ask_confirm', + INSTALLING = 'installing', + ERROR = 'error', +} + +interface GithubRelease { + id: number; + tag_name: string; + name: string; + published_at: string; + prerelease: boolean; + draft: boolean; + source_type?: 'release' | 'tag' | 'branch'; + archive_url?: string; +} + +interface GithubAsset { + id: number; + name: string; + size: number; + download_url: string; + content_type: string; +} + +export default function AddPluginPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const actionParam = searchParams.get('action'); + + const { refreshPlugins } = useSidebarData(); + const { + addTask, + setSelectedTaskId, + registerOnTaskComplete, + unregisterOnTaskComplete, + } = usePluginInstallTasks(); + + const [githubURL, setGithubURL] = useState(''); + const [githubReleases, setGithubReleases] = useState([]); + const [selectedRelease, setSelectedRelease] = useState(null); + const [githubAssets, setGithubAssets] = useState([]); + const [selectedAsset, setSelectedAsset] = useState(null); + const [githubOwner, setGithubOwner] = useState(''); + const [githubRepo, setGithubRepo] = useState(''); + const [fetchingReleases, setFetchingReleases] = useState(false); + const [fetchingAssets, setFetchingAssets] = useState(false); + const [installError, setInstallError] = useState(null); + const [pluginInstallStatus, setPluginInstallStatus] = + useState(PluginInstallStatus.WAIT_INPUT); + const fileInputRef = useRef(null); + + const isGithubMode = actionParam === 'github'; + const isUploadMode = actionParam === 'upload'; + + useEffect(() => { + const onComplete = (_taskId: number, success: boolean) => { + if (success) { + toast.success(t('plugins.installSuccess')); + refreshPlugins(); + } + }; + registerOnTaskComplete(onComplete); + return () => { + unregisterOnTaskComplete(onComplete); + }; + }, [registerOnTaskComplete, unregisterOnTaskComplete, refreshPlugins, t]); + + async function checkExtensionsLimit(): Promise { + const maxExtensions = systemInfo.limitation?.max_extensions ?? -1; + if (maxExtensions < 0) return true; + try { + const [pluginsResp, mcpResp] = await Promise.all([ + httpClient.getPlugins(), + httpClient.getMCPServers(), + ]); + const total = + (pluginsResp.plugins?.length ?? 0) + (mcpResp.servers?.length ?? 0); + if (total >= maxExtensions) { + toast.error( + t('limitation.maxExtensionsReached', { max: maxExtensions }), + ); + return false; + } + } catch { + // If we can't check, let backend handle it + } + return true; + } + + function resetGithubState() { + setGithubURL(''); + setGithubReleases([]); + setSelectedRelease(null); + setGithubAssets([]); + setSelectedAsset(null); + setGithubOwner(''); + setGithubRepo(''); + setFetchingReleases(false); + setFetchingAssets(false); + } + + async function fetchGithubReleases() { + if (!githubURL.trim()) { + toast.error(t('plugins.enterRepoUrl')); + return; + } + + setFetchingReleases(true); + setInstallError(null); + + try { + const result = await httpClient.getGithubReleases(githubURL); + setGithubReleases(result.releases); + setGithubOwner(result.owner); + setGithubRepo(result.repo); + + if (result.releases.length === 0) { + toast.warning(t('plugins.noReleasesFound')); + } else { + setPluginInstallStatus(PluginInstallStatus.SELECT_RELEASE); + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + setInstallError(errorMessage || t('plugins.fetchReleasesError')); + setPluginInstallStatus(PluginInstallStatus.ERROR); + } finally { + setFetchingReleases(false); + } + } + + async function handleReleaseSelect(release: GithubRelease) { + setSelectedRelease(release); + setFetchingAssets(true); + setInstallError(null); + + try { + const result = await httpClient.getGithubReleaseAssets( + githubOwner, + githubRepo, + release.id, + release.tag_name, + release.source_type, + release.archive_url, + ); + setGithubAssets(result.assets); + + if (result.assets.length === 0) { + toast.warning(t('plugins.noAssetsFound')); + } else { + setPluginInstallStatus(PluginInstallStatus.SELECT_ASSET); + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + setInstallError(errorMessage || t('plugins.fetchAssetsError')); + setPluginInstallStatus(PluginInstallStatus.ERROR); + } finally { + setFetchingAssets(false); + } + } + + function handleAssetSelect(asset: GithubAsset) { + setSelectedAsset(asset); + setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); + } + + function handleGithubConfirm() { + if (!selectedAsset || !selectedRelease) return; + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + const pluginDisplayName = `${githubOwner}/${githubRepo}`; + httpClient + .installPluginFromGithub( + selectedAsset.download_url, + githubOwner, + githubRepo, + selectedRelease.tag_name, + ) + .then((resp) => { + const taskId = resp.task_id; + const taskKey = `github-${taskId}`; + addTask({ + taskId, + pluginName: pluginDisplayName, + source: 'github', + extensionType: 'plugin', + fileSize: selectedAsset.size, + }); + setSelectedTaskId(taskKey); + resetGithubState(); + toast.success(t('plugins.installSuccess')); + navigate('/home/add-extension'); + }) + .catch((err) => { + setInstallError(err.msg); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } + + // Local file upload + const validateFileType = (file: File): boolean => { + const allowedExtensions = ['.lbpkg']; + const fileName = file.name.toLowerCase(); + return allowedExtensions.some((ext) => fileName.endsWith(ext)); + }; + + const uploadPluginFile = async (file: File) => { + if (!validateFileType(file)) { + toast.error(t('plugins.unsupportedFileType')); + return; + } + + if (!(await checkExtensionsLimit())) return; + + const fileName = file.name || 'local plugin'; + const fileSize = file.size; + setInstallError(null); + + httpClient + .installPluginFromLocal(file) + .then((resp) => { + const taskId = resp.task_id; + const taskKey = `local-${taskId}`; + addTask({ + taskId, + pluginName: fileName, + source: 'local', + extensionType: 'plugin', + fileSize: fileSize, + }); + setSelectedTaskId(taskKey); + toast.success(t('plugins.installSuccess')); + navigate('/home/add-extension'); + }) + .catch((err) => { + toast.error(t('plugins.installFailed') + (err.msg || '')); + }); + }; + + const handleFileSelect = async () => { + if (!(await checkExtensionsLimit())) return; + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + uploadPluginFile(file); + } + event.target.value = ''; + }; + + function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; + } + + function handleCancel() { + navigate('/home/add-extension'); + } + + // GitHub Install View + if (isGithubMode) { + return ( +
+ + +
+

{t('plugins.installFromGithub')}

+ +
+ +
+
+ + + + + {t('plugins.installFromGithub')} + + + {t('plugins.installFromGithubDesc')} + + + + {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( +
+

{t('plugins.enterRepoUrl')}

+
+ setGithubURL(e.target.value)} + /> + +
+
+ )} + + {pluginInstallStatus === PluginInstallStatus.SELECT_RELEASE && ( +
+
+

+ {t('plugins.selectRelease')} +

+ +
+
+ {githubReleases.map((release) => ( + handleReleaseSelect(release)} + > + +
+ + {release.name || release.tag_name} + + + {t('plugins.releaseTag', { + tag: release.tag_name, + })}{' '} + •{' '} + {t('plugins.publishedAt', { + date: new Date( + release.published_at, + ).toLocaleDateString(), + })} + +
+ {release.prerelease && ( + + {t('plugins.prerelease')} + + )} +
+
+ ))} +
+ {fetchingAssets && ( +

+ {t('plugins.loading')} +

+ )} +
+ )} + + {pluginInstallStatus === PluginInstallStatus.SELECT_ASSET && ( +
+
+

+ {t('plugins.selectAsset')} +

+ +
+ {selectedRelease && ( +
+
+ {selectedRelease.name || selectedRelease.tag_name} +
+
+ {selectedRelease.tag_name} +
+
+ )} +
+ {githubAssets.map((asset) => ( + handleAssetSelect(asset)} + > + + + {asset.name} + + + {t('plugins.assetSize', { + size: formatFileSize(asset.size), + })} + + + + ))} +
+
+ )} + + {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && ( +
+
+

+ {t('plugins.confirmInstall')} +

+ +
+ {selectedRelease && selectedAsset && ( +
+
+ + Repository:{' '} + + + {githubOwner}/{githubRepo} + +
+
+ Release: + + {selectedRelease.tag_name} + +
+
+ File: + {selectedAsset.name} +
+
+ )} +
+ +
+
+ )} + + {pluginInstallStatus === PluginInstallStatus.INSTALLING && ( +
+

{t('plugins.installing')}

+
+ )} + + {pluginInstallStatus === PluginInstallStatus.ERROR && ( +
+

{t('plugins.installFailed')}

+

{installError}

+
+ +
+
+ )} +
+
+
+
+
+ ); + } + + // Upload Mode - show file select dialog + if (isUploadMode) { + return ( +
+ + +
+

{t('plugins.uploadLocal')}

+ +
+ +
+
+ + + + + {t('plugins.uploadLocal')} + + + {t('plugins.uploadPluginOnly')} + + + +
+ +

+ {t('plugins.dragToUpload')} +

+

+ {t('plugins.selectFileToUpload')} +

+
+
+
+
+
+
+ ); + } + + // Default: redirect to add-extension + navigate('/home/add-extension'); + return null; +} \ No newline at end of file diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 20126eb4..e54a2b3e 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -143,7 +143,6 @@ const CREATABLE_CATEGORIES: EntityCategoryId[] = [ 'pipelines', 'knowledge', 'mcp', - 'plugins', 'skills', ]; diff --git a/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx index 69a7cc84..a6a717a6 100644 --- a/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx +++ b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx @@ -7,9 +7,7 @@ import { Workflow, BookMarked, Puzzle, - Store, - Hexagon, - Mountain, + PlusCircle, } from 'lucide-react'; const t = (key: string) => { @@ -98,10 +96,10 @@ export const sidebarConfigList = [ section: 'extensions', }), new SidebarChildVO({ - id: 'market', - name: t('sidebar.pluginMarket'), - icon: , - route: '/home/market', + id: 'add-extension', + name: t('sidebar.addExtension'), + icon: , + route: '/home/add-extension', description: t('plugins.description'), helpLink: { en_US: 'https://link.langbot.app/en/docs/plugins', @@ -110,29 +108,4 @@ export const sidebarConfigList = [ }, section: 'extensions', }), - new SidebarChildVO({ - id: 'mcp', - name: t('sidebar.mcpServers'), - icon: , - route: '/home/mcp', - description: t('mcp.title'), - helpLink: { - en_US: '', - zh_Hans: '', - }, - section: 'extensions', - }), - new SidebarChildVO({ - id: 'skills', - name: t('skills.title'), - icon: , - route: '/home/skills', - description: t('skills.description'), - helpLink: { - en_US: '', - zh_Hans: '', - ja_JP: '', - }, - section: 'extensions', - }), -]; +]; \ No newline at end of file diff --git a/web/src/app/home/mcp/MCPDetailContent.tsx b/web/src/app/home/mcp/MCPDetailContent.tsx index 49b2c61e..f085a297 100644 --- a/web/src/app/home/mcp/MCPDetailContent.tsx +++ b/web/src/app/home/mcp/MCPDetailContent.tsx @@ -148,6 +148,13 @@ export default function MCPDetailContent({ id }: { id: string }) {

{t('mcp.createServer')}

+
- - - - - - - {systemInfo.enable_marketplace && ( - { - navigate('/home/market'); - }} - > - - {t('plugins.goToMarketplace')} - - )} - - - {t('plugins.uploadLocal')} - - { - if (!(await checkExtensionsLimit())) return; - setInstallSource('github'); - setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT); - setInstallError(null); - resetGithubState(); - setShowGithubInstall(true); - }} - > - - {t('plugins.installFromGithub')} - - -
{/* Inline GitHub install flow */} diff --git a/web/src/app/home/skills/page.tsx b/web/src/app/home/skills/page.tsx index 3db8667c..aa39006e 100644 --- a/web/src/app/home/skills/page.tsx +++ b/web/src/app/home/skills/page.tsx @@ -15,48 +15,48 @@ export default function SkillsPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const detailId = searchParams.get('id'); + const actionParam = searchParams.get('action') as SkillInstallAction | null; const { refreshSkills, pendingSkillInstallAction, setPendingSkillInstallAction, } = useSidebarData(); - // Local active view: consumed from context on mount/change const [activeView, setActiveView] = useState(null); - // Consume pending action from sidebar context useEffect(() => { + if (actionParam && ['create', 'github', 'upload'].includes(actionParam)) { + setActiveView(actionParam); + return; + } if (!pendingSkillInstallAction) return; const action = pendingSkillInstallAction; setPendingSkillInstallAction(null); setActiveView(action); - }, [pendingSkillInstallAction, setPendingSkillInstallAction]); + }, [actionParam, pendingSkillInstallAction, setPendingSkillInstallAction]); - // If a detail id is present, show detail content (edit existing / old create mode) if (detailId) { return ; } - // Handle callback after skills are imported/created function handleImportedSkills(skillNames: string[]) { void refreshSkills(); setActiveView(null); - const primarySkill = skillNames[0]; - if (primarySkill) { - navigate(`/home/skills?id=${encodeURIComponent(primarySkill)}`); - return; - } - navigate('/home/skills'); + navigate('/home/add-extension'); + } + + function handleCancel() { + setActiveView(null); + navigate('/home/add-extension'); } - // Inline create manually view if (activeView === 'create') { return (

{t('skills.createSkill')}

-
@@ -104,13 +103,12 @@ export default function SkillsPage() { ); } - // Inline upload ZIP view if (activeView === 'upload') { return (

{t('skills.uploadZip')}

-
@@ -126,10 +124,6 @@ export default function SkillsPage() { ); } - // Default: no selection - return ( -
-

{t('skills.selectFromSidebar')}

-
- ); -} + navigate('/home/add-extension'); + return null; +} \ No newline at end of file diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index d985f36c..75e90d28 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -5,6 +5,7 @@ const enUS = { installedPlugins: 'Installed Extensions', pluginMarket: 'Extension Market', mcpServers: 'MCP Servers', + addExtension: 'Add Extension', pluginPages: 'Plugin Pages', pluginPagesTooltip: 'Visual pages provided by installed plugins', quickStart: 'Quick Start', @@ -433,6 +434,7 @@ const enUS = { arrange: 'Sort Plugins', install: 'Install', installPlugin: 'Install Plugin', + newPlugin: 'New Plugin', onlySupportGithub: 'Currently only supports installation from GitHub', enterGithubLink: 'Enter GitHub link of the plugin', installing: 'Installing plugin...', @@ -513,9 +515,10 @@ const enUS = { uploadLocal: 'Upload Local', debugging: 'Debugging', uploadLocalPlugin: 'Upload Local Plugin', + uploadPluginOnly: 'Only .lbpkg files are supported', dragToUpload: 'Drag plugin file here to upload', unsupportedFileType: - 'Unsupported file type, only .lbpkg and .zip files are supported', + 'Unsupported file type, only .lbpkg files are supported', uploadingPlugin: 'Uploading plugin...', uploadSuccess: 'Upload successful', uploadFailed: 'Upload failed', @@ -673,6 +676,7 @@ const enUS = { mcp: { title: 'MCP', createServer: 'Add MCP Server', + addMCPServer: 'Add MCP Server', editServer: 'Edit MCP Server', deleteServer: 'Delete MCP Server', confirmDeleteServer: 'Are you sure you want to delete this MCP server?', @@ -1346,6 +1350,7 @@ const enUS = { deleteSuccess: 'Deleted successfully', deleteError: 'Delete failed: ', deleteConfirmation: 'Are you sure you want to delete this skill?', + delete: 'Delete Skill', skillNameRequired: 'Skill name cannot be empty', skillDescriptionRequired: 'Skill description cannot be empty', packageRootRequired: 'Package root path cannot be empty', @@ -1364,6 +1369,7 @@ const enUS = { advancedSettings: 'Advanced Settings', searchSkills: 'Search skills...', selectSkills: 'Select Skills', + addSkill: 'Add Skill', builtin: 'Built-in', importFromGithub: 'Import from GitHub', createManually: 'Create Manually', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 6868c207..dbf657bd 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -5,6 +5,7 @@ const esES = { installedPlugins: 'Plugins instalados', pluginMarket: 'Tienda', mcpServers: 'Servidores MCP', + addExtension: 'Añadir extensión', pluginPages: 'Páginas de plugins', pluginPagesTooltip: 'Páginas visuales proporcionadas por los plugins instalados', @@ -446,6 +447,7 @@ const esES = { arrange: 'Ordenar plugins', install: 'Instalar', installPlugin: 'Instalar plugin', + newPlugin: 'Nuevo Plugin', onlySupportGithub: 'Actualmente solo se admite la instalación desde GitHub', enterGithubLink: 'Introduce el enlace de GitHub del plugin', installing: 'Instalando plugin...', @@ -681,6 +683,7 @@ const esES = { mcp: { title: 'MCP', createServer: 'Añadir servidor MCP', + addMCPServer: 'Añadir servidor MCP', editServer: 'Editar servidor MCP', deleteServer: 'Eliminar servidor MCP', confirmDeleteServer: diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 82a1235d..92e7b17f 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -5,6 +5,7 @@ const jaJP = { installedPlugins: 'インストール済みプラグイン', pluginMarket: 'プラグインマーケット', mcpServers: 'MCPサーバー', + addExtension: '拡張機能を追加', pluginPages: 'プラグインページ', pluginPagesTooltip: 'インストール済みプラグインが提供するビジュアルページ', quickStart: 'クイックスタート', @@ -438,6 +439,7 @@ const jaJP = { arrange: '並び替え', install: 'インストール', installPlugin: 'プラグインをインストール', + newPlugin: '新規プラグイン', onlySupportGithub: '現在はGitHubからのインストールのみサポートしています', enterGithubLink: 'プラグインのGitHubリンクを入力してください', installing: 'プラグインをインストール中...', @@ -672,6 +674,7 @@ const jaJP = { mcp: { title: 'MCP', createServer: 'MCPサーバーを追加', + addMCPServer: 'MCPサーバーを追加', editServer: 'MCPサーバーを編集', deleteServer: 'MCPサーバーを削除', confirmDeleteServer: 'このMCPサーバーを削除してもよろしいですか?', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index 6025fd45..045d0c9e 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -5,6 +5,7 @@ const ruRU = { installedPlugins: 'Установленные плагины', pluginMarket: 'Маркетплейс', mcpServers: 'MCP-серверы', + addExtension: 'Добавить расширение', pluginPages: 'Страницы плагинов', pluginPagesTooltip: 'Визуальные страницы, предоставляемые установленными плагинами', @@ -441,6 +442,7 @@ const ruRU = { arrange: 'Сортировка плагинов', install: 'Установить', installPlugin: 'Установить плагин', + newPlugin: 'Новый плагин', onlySupportGithub: 'В настоящее время поддерживается установка только с GitHub', enterGithubLink: 'Введите ссылку на GitHub плагина', @@ -677,6 +679,7 @@ const ruRU = { mcp: { title: 'MCP', createServer: 'Добавить MCP-сервер', + addMCPServer: 'Добавить MCP-сервер', editServer: 'Редактировать MCP-сервер', deleteServer: 'Удалить MCP-сервер', confirmDeleteServer: 'Вы уверены, что хотите удалить этот MCP-сервер?', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 2e27cbc9..6d305350 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -5,6 +5,7 @@ const thTH = { installedPlugins: 'ปลั๊กอินที่ติดตั้ง', pluginMarket: 'ตลาดปลั๊กอิน', mcpServers: 'เซิร์ฟเวอร์ MCP', + addExtension: 'เพิ่มส่วนขยาย', pluginPages: 'หน้าปลั๊กอิน', pluginPagesTooltip: 'หน้าเว็บที่จัดทำโดยปลั๊กอินที่ติดตั้ง', quickStart: 'เริ่มต้นอย่างรวดเร็ว', @@ -429,6 +430,7 @@ const thTH = { arrange: 'เรียงลำดับปลั๊กอิน', install: 'ติดตั้ง', installPlugin: 'ติดตั้งปลั๊กอิน', + newPlugin: 'สร้างปลั๊กอินใหม่', onlySupportGithub: 'ปัจจุบันรองรับเฉพาะการติดตั้งจาก GitHub', enterGithubLink: 'กรอกลิงก์ GitHub ของปลั๊กอิน', installing: 'กำลังติดตั้งปลั๊กอิน...', @@ -658,6 +660,7 @@ const thTH = { mcp: { title: 'MCP', createServer: 'เพิ่มเซิร์ฟเวอร์ MCP', + addMCPServer: 'เพิ่มเซิร์ฟเวอร์ MCP', editServer: 'แก้ไขเซิร์ฟเวอร์ MCP', deleteServer: 'ลบเซิร์ฟเวอร์ MCP', confirmDeleteServer: 'คุณแน่ใจหรือไม่ว่าต้องการลบเซิร์ฟเวอร์ MCP นี้?', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index 0f8aed56..15dd8d72 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -5,6 +5,7 @@ const viVN = { installedPlugins: 'Plugin đã cài đặt', pluginMarket: 'Chợ ứng dụng', mcpServers: 'Máy chủ MCP', + addExtension: 'Thêm tiện ích mở rộng', pluginPages: 'Trang plugin', pluginPagesTooltip: 'Các trang trực quan được cung cấp bởi plugin đã cài đặt', @@ -439,6 +440,7 @@ const viVN = { arrange: 'Sắp xếp Plugin', install: 'Cài đặt', installPlugin: 'Cài đặt Plugin', + newPlugin: 'Plugin mới', onlySupportGithub: 'Hiện chỉ hỗ trợ cài đặt từ GitHub', enterGithubLink: 'Nhập liên kết GitHub của plugin', installing: 'Đang cài đặt plugin...', @@ -671,6 +673,7 @@ const viVN = { mcp: { title: 'MCP', createServer: 'Thêm máy chủ MCP', + addMCPServer: 'Thêm máy chủ MCP', editServer: 'Chỉnh sửa máy chủ MCP', deleteServer: 'Xóa máy chủ MCP', confirmDeleteServer: 'Bạn có chắc chắn muốn xóa máy chủ MCP này không?', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 9a60db77..3471637d 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -1,10 +1,11 @@ const zhHans = { sidebar: { home: '首页', - extensions: '扩展', + extensions: '拓展', installedPlugins: '已安装拓展', pluginMarket: '拓展市场', mcpServers: 'MCP 服务器', + addExtension: '添加拓展', pluginPages: '插件页面', pluginPagesTooltip: '由已安装的插件提供的可视化页面', quickStart: '快速开始向导', @@ -416,6 +417,7 @@ const zhHans = { arrange: '编排', install: '安装', installPlugin: '安装插件', + newPlugin: '新建插件', onlySupportGithub: '目前仅支持从 GitHub 安装', enterGithubLink: '请输入插件的Github链接', installing: '正在安装插件...', @@ -490,8 +492,9 @@ const zhHans = { uploadLocal: '本地上传', debugging: '调试中', uploadLocalPlugin: '上传本地插件', + uploadPluginOnly: '仅支持 .lbpkg 文件', dragToUpload: '拖拽文件到此处上传', - unsupportedFileType: '不支持的文件类型,仅支持 .lbpkg 和 .zip 文件', + unsupportedFileType: '不支持的文件类型,仅支持 .lbpkg 文件', uploadingPlugin: '正在上传插件...', uploadSuccess: '上传成功', uploadFailed: '上传失败', @@ -645,6 +648,7 @@ const zhHans = { mcp: { title: 'MCP', createServer: '添加 MCP 服务器', + addMCPServer: '添加 MCP 服务器', editServer: '修改 MCP 服务器', deleteServer: '删除 MCP 服务器', confirmDeleteServer: '你确定要删除此 MCP 服务器吗?', @@ -1290,6 +1294,7 @@ const zhHans = { deleteSuccess: '删除成功', deleteError: '删除失败:', deleteConfirmation: '你确定要删除这个技能吗?', + delete: '删除技能', skillNameRequired: '技能名称不能为空', skillDescriptionRequired: '技能描述不能为空', packageRootRequired: '技能目录不能为空', @@ -1308,6 +1313,7 @@ const zhHans = { searchSkills: '搜索技能...', selectSkills: '选择技能', builtin: '内置', + addSkill: '添加技能', importFromGithub: '从 GitHub 导入', createManually: '手动创建', uploadZip: '上传 ZIP 包', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 130ac3e8..51d4b653 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -5,6 +5,7 @@ const zhHant = { installedPlugins: '已安裝外掛', pluginMarket: '外掛市場', mcpServers: 'MCP 伺服器', + addExtension: '添加擴展', pluginPages: '插件頁面', pluginPagesTooltip: '由已安裝的插件提供的視覺化頁面', quickStart: '快速開始', @@ -416,6 +417,7 @@ const zhHant = { arrange: '編排', install: '安裝', installPlugin: '安裝外掛', + newPlugin: '新建外掛', installFromGithub: '來自 GitHub', onlySupportGithub: '目前僅支援從 GitHub 安裝', enterGithubLink: '請輸入外掛的Github連結', @@ -639,6 +641,7 @@ const zhHant = { mcp: { title: 'MCP', createServer: '新增MCP伺服器', + addMCPServer: '新增 MCP 伺服器', editServer: '編輯MCP伺服器', deleteServer: '刪除MCP伺服器', confirmDeleteServer: '您確定要刪除此MCP伺服器嗎?', @@ -1327,6 +1330,67 @@ const zhHant = { selectFromSidebar: '從側邊欄選擇一個插件頁面', invalidPage: '無效的插件頁面', }, + skills: { + title: '技能', + description: '創建和管理可在對話中激活的技能', + createSkill: '創建技能', + editSkill: '編輯技能', + getSkillListError: '獲取技能列表失敗:', + skillName: '技能名稱', + displayName: '技能名稱', + displayNamePlaceholder: '顯示名稱', + skillSlug: '目錄名稱', + skillSlugPlaceholder: 'english-name-only', + skillSlugHelp: '用作技能目錄名,僅支援英文字母、數字、連字符和底線。', + skillDescription: '技能描述', + skillInstructions: '指令內容', + autoActivate: '自動啟用', + saveSuccess: '儲存成功', + saveError: '儲存失敗:', + createSuccess: '創建成功', + createError: '創建失敗:', + deleteSuccess: '刪除成功', + deleteError: '刪除失敗:', + deleteConfirmation: '你確定要刪除這個技能嗎?', + delete: '刪除技能', + skillNameRequired: '技能名稱不能為空', + skillDescriptionRequired: '技能描述不能為空', + packageRootRequired: '技能目錄不能為空', + scan: '掃描', + scanSuccess: '目錄掃描成功', + scanError: '掃描目錄失敗:', + noSkills: '暫未配置任何技能', + preview: '預覽', + previewInstructions: '預覽指令', + instructionsPlaceholder: '使用 Markdown 格式輸入技能指令...', + descriptionPlaceholder: '簡短描述此技能的功能', + packageRoot: '技能目錄', + packageRootHelp: '非必填。僅在導入已有技能目錄時需要填寫。', + advancedSettings: '進階設定', + searchSkills: '搜尋技能...', + selectSkills: '選擇技能', + builtin: '內建', + addSkill: '添加技能', + importFromGithub: '從 GitHub 導入', + createManually: '手動創建', + uploadZip: '上傳 ZIP 包', + uploadZipOnly: '僅支援 .zip 技能包', + installSuccess: '技能安裝成功', + installError: '安裝技能失敗:', + enterRepoUrl: '輸入 GitHub 倉庫地址', + repoUrlPlaceholder: '例如 https://github.com/owner/repo', + fetchingReleases: '正在獲取發布版本...', + selectRelease: '選擇發布版本', + noReleasesFound: '未找到發布版本', + fetchReleasesError: '獲取發布版本失敗:', + selectAsset: '選擇要安裝的檔案', + sourceArchive: '源碼包 (zip)', + noAssetsFound: '此版本暫無可安裝的檔案', + fetchAssetsError: '獲取檔案列表失敗:', + backToReleases: '返回版本列表', + backToRepoUrl: '返回倉庫地址', + selectFromSidebar: '從側邊欄選擇一個技能', + }, }; export default zhHant; diff --git a/web/src/router.tsx b/web/src/router.tsx index a1af63d6..41a92061 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -18,6 +18,8 @@ import MonitoringPage from '@/app/home/monitoring/page'; import BotsPage from '@/app/home/bots/page'; import PipelinesPage from '@/app/home/pipelines/page'; import PluginsPage from '@/app/home/plugins/page'; +import AddExtensionPage from '@/app/home/add-extension/page'; +import AddPluginPage from '@/app/home/add-plugin/page'; import MarketPage from '@/app/home/market/page'; import MCPPage from '@/app/home/mcp/page'; import KnowledgePage from '@/app/home/knowledge/page'; @@ -117,6 +119,26 @@ export const router = createBrowserRouter([ ), }, + { + path: '/home/add-extension', + element: ( + }> + + + + + ), + }, + { + path: '/home/add-plugin', + element: ( + }> + + + + + ), + }, { path: '/home/market', element: (