feat: refactor market

This commit is contained in:
WangCham
2026-05-09 11:49:44 +08:00
parent f306c762c8
commit fffc862fe6
18 changed files with 1021 additions and 116 deletions

View File

@@ -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}')

View 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>
</>
);
}

View 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,
})}{' '}
&bull;{' '}
{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;
}

View File

@@ -143,7 +143,6 @@ const CREATABLE_CATEGORIES: EntityCategoryId[] = [
'pipelines',
'knowledge',
'mcp',
'plugins',
'skills',
];

View File

@@ -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',
}),
];
];

View File

@@ -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"

View File

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

View File

@@ -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 */}

View File

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

View File

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

View File

@@ -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:

View File

@@ -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サーバーを削除してもよろしいですか',

View File

@@ -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-сервер?',

View File

@@ -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 นี้?',

View File

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

View File

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

View File

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

View File

@@ -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: (