mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 20:14:36 +00:00
feat: refactor market
This commit is contained in:
@@ -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}')
|
||||
|
||||
239
web/src/app/home/add-extension/page.tsx
Normal file
239
web/src/app/home/add-extension/page.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
||||
<p className="text-muted-foreground">{t('plugins.marketplace')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <AddExtensionContent />;
|
||||
}
|
||||
|
||||
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<Record<string, string>>({});
|
||||
const [installExtensionType, setInstallExtensionType] = useState<'plugin' | 'mcp' | 'skill'>('plugin');
|
||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||
useState<PluginInstallStatus>(PluginInstallStatus.ASK_CONFIRM);
|
||||
const [installError, setInstallError] = useState<string | null>(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 (
|
||||
<>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex flex-row justify-end items-center px-[0.8rem] pb-4 flex-shrink-0 gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
className="px-6 py-4 cursor-pointer"
|
||||
onClick={() => navigate('/home/mcp?id=new')}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{t('mcp.addMCPServer')}
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{t('skills.addSkill')}
|
||||
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => navigate('/home/skills?action=create')}>
|
||||
{t('skills.createManually')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate('/home/skills?action=upload')}>
|
||||
{t('skills.uploadZip')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate('/home/skills?action=github')}>
|
||||
{t('skills.importFromGithub')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{t('plugins.newPlugin')}
|
||||
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => navigate('/home/add-plugin?action=github')}>
|
||||
{t('plugins.installFromGithub')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate('/home/add-plugin?action=upload')}>
|
||||
{t('plugins.uploadLocal')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<MarketPage installPlugin={handleInstallPlugin} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
onOpenChange={(open) => {
|
||||
setModalOpen(open);
|
||||
if (!open) {
|
||||
setInstallError(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="w-[500px] max-h-[80vh] p-6 bg-white dark:bg-[#1a1a1e] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-4">
|
||||
<Download className="size-6" />
|
||||
<span>{t('plugins.installPlugin')}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">
|
||||
{t('plugins.askConfirm', {
|
||||
name: installInfo.plugin_name,
|
||||
version: installInfo.plugin_version,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">{t('plugins.installing')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">{t('plugins.installFailed')}</p>
|
||||
<p className="mb-2 text-red-500">{installError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleModalConfirm}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<Button variant="default" onClick={() => setModalOpen(false)}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
586
web/src/app/home/add-plugin/page.tsx
Normal file
586
web/src/app/home/add-plugin/page.tsx
Normal file
@@ -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<GithubRelease[]>([]);
|
||||
const [selectedRelease, setSelectedRelease] = useState<GithubRelease | null>(null);
|
||||
const [githubAssets, setGithubAssets] = useState<GithubAsset[]>([]);
|
||||
const [selectedAsset, setSelectedAsset] = useState<GithubAsset | null>(null);
|
||||
const [githubOwner, setGithubOwner] = useState('');
|
||||
const [githubRepo, setGithubRepo] = useState('');
|
||||
const [fetchingReleases, setFetchingReleases] = useState(false);
|
||||
const [fetchingAssets, setFetchingAssets] = useState(false);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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<boolean> {
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="h-full flex flex-col">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".lbpkg"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('plugins.installFromGithub')}</h1>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<div className="mx-auto max-w-3xl space-y-6 pb-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Github className="size-5" />
|
||||
{t('plugins.installFromGithub')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('plugins.installFromGithubDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
||||
<div>
|
||||
<p className="mb-2 text-sm">{t('plugins.enterRepoUrl')}</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('plugins.repoUrlPlaceholder')}
|
||||
value={githubURL}
|
||||
onChange={(e) => setGithubURL(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={fetchGithubReleases}
|
||||
disabled={!githubURL.trim() || fetchingReleases}
|
||||
>
|
||||
{fetchingReleases
|
||||
? t('plugins.loading')
|
||||
: t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.SELECT_RELEASE && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="font-medium text-sm">
|
||||
{t('plugins.selectRelease')}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT);
|
||||
setGithubReleases([]);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
{t('plugins.backToRepoUrl')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-2 pb-2">
|
||||
{githubReleases.map((release) => (
|
||||
<Card
|
||||
key={release.id}
|
||||
className="cursor-pointer hover:shadow-sm transition-shadow duration-200 shadow-none py-4"
|
||||
onClick={() => handleReleaseSelect(release)}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-start justify-between px-3 space-y-0">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-sm">
|
||||
{release.name || release.tag_name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1">
|
||||
{t('plugins.releaseTag', {
|
||||
tag: release.tag_name,
|
||||
})}{' '}
|
||||
•{' '}
|
||||
{t('plugins.publishedAt', {
|
||||
date: new Date(
|
||||
release.published_at,
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{release.prerelease && (
|
||||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 px-2 py-0.5 rounded ml-2 shrink-0">
|
||||
{t('plugins.prerelease')}
|
||||
</span>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{fetchingAssets && (
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t('plugins.loading')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.SELECT_ASSET && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="font-medium text-sm">
|
||||
{t('plugins.selectAsset')}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPluginInstallStatus(
|
||||
PluginInstallStatus.SELECT_RELEASE,
|
||||
);
|
||||
setGithubAssets([]);
|
||||
setSelectedAsset(null);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
{t('plugins.backToReleases')}
|
||||
</Button>
|
||||
</div>
|
||||
{selectedRelease && (
|
||||
<div className="mb-3 p-2 bg-muted rounded">
|
||||
<div className="text-sm font-medium">
|
||||
{selectedRelease.name || selectedRelease.tag_name}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{selectedRelease.tag_name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-2 pb-2">
|
||||
{githubAssets.map((asset) => (
|
||||
<Card
|
||||
key={asset.id}
|
||||
className="cursor-pointer hover:shadow-sm transition-shadow duration-200 shadow-none py-3"
|
||||
onClick={() => handleAssetSelect(asset)}
|
||||
>
|
||||
<CardHeader className="px-3">
|
||||
<CardTitle className="text-sm">
|
||||
{asset.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{t('plugins.assetSize', {
|
||||
size: formatFileSize(asset.size),
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="font-medium text-sm">
|
||||
{t('plugins.confirmInstall')}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPluginInstallStatus(
|
||||
PluginInstallStatus.SELECT_ASSET,
|
||||
);
|
||||
setSelectedAsset(null);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
{t('plugins.backToAssets')}
|
||||
</Button>
|
||||
</div>
|
||||
{selectedRelease && selectedAsset && (
|
||||
<div className="p-3 bg-muted rounded space-y-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium">
|
||||
Repository:{' '}
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{githubOwner}/{githubRepo}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium">Release: </span>
|
||||
<span className="text-sm">
|
||||
{selectedRelease.tag_name}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium">File: </span>
|
||||
<span className="text-sm">{selectedAsset.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button onClick={handleGithubConfirm}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
||||
<div>
|
||||
<p className="text-sm">{t('plugins.installing')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<div>
|
||||
<p className="text-sm mb-1">{t('plugins.installFailed')}</p>
|
||||
<p className="text-sm text-destructive">{installError}</p>
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button variant="default" onClick={handleCancel}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Upload Mode - show file select dialog
|
||||
if (isUploadMode) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".lbpkg"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('plugins.uploadLocal')}</h1>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<div className="mx-auto max-w-3xl space-y-6 pb-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<UploadIcon className="size-5" />
|
||||
{t('plugins.uploadLocal')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('plugins.uploadPluginOnly')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center cursor-pointer hover:border-blue-500 transition-colors"
|
||||
onClick={handleFileSelect}
|
||||
>
|
||||
<UploadIcon className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{t('plugins.dragToUpload')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
{t('plugins.selectFileToUpload')}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: redirect to add-extension
|
||||
navigate('/home/add-extension');
|
||||
return null;
|
||||
}
|
||||
@@ -143,7 +143,6 @@ const CREATABLE_CATEGORIES: EntityCategoryId[] = [
|
||||
'pipelines',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
'plugins',
|
||||
'skills',
|
||||
];
|
||||
|
||||
|
||||
@@ -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: <Store className="text-blue-500" />,
|
||||
route: '/home/market',
|
||||
id: 'add-extension',
|
||||
name: t('sidebar.addExtension'),
|
||||
icon: <PlusCircle className="text-blue-500" />,
|
||||
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: <Hexagon className="text-blue-500" />,
|
||||
route: '/home/mcp',
|
||||
description: t('mcp.title'),
|
||||
helpLink: {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
section: 'extensions',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'skills',
|
||||
name: t('skills.title'),
|
||||
icon: <Mountain className="text-blue-500" />,
|
||||
route: '/home/skills',
|
||||
description: t('skills.description'),
|
||||
helpLink: {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
ja_JP: '',
|
||||
},
|
||||
section: 'extensions',
|
||||
}),
|
||||
];
|
||||
];
|
||||
@@ -148,6 +148,13 @@ export default function MCPDetailContent({ id }: { id: string }) {
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('mcp.createServer')}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate('/home/add-extension')}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -142,7 +142,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
|
||||
for (const server of mcpResp.servers) {
|
||||
extensions.push(new ExtensionCardVO({
|
||||
id: `mcp-${server.name}`,
|
||||
id: server.name,
|
||||
author: '',
|
||||
label: server.name.replace(/__/g, '/'),
|
||||
name: server.name,
|
||||
@@ -160,7 +160,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
|
||||
for (const skill of skillsResp.skills) {
|
||||
extensions.push(new ExtensionCardVO({
|
||||
id: `skill-${skill.name}`,
|
||||
id: skill.name,
|
||||
author: '',
|
||||
label: skill.display_name || skill.name,
|
||||
name: skill.name,
|
||||
@@ -185,9 +185,9 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
|
||||
function handleExtensionClick(extension: ExtensionCardVO) {
|
||||
if (extension.type === 'mcp') {
|
||||
navigate(`/home/mcp`);
|
||||
navigate(`/home/mcp?id=${encodeURIComponent(extension.id)}`);
|
||||
} else if (extension.type === 'skill') {
|
||||
navigate(`/home/skills`);
|
||||
navigate(`/home/skills?id=${encodeURIComponent(extension.id)}`);
|
||||
} else {
|
||||
const extensionId = `${extension.author}/${extension.name}`;
|
||||
navigate(`/home/plugins?id=${encodeURIComponent(extensionId)}`);
|
||||
|
||||
@@ -5,10 +5,7 @@ import PluginDetailContent from './PluginDetailContent';
|
||||
import styles from './plugins.module.css';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
PlusIcon,
|
||||
ChevronDownIcon,
|
||||
UploadIcon,
|
||||
StoreIcon,
|
||||
Power,
|
||||
Github,
|
||||
ChevronLeft,
|
||||
@@ -19,12 +16,6 @@ import {
|
||||
Unlink,
|
||||
} from 'lucide-react';
|
||||
import { copyToClipboard } from '@/app/utils/clipboard';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -631,45 +622,6 @@ function PluginListView() {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{t('plugins.install')}
|
||||
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{systemInfo.enable_marketplace && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigate('/home/market');
|
||||
}}
|
||||
>
|
||||
<StoreIcon className="w-4 h-4" />
|
||||
{t('plugins.goToMarketplace')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleFileSelect}>
|
||||
<UploadIcon className="w-4 h-4" />
|
||||
{t('plugins.uploadLocal')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
if (!(await checkExtensionsLimit())) return;
|
||||
setInstallSource('github');
|
||||
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT);
|
||||
setInstallError(null);
|
||||
resetGithubState();
|
||||
setShowGithubInstall(true);
|
||||
}}
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
{t('plugins.installFromGithub')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Inline GitHub install flow */}
|
||||
|
||||
@@ -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<SkillInstallAction>(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 <SkillDetailContent id={detailId} />;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('skills.createSkill')}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={() => setActiveView(null)}>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" form="skill-form">
|
||||
@@ -80,7 +80,6 @@ export default function SkillsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Inline GitHub import view
|
||||
if (activeView === 'github') {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
@@ -88,7 +87,7 @@ export default function SkillsPage() {
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('skills.importFromGithub')}
|
||||
</h1>
|
||||
<Button variant="outline" onClick={() => setActiveView(null)}>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -104,13 +103,12 @@ export default function SkillsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Inline upload ZIP view
|
||||
if (activeView === 'upload') {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('skills.uploadZip')}</h1>
|
||||
<Button variant="outline" onClick={() => setActiveView(null)}>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -126,10 +124,6 @@ export default function SkillsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Default: no selection
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<p>{t('skills.selectFromSidebar')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
navigate('/home/add-extension');
|
||||
return null;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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サーバーを削除してもよろしいですか?',
|
||||
|
||||
@@ -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-сервер?',
|
||||
|
||||
@@ -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 นี้?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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 包',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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([
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/home/add-extension',
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<HomeLayout>
|
||||
<AddExtensionPage />
|
||||
</HomeLayout>
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/home/add-plugin',
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<HomeLayout>
|
||||
<AddPluginPage />
|
||||
</HomeLayout>
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/home/market',
|
||||
element: (
|
||||
|
||||
Reference in New Issue
Block a user