mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 11:29:39 +08:00
feat: add supports for install plugin from GitHub repo releases
Add GitHub release installation for plugins
This commit is contained in:
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import quart
|
import quart
|
||||||
|
import re
|
||||||
|
import httpx
|
||||||
|
|
||||||
from .....core import taskmgr
|
from .....core import taskmgr
|
||||||
from .. import group
|
from .. import group
|
||||||
@@ -48,7 +50,9 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
delete_data = quart.request.args.get('delete_data', 'false').lower() == 'true'
|
delete_data = quart.request.args.get('delete_data', 'false').lower() == 'true'
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
wrapper = self.ap.task_mgr.create_user_task(
|
||||||
self.ap.plugin_connector.delete_plugin(author, plugin_name, delete_data=delete_data, task_context=ctx),
|
self.ap.plugin_connector.delete_plugin(
|
||||||
|
author, plugin_name, delete_data=delete_data, task_context=ctx
|
||||||
|
),
|
||||||
kind='plugin-operation',
|
kind='plugin-operation',
|
||||||
name=f'plugin-remove-{plugin_name}',
|
name=f'plugin-remove-{plugin_name}',
|
||||||
label=f'Removing plugin {plugin_name}',
|
label=f'Removing plugin {plugin_name}',
|
||||||
@@ -90,23 +94,145 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
|
|
||||||
return quart.Response(icon_data, mimetype=mime_type)
|
return quart.Response(icon_data, mimetype=mime_type)
|
||||||
|
|
||||||
|
@self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
|
async def _() -> str:
|
||||||
|
"""Get releases from a GitHub repository URL"""
|
||||||
|
data = await quart.request.json
|
||||||
|
repo_url = data.get('repo_url', '')
|
||||||
|
|
||||||
|
# Parse GitHub repository URL to extract owner and repo
|
||||||
|
# Supports: https://github.com/owner/repo or github.com/owner/repo
|
||||||
|
pattern = r'github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$'
|
||||||
|
match = re.search(pattern, repo_url)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
return self.http_status(400, -1, 'Invalid GitHub repository URL')
|
||||||
|
|
||||||
|
owner, repo = match.groups()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch releases from GitHub API
|
||||||
|
url = f'https://api.github.com/repos/{owner}/{repo}/releases'
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
trust_env=True,
|
||||||
|
follow_redirects=True,
|
||||||
|
timeout=10,
|
||||||
|
) as client:
|
||||||
|
response = await client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
releases = response.json()
|
||||||
|
|
||||||
|
# Format releases data for frontend
|
||||||
|
formatted_releases = []
|
||||||
|
for release in releases:
|
||||||
|
formatted_releases.append(
|
||||||
|
{
|
||||||
|
'id': release['id'],
|
||||||
|
'tag_name': release['tag_name'],
|
||||||
|
'name': release['name'],
|
||||||
|
'published_at': release['published_at'],
|
||||||
|
'prerelease': release['prerelease'],
|
||||||
|
'draft': release['draft'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.success(data={'releases': formatted_releases, 'owner': owner, 'repo': repo})
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
return self.http_status(500, -1, f'Failed to fetch releases: {str(e)}')
|
||||||
|
|
||||||
|
@self.route(
|
||||||
|
'/github/release-assets',
|
||||||
|
methods=['POST'],
|
||||||
|
auth_type=group.AuthType.USER_TOKEN,
|
||||||
|
)
|
||||||
|
async def _() -> str:
|
||||||
|
"""Get assets from a specific GitHub release"""
|
||||||
|
data = await quart.request.json
|
||||||
|
owner = data.get('owner', '')
|
||||||
|
repo = data.get('repo', '')
|
||||||
|
release_id = data.get('release_id', '')
|
||||||
|
|
||||||
|
if not all([owner, repo, release_id]):
|
||||||
|
return self.http_status(400, -1, 'Missing required parameters')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch release assets from GitHub API
|
||||||
|
url = f'https://api.github.com/repos/{owner}/{repo}/releases/{release_id}'
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
trust_env=True,
|
||||||
|
follow_redirects=True,
|
||||||
|
timeout=10,
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
release = response.json()
|
||||||
|
|
||||||
|
# Format assets data for frontend
|
||||||
|
formatted_assets = []
|
||||||
|
for asset in release.get('assets', []):
|
||||||
|
formatted_assets.append(
|
||||||
|
{
|
||||||
|
'id': asset['id'],
|
||||||
|
'name': asset['name'],
|
||||||
|
'size': asset['size'],
|
||||||
|
'download_url': asset['browser_download_url'],
|
||||||
|
'content_type': asset['content_type'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# add zipball as a downloadable asset
|
||||||
|
# formatted_assets.append(
|
||||||
|
# {
|
||||||
|
# "id": 0,
|
||||||
|
# "name": "Source code (zip)",
|
||||||
|
# "size": -1,
|
||||||
|
# "download_url": release["zipball_url"],
|
||||||
|
# "content_type": "application/zip",
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
return self.success(data={'assets': formatted_assets})
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
return self.http_status(500, -1, f'Failed to fetch release assets: {str(e)}')
|
||||||
|
|
||||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
|
"""Install plugin from GitHub release asset"""
|
||||||
data = await quart.request.json
|
data = await quart.request.json
|
||||||
|
asset_url = data.get('asset_url', '')
|
||||||
|
owner = data.get('owner', '')
|
||||||
|
repo = data.get('repo', '')
|
||||||
|
release_tag = data.get('release_tag', '')
|
||||||
|
|
||||||
|
if not asset_url:
|
||||||
|
return self.http_status(400, -1, 'Missing asset_url parameter')
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
short_source_str = data['source'][-8:]
|
install_info = {
|
||||||
|
'asset_url': asset_url,
|
||||||
|
'owner': owner,
|
||||||
|
'repo': repo,
|
||||||
|
'release_tag': release_tag,
|
||||||
|
'github_url': f'https://github.com/{owner}/{repo}',
|
||||||
|
}
|
||||||
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
wrapper = self.ap.task_mgr.create_user_task(
|
||||||
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
|
self.ap.plugin_connector.install_plugin(PluginInstallSource.GITHUB, install_info, task_context=ctx),
|
||||||
kind='plugin-operation',
|
kind='plugin-operation',
|
||||||
name='plugin-install-github',
|
name='plugin-install-github',
|
||||||
label=f'Installing plugin from github ...{short_source_str}',
|
label=f'Installing plugin from GitHub {owner}/{repo}@{release_tag}',
|
||||||
context=ctx,
|
context=ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
return self.success(data={'task_id': wrapper.id})
|
||||||
|
|
||||||
@self.route('/install/marketplace', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
@self.route(
|
||||||
|
'/install/marketplace',
|
||||||
|
methods=['POST'],
|
||||||
|
auth_type=group.AuthType.USER_TOKEN,
|
||||||
|
)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
data = await quart.request.json
|
data = await quart.request.json
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,24 @@ from typing import Any
|
|||||||
import typing
|
import typing
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import httpx
|
||||||
from async_lru import alru_cache
|
from async_lru import alru_cache
|
||||||
|
|
||||||
from ..core import app
|
from ..core import app
|
||||||
from . import handler
|
from . import handler
|
||||||
from ..utils import platform
|
from ..utils import platform
|
||||||
from langbot_plugin.runtime.io.controllers.stdio import client as stdio_client_controller
|
from langbot_plugin.runtime.io.controllers.stdio import (
|
||||||
|
client as stdio_client_controller,
|
||||||
|
)
|
||||||
from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller
|
from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller
|
||||||
from langbot_plugin.api.entities import events
|
from langbot_plugin.api.entities import events
|
||||||
from langbot_plugin.api.entities import context
|
from langbot_plugin.api.entities import context
|
||||||
import langbot_plugin.runtime.io.connection as base_connection
|
import langbot_plugin.runtime.io.connection as base_connection
|
||||||
from langbot_plugin.api.definition.components.manifest import ComponentManifest
|
from langbot_plugin.api.definition.components.manifest import ComponentManifest
|
||||||
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
|
from langbot_plugin.api.entities.builtin.command import (
|
||||||
|
context as command_context,
|
||||||
|
errors as command_errors,
|
||||||
|
)
|
||||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||||
from ..core import taskmgr
|
from ..core import taskmgr
|
||||||
|
|
||||||
@@ -71,7 +76,9 @@ class PluginRuntimeConnector:
|
|||||||
return
|
return
|
||||||
|
|
||||||
async def new_connection_callback(connection: base_connection.Connection):
|
async def new_connection_callback(connection: base_connection.Connection):
|
||||||
async def disconnect_callback(rchandler: handler.RuntimeConnectionHandler) -> bool:
|
async def disconnect_callback(
|
||||||
|
rchandler: handler.RuntimeConnectionHandler,
|
||||||
|
) -> bool:
|
||||||
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime():
|
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime():
|
||||||
self.ap.logger.error('Disconnected from plugin runtime, trying to reconnect...')
|
self.ap.logger.error('Disconnected from plugin runtime, trying to reconnect...')
|
||||||
await self.runtime_disconnect_callback(self)
|
await self.runtime_disconnect_callback(self)
|
||||||
@@ -98,7 +105,8 @@ class PluginRuntimeConnector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def make_connection_failed_callback(
|
async def make_connection_failed_callback(
|
||||||
ctrl: ws_client_controller.WebSocketClientController, exc: Exception = None
|
ctrl: ws_client_controller.WebSocketClientController,
|
||||||
|
exc: Exception = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if exc is not None:
|
if exc is not None:
|
||||||
self.ap.logger.error(f'Failed to connect to plugin runtime({ws_url}): {exc}')
|
self.ap.logger.error(f'Failed to connect to plugin runtime({ws_url}): {exc}')
|
||||||
@@ -150,6 +158,25 @@ class PluginRuntimeConnector:
|
|||||||
install_info['plugin_file_key'] = file_key
|
install_info['plugin_file_key'] = file_key
|
||||||
del install_info['plugin_file']
|
del install_info['plugin_file']
|
||||||
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
|
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
|
||||||
|
elif install_source == PluginInstallSource.GITHUB:
|
||||||
|
# download and transfer file
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
trust_env=True,
|
||||||
|
follow_redirects=True,
|
||||||
|
timeout=20,
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
install_info['asset_url'],
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
file_bytes = response.content
|
||||||
|
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')
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.error(f'Failed to download file from GitHub: {e}')
|
||||||
|
raise Exception(f'Failed to download file from GitHub: {e}')
|
||||||
|
|
||||||
async for ret in self.handler.install_plugin(install_source.value, install_info):
|
async for ret in self.handler.install_plugin(install_source.value, install_info):
|
||||||
current_action = ret.get('current_action', None)
|
current_action = ret.get('current_action', None)
|
||||||
@@ -163,7 +190,10 @@ class PluginRuntimeConnector:
|
|||||||
task_context.trace(trace)
|
task_context.trace(trace)
|
||||||
|
|
||||||
async def upgrade_plugin(
|
async def upgrade_plugin(
|
||||||
self, plugin_author: str, plugin_name: str, task_context: taskmgr.TaskContext | None = None
|
self,
|
||||||
|
plugin_author: str,
|
||||||
|
plugin_name: str,
|
||||||
|
task_context: taskmgr.TaskContext | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
async for ret in self.handler.upgrade_plugin(plugin_author, plugin_name):
|
async for ret in self.handler.upgrade_plugin(plugin_author, plugin_name):
|
||||||
current_action = ret.get('current_action', None)
|
current_action = ret.get('current_action', None)
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import MCPDeleteConfirmDialog from '@/app/home/plugins/mcp-server/mcp-form/MCPDe
|
|||||||
import styles from './plugins.module.css';
|
import styles from './plugins.module.css';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
} from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
@@ -16,6 +22,8 @@ import {
|
|||||||
StoreIcon,
|
StoreIcon,
|
||||||
Download,
|
Download,
|
||||||
Power,
|
Power,
|
||||||
|
Github,
|
||||||
|
ChevronLeft,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -41,11 +49,30 @@ import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
|||||||
|
|
||||||
enum PluginInstallStatus {
|
enum PluginInstallStatus {
|
||||||
WAIT_INPUT = 'wait_input',
|
WAIT_INPUT = 'wait_input',
|
||||||
|
SELECT_RELEASE = 'select_release',
|
||||||
|
SELECT_ASSET = 'select_asset',
|
||||||
ASK_CONFIRM = 'ask_confirm',
|
ASK_CONFIRM = 'ask_confirm',
|
||||||
INSTALLING = 'installing',
|
INSTALLING = 'installing',
|
||||||
ERROR = 'error',
|
ERROR = 'error',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GithubRelease {
|
||||||
|
id: number;
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
prerelease: boolean;
|
||||||
|
draft: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GithubAsset {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
download_url: string;
|
||||||
|
content_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function PluginConfigPage() {
|
export default function PluginConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState('installed');
|
const [activeTab, setActiveTab] = useState('installed');
|
||||||
@@ -57,6 +84,16 @@ export default function PluginConfigPage() {
|
|||||||
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
|
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
|
||||||
const [installError, setInstallError] = useState<string | null>(null);
|
const [installError, setInstallError] = useState<string | null>(null);
|
||||||
const [githubURL, setGithubURL] = useState('');
|
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 [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [pluginSystemStatus, setPluginSystemStatus] =
|
const [pluginSystemStatus, setPluginSystemStatus] =
|
||||||
useState<ApiRespPluginSystemStatus | null>(null);
|
useState<ApiRespPluginSystemStatus | null>(null);
|
||||||
@@ -86,6 +123,14 @@ export default function PluginConfigPage() {
|
|||||||
fetchPluginSystemStatus();
|
fetchPluginSystemStatus();
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
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 watchTask(taskId: number) {
|
function watchTask(taskId: number) {
|
||||||
let alreadySuccess = false;
|
let alreadySuccess = false;
|
||||||
|
|
||||||
@@ -101,7 +146,7 @@ export default function PluginConfigPage() {
|
|||||||
toast.success(t('plugins.installSuccess'));
|
toast.success(t('plugins.installSuccess'));
|
||||||
alreadySuccess = true;
|
alreadySuccess = true;
|
||||||
}
|
}
|
||||||
setGithubURL('');
|
resetGithubState();
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
pluginInstalledRef.current?.refreshPluginList();
|
pluginInstalledRef.current?.refreshPluginList();
|
||||||
}
|
}
|
||||||
@@ -112,52 +157,143 @@ export default function PluginConfigPage() {
|
|||||||
|
|
||||||
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
||||||
|
|
||||||
function handleModalConfirm() {
|
function resetGithubState() {
|
||||||
installPlugin(installSource, installInfo as Record<string, unknown>);
|
setGithubURL('');
|
||||||
|
setGithubReleases([]);
|
||||||
|
setSelectedRelease(null);
|
||||||
|
setGithubAssets([]);
|
||||||
|
setSelectedAsset(null);
|
||||||
|
setGithubOwner('');
|
||||||
|
setGithubRepo('');
|
||||||
|
setFetchingReleases(false);
|
||||||
|
setFetchingAssets(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const installPlugin = useCallback(
|
async function fetchGithubReleases() {
|
||||||
(installSource: string, installInfo: Record<string, unknown>) => {
|
if (!githubURL.trim()) {
|
||||||
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
toast.error(t('plugins.enterRepoUrl'));
|
||||||
if (installSource === 'github') {
|
return;
|
||||||
httpClient
|
}
|
||||||
.installPluginFromGithub((installInfo as { url: string }).url)
|
|
||||||
.then((resp) => {
|
setFetchingReleases(true);
|
||||||
const taskId = resp.task_id;
|
setInstallError(null);
|
||||||
watchTask(taskId);
|
|
||||||
})
|
try {
|
||||||
.catch((err) => {
|
const result = await httpClient.getGithubReleases(githubURL);
|
||||||
console.log('error when install plugin:', err);
|
setGithubReleases(result.releases);
|
||||||
setInstallError(err.message);
|
setGithubOwner(result.owner);
|
||||||
setPluginInstallStatus(PluginInstallStatus.ERROR);
|
setGithubRepo(result.repo);
|
||||||
});
|
|
||||||
} else if (installSource === 'local') {
|
if (result.releases.length === 0) {
|
||||||
httpClient
|
toast.warning(t('plugins.noReleasesFound'));
|
||||||
.installPluginFromLocal((installInfo as { file: File }).file)
|
} else {
|
||||||
.then((resp) => {
|
setPluginInstallStatus(PluginInstallStatus.SELECT_RELEASE);
|
||||||
const taskId = resp.task_id;
|
|
||||||
watchTask(taskId);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('error when install plugin:', err);
|
|
||||||
setInstallError(err.message);
|
|
||||||
setPluginInstallStatus(PluginInstallStatus.ERROR);
|
|
||||||
});
|
|
||||||
} else if (installSource === 'marketplace') {
|
|
||||||
httpClient
|
|
||||||
.installPluginFromMarketplace(
|
|
||||||
(installInfo as { plugin_author: string }).plugin_author,
|
|
||||||
(installInfo as { plugin_name: string }).plugin_name,
|
|
||||||
(installInfo as { plugin_version: string }).plugin_version,
|
|
||||||
)
|
|
||||||
.then((resp) => {
|
|
||||||
const taskId = resp.task_id;
|
|
||||||
watchTask(taskId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
} catch (error: unknown) {
|
||||||
[watchTask],
|
console.error('Failed to fetch GitHub releases:', error);
|
||||||
);
|
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,
|
||||||
|
);
|
||||||
|
setGithubAssets(result.assets);
|
||||||
|
|
||||||
|
if (result.assets.length === 0) {
|
||||||
|
toast.warning(t('plugins.noAssetsFound'));
|
||||||
|
} else {
|
||||||
|
setPluginInstallStatus(PluginInstallStatus.SELECT_ASSET);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Failed to fetch GitHub release assets:', error);
|
||||||
|
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 handleModalConfirm() {
|
||||||
|
if (installSource === 'github' && selectedAsset && selectedRelease) {
|
||||||
|
installPlugin('github', {
|
||||||
|
asset_url: selectedAsset.download_url,
|
||||||
|
owner: githubOwner,
|
||||||
|
repo: githubRepo,
|
||||||
|
release_tag: selectedRelease.tag_name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
installPlugin(installSource, installInfo as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installPlugin(
|
||||||
|
installSource: string,
|
||||||
|
installInfo: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
) {
|
||||||
|
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
||||||
|
if (installSource === 'github') {
|
||||||
|
httpClient
|
||||||
|
.installPluginFromGithub(
|
||||||
|
installInfo.asset_url,
|
||||||
|
installInfo.owner,
|
||||||
|
installInfo.repo,
|
||||||
|
installInfo.release_tag,
|
||||||
|
)
|
||||||
|
.then((resp) => {
|
||||||
|
const taskId = resp.task_id;
|
||||||
|
watchTask(taskId);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('error when install plugin:', err);
|
||||||
|
setInstallError(err.message);
|
||||||
|
setPluginInstallStatus(PluginInstallStatus.ERROR);
|
||||||
|
});
|
||||||
|
} else if (installSource === 'local') {
|
||||||
|
httpClient
|
||||||
|
.installPluginFromLocal(installInfo.file)
|
||||||
|
.then((resp) => {
|
||||||
|
const taskId = resp.task_id;
|
||||||
|
watchTask(taskId);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('error when install plugin:', err);
|
||||||
|
setInstallError(err.message);
|
||||||
|
setPluginInstallStatus(PluginInstallStatus.ERROR);
|
||||||
|
});
|
||||||
|
} else if (installSource === 'marketplace') {
|
||||||
|
httpClient
|
||||||
|
.installPluginFromMarketplace(
|
||||||
|
installInfo.plugin_author,
|
||||||
|
installInfo.plugin_name,
|
||||||
|
installInfo.plugin_version,
|
||||||
|
)
|
||||||
|
.then((resp) => {
|
||||||
|
const taskId = resp.task_id;
|
||||||
|
watchTask(taskId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const validateFileType = (file: File): boolean => {
|
const validateFileType = (file: File): boolean => {
|
||||||
const allowedExtensions = ['.lbpkg', '.zip'];
|
const allowedExtensions = ['.lbpkg', '.zip'];
|
||||||
@@ -353,10 +489,6 @@ export default function PluginConfigPage() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem onClick={handleFileSelect}>
|
|
||||||
<UploadIcon className="w-4 h-4" />
|
|
||||||
{t('plugins.uploadLocal')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{systemInfo.enable_marketplace && (
|
{systemInfo.enable_marketplace && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -367,6 +499,22 @@ export default function PluginConfigPage() {
|
|||||||
{t('plugins.marketplace')}
|
{t('plugins.marketplace')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
<DropdownMenuItem onClick={handleFileSelect}>
|
||||||
|
<UploadIcon className="w-4 h-4" />
|
||||||
|
{t('plugins.uploadLocal')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setInstallSource('github');
|
||||||
|
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT);
|
||||||
|
setInstallError(null);
|
||||||
|
resetGithubState();
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
{t('plugins.installFromGithub')}
|
||||||
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -402,49 +550,247 @@ export default function PluginConfigPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
<Dialog
|
||||||
<DialogContent className="w-[500px] p-6 bg-white dark:bg-[#1a1a1e]">
|
open={modalOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setModalOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
resetGithubState();
|
||||||
|
setInstallError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="w-[500px] max-h-[80vh] p-6 bg-white dark:bg-[#1a1a1e] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-4">
|
<DialogTitle className="flex items-center gap-4">
|
||||||
<Download className="size-6" />
|
{installSource === 'github' ? (
|
||||||
|
<Github className="size-6" />
|
||||||
|
) : (
|
||||||
|
<Download className="size-6" />
|
||||||
|
)}
|
||||||
<span>{t('plugins.installPlugin')}</span>
|
<span>{t('plugins.installPlugin')}</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
|
||||||
<div className="mt-4">
|
{/* GitHub Install Flow */}
|
||||||
<p className="mb-2">{t('plugins.onlySupportGithub')}</p>
|
{installSource === 'github' &&
|
||||||
<Input
|
pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
||||||
placeholder={t('plugins.enterGithubLink')}
|
<div className="mt-4">
|
||||||
value={githubURL}
|
<p className="mb-2">{t('plugins.enterRepoUrl')}</p>
|
||||||
onChange={(e) => setGithubURL(e.target.value)}
|
<Input
|
||||||
className="mb-4"
|
placeholder={t('plugins.repoUrlPlaceholder')}
|
||||||
/>
|
value={githubURL}
|
||||||
</div>
|
onChange={(e) => setGithubURL(e.target.value)}
|
||||||
)}
|
className="mb-4"
|
||||||
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
/>
|
||||||
<div className="mt-4">
|
{fetchingReleases && (
|
||||||
<p className="mb-2">
|
<p className="text-sm text-gray-500">
|
||||||
{t('plugins.askConfirm', {
|
{t('plugins.fetchingReleases')}
|
||||||
name: installInfo.plugin_name,
|
</p>
|
||||||
version: installInfo.plugin_version,
|
)}
|
||||||
})}
|
</div>
|
||||||
</p>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
{installSource === 'github' &&
|
||||||
|
pluginInstallStatus === PluginInstallStatus.SELECT_RELEASE && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<p className="font-medium">{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-gray-500 mt-4">
|
||||||
|
{t('plugins.loading')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{installSource === 'github' &&
|
||||||
|
pluginInstallStatus === PluginInstallStatus.SELECT_ASSET && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<p className="font-medium">{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-4 p-2 bg-gray-50 dark:bg-gray-900 rounded">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{selectedRelease.name || selectedRelease.tag_name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Marketplace Install Confirm */}
|
||||||
|
{installSource === 'marketplace' &&
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* GitHub Install Confirm */}
|
||||||
|
{installSource === 'github' &&
|
||||||
|
pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<p className="font-medium">{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-gray-50 dark:bg-gray-900 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Installing State */}
|
||||||
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="mb-2">{t('plugins.installing')}</p>
|
<p className="mb-2">{t('plugins.installing')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="mb-2">{t('plugins.installFailed')}</p>
|
<p className="mb-2">{t('plugins.installFailed')}</p>
|
||||||
<p className="mb-2 text-red-500">{installError}</p>
|
<p className="mb-2 text-red-500">{installError}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
{(pluginInstallStatus === PluginInstallStatus.WAIT_INPUT ||
|
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT &&
|
||||||
pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM) && (
|
installSource === 'github' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setModalOpen(false);
|
||||||
|
resetGithubState();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={fetchGithubReleases}
|
||||||
|
disabled={!githubURL.trim() || fetchingReleases}
|
||||||
|
>
|
||||||
|
{fetchingReleases
|
||||||
|
? t('plugins.loading')
|
||||||
|
: t('common.confirm')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
|
|||||||
@@ -454,9 +454,52 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public installPluginFromGithub(
|
public installPluginFromGithub(
|
||||||
source: string,
|
assetUrl: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
releaseTag: string,
|
||||||
): Promise<AsyncTaskCreatedResp> {
|
): Promise<AsyncTaskCreatedResp> {
|
||||||
return this.post('/api/v1/plugins/install/github', { source });
|
return this.post('/api/v1/plugins/install/github', {
|
||||||
|
asset_url: assetUrl,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
release_tag: releaseTag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGithubReleases(repoUrl: string): Promise<{
|
||||||
|
releases: Array<{
|
||||||
|
id: number;
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
prerelease: boolean;
|
||||||
|
draft: boolean;
|
||||||
|
}>;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
}> {
|
||||||
|
return this.post('/api/v1/plugins/github/releases', { repo_url: repoUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGithubReleaseAssets(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
releaseId: number,
|
||||||
|
): Promise<{
|
||||||
|
assets: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
download_url: string;
|
||||||
|
content_type: string;
|
||||||
|
}>;
|
||||||
|
}> {
|
||||||
|
return this.post('/api/v1/plugins/github/release-assets', {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
release_id: releaseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public installPluginFromLocal(file: File): Promise<AsyncTaskCreatedResp> {
|
public installPluginFromLocal(file: File): Promise<AsyncTaskCreatedResp> {
|
||||||
|
|||||||
@@ -242,6 +242,26 @@ const enUS = {
|
|||||||
saveConfigSuccessDebugPlugin:
|
saveConfigSuccessDebugPlugin:
|
||||||
'Configuration saved successfully, please manually restart the plugin',
|
'Configuration saved successfully, please manually restart the plugin',
|
||||||
saveConfigError: 'Configuration save failed: ',
|
saveConfigError: 'Configuration save failed: ',
|
||||||
|
installFromGithub: 'From GitHub',
|
||||||
|
enterRepoUrl: 'Enter GitHub repository URL',
|
||||||
|
repoUrlPlaceholder: 'e.g., https://github.com/owner/repo',
|
||||||
|
fetchingReleases: 'Fetching releases...',
|
||||||
|
selectRelease: 'Select Release',
|
||||||
|
noReleasesFound: 'No releases found',
|
||||||
|
fetchReleasesError: 'Failed to fetch releases: ',
|
||||||
|
selectAsset: 'Select file to install',
|
||||||
|
noAssetsFound: 'No .lbpkg files available in this release',
|
||||||
|
fetchAssetsError: 'Failed to fetch assets: ',
|
||||||
|
backToReleases: 'Back to releases',
|
||||||
|
backToRepoUrl: 'Back to repository URL',
|
||||||
|
backToAssets: 'Back to assets',
|
||||||
|
releaseTag: 'Tag: {{tag}}',
|
||||||
|
releaseName: 'Name: {{name}}',
|
||||||
|
publishedAt: 'Published at: {{date}}',
|
||||||
|
prerelease: 'Pre-release',
|
||||||
|
assetSize: 'Size: {{size}}',
|
||||||
|
confirmInstall: 'Confirm Install',
|
||||||
|
installFromGithubDesc: 'Install plugin from GitHub Release',
|
||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'Search plugins...',
|
searchPlaceholder: 'Search plugins...',
|
||||||
|
|||||||
@@ -242,6 +242,26 @@ const jaJP = {
|
|||||||
saveConfigSuccessDebugPlugin:
|
saveConfigSuccessDebugPlugin:
|
||||||
'設定を保存しました。手動でプラグインを再起動してください',
|
'設定を保存しました。手動でプラグインを再起動してください',
|
||||||
saveConfigError: '設定の保存に失敗しました:',
|
saveConfigError: '設定の保存に失敗しました:',
|
||||||
|
installFromGithub: 'GitHubから',
|
||||||
|
enterRepoUrl: 'GitHubリポジトリのURLを入力してください',
|
||||||
|
repoUrlPlaceholder: '例: https://github.com/owner/repo',
|
||||||
|
fetchingReleases: 'リリース一覧を取得中...',
|
||||||
|
selectRelease: 'リリースを選択',
|
||||||
|
noReleasesFound: 'リリースが見つかりません',
|
||||||
|
fetchReleasesError: 'リリース一覧の取得に失敗しました:',
|
||||||
|
selectAsset: 'インストールするファイルを選択',
|
||||||
|
noAssetsFound: 'このリリースには利用可能な .lbpkg ファイルがありません',
|
||||||
|
fetchAssetsError: 'ファイル一覧の取得に失敗しました:',
|
||||||
|
backToReleases: 'リリース一覧に戻る',
|
||||||
|
backToRepoUrl: 'リポジトリURLに戻る',
|
||||||
|
backToAssets: 'ファイル選択に戻る',
|
||||||
|
releaseTag: 'タグ: {{tag}}',
|
||||||
|
releaseName: '名前: {{name}}',
|
||||||
|
publishedAt: '公開日: {{date}}',
|
||||||
|
prerelease: 'プレリリース',
|
||||||
|
assetSize: 'サイズ: {{size}}',
|
||||||
|
confirmInstall: 'インストールを確認',
|
||||||
|
installFromGithubDesc: 'GitHubリリースからプラグインをインストール',
|
||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: 'プラグインを検索...',
|
searchPlaceholder: 'プラグインを検索...',
|
||||||
|
|||||||
@@ -230,6 +230,26 @@ const zhHans = {
|
|||||||
saveConfigSuccessNormal: '保存配置成功',
|
saveConfigSuccessNormal: '保存配置成功',
|
||||||
saveConfigSuccessDebugPlugin: '保存配置成功,请手动重启插件',
|
saveConfigSuccessDebugPlugin: '保存配置成功,请手动重启插件',
|
||||||
saveConfigError: '保存配置失败:',
|
saveConfigError: '保存配置失败:',
|
||||||
|
installFromGithub: '来自 GitHub',
|
||||||
|
enterRepoUrl: '请输入 GitHub 仓库地址',
|
||||||
|
repoUrlPlaceholder: '例如: https://github.com/owner/repo',
|
||||||
|
fetchingReleases: '正在获取 Release 列表...',
|
||||||
|
selectRelease: '选择 Release',
|
||||||
|
noReleasesFound: '未找到 Release',
|
||||||
|
fetchReleasesError: '获取 Release 列表失败:',
|
||||||
|
selectAsset: '选择要安装的文件',
|
||||||
|
noAssetsFound: '该 Release 没有可用的 .lbpkg 文件',
|
||||||
|
fetchAssetsError: '获取文件列表失败:',
|
||||||
|
backToReleases: '返回 Release 列表',
|
||||||
|
backToRepoUrl: '返回仓库地址',
|
||||||
|
backToAssets: '返回文件选择',
|
||||||
|
releaseTag: 'Tag: {{tag}}',
|
||||||
|
releaseName: '名称: {{name}}',
|
||||||
|
publishedAt: '发布于: {{date}}',
|
||||||
|
prerelease: '预发布',
|
||||||
|
assetSize: '大小: {{size}}',
|
||||||
|
confirmInstall: '确认安装',
|
||||||
|
installFromGithubDesc: '从 GitHub Release 安装插件',
|
||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: '搜索插件...',
|
searchPlaceholder: '搜索插件...',
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ const zhHant = {
|
|||||||
marketplace: 'Marketplace',
|
marketplace: 'Marketplace',
|
||||||
arrange: '編排',
|
arrange: '編排',
|
||||||
install: '安裝',
|
install: '安裝',
|
||||||
installFromGithub: '從 GitHub 安裝外掛',
|
installFromGithub: '來自 GitHub',
|
||||||
onlySupportGithub: '目前僅支援從 GitHub 安裝',
|
onlySupportGithub: '目前僅支援從 GitHub 安裝',
|
||||||
enterGithubLink: '請輸入外掛的Github連結',
|
enterGithubLink: '請輸入外掛的Github連結',
|
||||||
installing: '正在安裝外掛...',
|
installing: '正在安裝外掛...',
|
||||||
@@ -229,6 +229,25 @@ const zhHant = {
|
|||||||
saveConfigSuccessNormal: '儲存配置成功',
|
saveConfigSuccessNormal: '儲存配置成功',
|
||||||
saveConfigSuccessDebugPlugin: '儲存配置成功,請手動重啟插件',
|
saveConfigSuccessDebugPlugin: '儲存配置成功,請手動重啟插件',
|
||||||
saveConfigError: '儲存配置失敗:',
|
saveConfigError: '儲存配置失敗:',
|
||||||
|
enterRepoUrl: '請輸入 GitHub 倉庫地址',
|
||||||
|
repoUrlPlaceholder: '例如: https://github.com/owner/repo',
|
||||||
|
fetchingReleases: '正在獲取 Release 列表...',
|
||||||
|
selectRelease: '選擇 Release',
|
||||||
|
noReleasesFound: '未找到 Release',
|
||||||
|
fetchReleasesError: '獲取 Release 列表失敗:',
|
||||||
|
selectAsset: '選擇要安裝的文件',
|
||||||
|
noAssetsFound: '該 Release 沒有可用的 .lbpkg 文件',
|
||||||
|
fetchAssetsError: '獲取文件列表失敗:',
|
||||||
|
backToReleases: '返回 Release 列表',
|
||||||
|
backToRepoUrl: '返回倉庫地址',
|
||||||
|
backToAssets: '返回文件選擇',
|
||||||
|
releaseTag: 'Tag: {{tag}}',
|
||||||
|
releaseName: '名稱: {{name}}',
|
||||||
|
publishedAt: '發佈於: {{date}}',
|
||||||
|
prerelease: '預發佈',
|
||||||
|
assetSize: '大小: {{size}}',
|
||||||
|
confirmInstall: '確認安裝',
|
||||||
|
installFromGithubDesc: '從 GitHub Release 安裝插件',
|
||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
searchPlaceholder: '搜尋插件...',
|
searchPlaceholder: '搜尋插件...',
|
||||||
|
|||||||
Reference in New Issue
Block a user