mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-29 20:05:25 +08:00
feat(plugin): upload debug plugin to LangBot Space with optional GitH… (#2612)
* feat(plugin): upload debug plugin to LangBot Space with optional GitHub sync - SDK: reusable in-memory .lbpkg packaging + git sync helper (local git by default, optional repo/token override) - routing: space-upload config/upload/git-sync endpoints; debug plugins resolved via OSS bridge without touching get_plugin_info - frontend: debug-card entry + upload dialog with multi-language metadata, icon upload and changelog - i18n across 8 locales; operation-trace + action allowlist entries * fix(plugin-space-upload): stable dialog width, single-line locale tabs, broader error surfacing - fixed dialog width so toggling the GitHub section no longer resizes it - language tabs render on one line (horizontal scroll when needed) - surface the real upload/git error message (msg field) instead of [object Object] - hint that a missing GitHub repo is auto-created when a token is provided * fix(plugin-space-upload): narrow dialog and reset sm:max-w clamp The base DialogContent applies sm:max-w-lg at the sm breakpoint, which overrode the unprefixed max-w-none and clamped the upload card to 32rem on desktop. Set an explicit sm:max-width and size the card to ~36rem. Locale tabs now wrap instead of scrolling horizontally. * fix * fix(plugin-space-upload): address CI and UX feedback - narrow the upload dialog to ~36rem and reset the sm:max-w clamp - guide the publisher to the Space review page after a successful upload - drop the redundant null comparison flagged by code quality - update the operation-action expectation and Space/LangBot Account wording - ruff format the touched API modules * feat(plugin-space-upload): public repo, editable author/license, card red dot * fix(market): replace yellow download count with update available badge * style(market): unify installed and update indicators into badges * feat(plugin): redesign debug card banner, style debug icon, and prioritize debug plugins * feat(plugin-space-upload): simplify upload dialog layout, add creator verification and market redirection * fix(i18n): use LangBot Account terminology in Space upload dialog Co-authored-by: multica-agent <[email protected]> * style(i18n): reflow reworded locale strings for prettier Co-authored-by: multica-agent <[email protected]> * test(utils): stop timing out the pattern-count cap test on loaded runners Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Hyu <[email protected]> Co-authored-by: RockChinQ <[email protected]> Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
Hyu
RockChinQ
parent
4687fc96e7
commit
4b537973f9
@@ -70,3 +70,6 @@ packaging/fnos/app/desktop/images/
|
||||
packaging/fnos/*.fpk
|
||||
|
||||
r.ps1
|
||||
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
@@ -40,9 +40,11 @@ class ExtensionsRouterGroup(group.RouterGroup):
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
def _sort_key(item: dict) -> str:
|
||||
def _sort_key(item: dict) -> tuple[int, str]:
|
||||
is_debug = 0 if (item.get('type') == 'plugin' and item.get('plugin', {}).get('debug')) else 1
|
||||
name = ''
|
||||
if item['type'] == 'plugin':
|
||||
return (
|
||||
name = (
|
||||
item['plugin']
|
||||
.get('manifest', {})
|
||||
.get('manifest', {})
|
||||
@@ -50,11 +52,11 @@ class ExtensionsRouterGroup(group.RouterGroup):
|
||||
.get('name', '')
|
||||
.lower()
|
||||
)
|
||||
if item['type'] == 'mcp':
|
||||
return (item['server'].get('name') or '').lower()
|
||||
if item['type'] == 'skill':
|
||||
return (item['skill'].get('display_name') or item['skill'].get('name') or '').lower()
|
||||
return ''
|
||||
elif item['type'] == 'mcp':
|
||||
name = (item['server'].get('name') or '').lower()
|
||||
elif item['type'] == 'skill':
|
||||
name = (item['skill'].get('display_name') or item['skill'].get('name') or '').lower()
|
||||
return (is_debug, name)
|
||||
|
||||
extensions: list[dict] = []
|
||||
if isinstance(plugins, list):
|
||||
|
||||
@@ -23,6 +23,7 @@ from ...context import ExecutionContext, RequestContext
|
||||
from .....operation_trace import service as settings_service
|
||||
from .. import group
|
||||
from .....workspace.errors import WorkspaceNotFoundError
|
||||
from langbot_plugin.entities.io.errors import ActionCallError
|
||||
from .....plugin.github import validate_github_plugin_install_info
|
||||
from .....plugin.archive import inspect_plugin_archive_metadata
|
||||
from .....utils import httpclient
|
||||
@@ -436,6 +437,257 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
async def _space_access_token_for_workspace(
|
||||
request_context: RequestContext,
|
||||
) -> str | None:
|
||||
"""Resolve the Workspace owner's or requesting account's LangBot Space token, if bound.
|
||||
|
||||
Every step is best-effort: a Space account is optional for the
|
||||
underlying LangBot install, so an unavailable identity service must
|
||||
not turn the upload page's metadata read into a 500.
|
||||
"""
|
||||
try:
|
||||
user_service = getattr(self.ap, 'user_service', None)
|
||||
space_service = getattr(self.ap, 'space_service', None)
|
||||
if user_service is None or space_service is None:
|
||||
return None
|
||||
if request_context.account_uuid:
|
||||
account = await user_service.get_user_by_uuid(request_context.account_uuid)
|
||||
if account and account.account_type == 'space':
|
||||
token = await space_service.get_valid_access_token(account.user)
|
||||
if token:
|
||||
return token
|
||||
owner = await user_service.get_workspace_owner(request_context.workspace_uuid)
|
||||
if owner is None:
|
||||
return None
|
||||
return await space_service.get_valid_access_token(owner.user)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def _resolve_upload_target(author: str, plugin_name: str) -> dict | None:
|
||||
"""Resolve a plugin for the upload page, preferring debug connections.
|
||||
|
||||
A plugin shown as "debugging" is not persisted as an installation, so
|
||||
it is looked up through the OSS debug bridge first; installed plugins
|
||||
remain a fallback so the page also works for local/marketplace ones.
|
||||
"""
|
||||
connector = self.ap.plugin_connector
|
||||
for resolver in (
|
||||
getattr(connector, 'get_debug_plugin_info', None),
|
||||
getattr(connector, 'get_plugin_info', None),
|
||||
):
|
||||
if resolver is None:
|
||||
continue
|
||||
try:
|
||||
info = await resolver(author, plugin_name)
|
||||
except Exception:
|
||||
self.ap.logger.exception('Failed to resolve plugin %s/%s for upload', author, plugin_name)
|
||||
continue
|
||||
if info:
|
||||
return info
|
||||
return None
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/space-upload/config',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
)
|
||||
async def _(author: str, plugin_name: str, request_context: RequestContext) -> str:
|
||||
"""Metadata and connection state for the "upload to Space" page."""
|
||||
await self._require_authenticated_plugin_runtime_context(request_context)
|
||||
plugin = await _resolve_upload_target(author, plugin_name)
|
||||
if plugin is None:
|
||||
return self.http_status(404, -1, 'plugin not found')
|
||||
|
||||
metadata = plugin.get('manifest', {}).get('manifest', {}).get('metadata', {}) or {}
|
||||
access_token = await _space_access_token_for_workspace(request_context)
|
||||
space_connected = False
|
||||
has_creator_profile = False
|
||||
creator_slug = None
|
||||
author_matches = False
|
||||
|
||||
if access_token:
|
||||
try:
|
||||
space_service = getattr(self.ap, 'space_service', None)
|
||||
if space_service is not None:
|
||||
token_valid, creator_profile = await space_service.get_creator_profile(access_token)
|
||||
if token_valid:
|
||||
space_connected = True
|
||||
if creator_profile and creator_profile.get('slug'):
|
||||
has_creator_profile = True
|
||||
creator_slug = creator_profile['slug']
|
||||
plugin_author = metadata.get('author', author)
|
||||
if plugin_author and creator_slug and plugin_author.lower() == creator_slug.lower():
|
||||
author_matches = True
|
||||
except Exception:
|
||||
self.ap.logger.exception('Failed to retrieve creator profile for space-upload/config')
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'debug': bool(plugin.get('debug')),
|
||||
'metadata': {
|
||||
'author': metadata.get('author', author),
|
||||
'name': metadata.get('name', plugin_name),
|
||||
'label': metadata.get('label'),
|
||||
'description': metadata.get('description'),
|
||||
'version': metadata.get('version'),
|
||||
'repository': metadata.get('repository'),
|
||||
'license': metadata.get('license'),
|
||||
},
|
||||
'space_connected': space_connected,
|
||||
'has_creator_profile': has_creator_profile,
|
||||
'creator_slug': creator_slug,
|
||||
'author_matches': author_matches,
|
||||
'cloud_service_url': self.ap.instance_config.data.get('space', {}).get(
|
||||
'web_url',
|
||||
self.ap.instance_config.data.get('space', {}).get('url', 'https://space.langbot.app'),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/space-upload',
|
||||
methods=['POST'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
)
|
||||
async def _(author: str, plugin_name: str, request_context: RequestContext) -> str:
|
||||
"""Build the debug plugin and publish it to LangBot Space.
|
||||
|
||||
Optionally synchronises the working directory to GitHub first, so the
|
||||
published plugin and the repository stay aligned.
|
||||
"""
|
||||
await self._require_authenticated_plugin_runtime_context(request_context)
|
||||
try:
|
||||
data = await quart.request.get_json(silent=True) or {}
|
||||
except Exception:
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
return self.http_status(400, -1, 'invalid request body')
|
||||
|
||||
overrides = data.get('manifest_overrides') or {}
|
||||
if not isinstance(overrides, dict):
|
||||
return self.http_status(400, -1, 'manifest_overrides must be an object')
|
||||
|
||||
sync_github = data.get('sync_github') is True
|
||||
git_result: dict | None = None
|
||||
|
||||
try:
|
||||
package_result = await self.ap.plugin_connector.get_plugin_package(
|
||||
author,
|
||||
plugin_name,
|
||||
manifest_overrides=overrides,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning('Building package for %s/%s failed: %s', author, plugin_name, exc)
|
||||
message = str(exc)
|
||||
if 'build_plugin_package' in message and 'not found' in message.lower():
|
||||
message = (
|
||||
'The debugging plugin process does not support packaging yet. '
|
||||
'Update the LangBot plugin SDK used by `lbp run` and restart it.'
|
||||
)
|
||||
return self.http_status(400, -1, message)
|
||||
|
||||
package_bytes = package_result.get('package') or b''
|
||||
if not package_bytes:
|
||||
return self.http_status(400, -1, 'Failed to build the plugin package')
|
||||
|
||||
if sync_github:
|
||||
try:
|
||||
git_result = await self.ap.plugin_connector.git_sync_plugin(
|
||||
author,
|
||||
plugin_name,
|
||||
repo_url=str(data.get('repo_url') or ''),
|
||||
token=str(data.get('token') or ''),
|
||||
branch=str(data.get('branch') or ''),
|
||||
commit_message=str(data.get('commit_message') or ''),
|
||||
manifest_overrides=overrides,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning('Git sync failed for %s/%s: %s', author, plugin_name, exc)
|
||||
return self.http_status(400, -1, f'Git sync failed: {exc}')
|
||||
|
||||
access_token = await _space_access_token_for_workspace(request_context)
|
||||
if not access_token:
|
||||
return self.http_status(
|
||||
400,
|
||||
-1,
|
||||
'Please connect a LangBot Space account before uploading plugins',
|
||||
)
|
||||
|
||||
space_service = getattr(self.ap, 'space_service', None)
|
||||
if space_service is not None:
|
||||
token_valid, creator_profile = await space_service.get_creator_profile(access_token)
|
||||
if not token_valid:
|
||||
return self.http_status(401, -1, 'Space login session has expired. Please re-authenticate.')
|
||||
if not creator_profile or not creator_profile.get('slug'):
|
||||
return self.http_status(
|
||||
400, -1, 'Please set up a creator profile on Space before uploading plugins.'
|
||||
)
|
||||
creator_slug = creator_profile.get('slug')
|
||||
if author and creator_slug and author.lower() != creator_slug.lower():
|
||||
return self.http_status(
|
||||
400,
|
||||
-1,
|
||||
f"Plugin author '{author}' does not match your Space creator slug '@{creator_slug}'.",
|
||||
)
|
||||
|
||||
try:
|
||||
submission = await self.ap.space_service.publish_plugin(
|
||||
access_token,
|
||||
package_bytes,
|
||||
filename=package_result.get('filename') or 'plugin.lbpkg',
|
||||
changelog=str(data.get('changelog') or ''),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, -1, str(exc))
|
||||
|
||||
quart.g.operation_log_resource_id = f'{author}/{plugin_name}'
|
||||
return self.success(
|
||||
data={
|
||||
'submission': submission,
|
||||
'git': git_result,
|
||||
'filename': package_result.get('filename', ''),
|
||||
}
|
||||
)
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/space-upload/git-sync',
|
||||
methods=['POST'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
)
|
||||
async def _(author: str, plugin_name: str, request_context: RequestContext) -> str:
|
||||
"""Synchronise only the debug plugin's working directory to GitHub."""
|
||||
await self._require_authenticated_plugin_runtime_context(request_context)
|
||||
try:
|
||||
data = await quart.request.get_json(silent=True) or {}
|
||||
except Exception:
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
return self.http_status(400, -1, 'invalid request body')
|
||||
|
||||
overrides = data.get('manifest_overrides') or {}
|
||||
if not isinstance(overrides, dict):
|
||||
return self.http_status(400, -1, 'manifest_overrides must be an object')
|
||||
|
||||
try:
|
||||
git_result = await self.ap.plugin_connector.git_sync_plugin(
|
||||
author,
|
||||
plugin_name,
|
||||
repo_url=str(data.get('repo_url') or ''),
|
||||
token=str(data.get('token') or ''),
|
||||
branch=str(data.get('branch') or ''),
|
||||
commit_message=str(data.get('commit_message') or ''),
|
||||
manifest_overrides=overrides,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning('Git sync failed for %s/%s: %s', author, plugin_name, exc)
|
||||
return self.http_status(400, -1, str(exc))
|
||||
|
||||
return self.success(data={'git': git_result})
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/upgrade',
|
||||
methods=['POST'],
|
||||
@@ -616,15 +868,17 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
await self._require_authenticated_plugin_runtime_context(request_context)
|
||||
try:
|
||||
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
|
||||
except ValueError as exc:
|
||||
if not icon_data or not icon_data.get('plugin_icon_base64'):
|
||||
return quart.Response('Icon not found', status=404)
|
||||
except (ValueError, ActionCallError) as exc:
|
||||
# A plugin that is not installed for this Workspace is a missing
|
||||
# resource, not a server fault: the WebUI asks for icons by plugin
|
||||
# identity and must be able to fall back silently.
|
||||
if _is_plugin_not_installed(exc):
|
||||
if isinstance(exc, ActionCallError) or _is_plugin_not_installed(exc):
|
||||
return quart.Response('Icon not found', status=404)
|
||||
raise
|
||||
icon_bytes = await asyncio.to_thread(base64.b64decode, icon_data['plugin_icon_base64'])
|
||||
return quart.Response(icon_bytes, mimetype=icon_data['mime_type'])
|
||||
return quart.Response(icon_bytes, mimetype=icon_data.get('mime_type', 'image/svg+xml'))
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/authenticated-assets/<path:filepath>',
|
||||
@@ -642,7 +896,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
asset_path = _normalize_plugin_asset_path(filepath)
|
||||
if asset_path is None:
|
||||
return quart.Response('Asset not found', status=404)
|
||||
asset_data = await self.ap.plugin_connector.get_plugin_assets(author, plugin_name, asset_path)
|
||||
try:
|
||||
asset_data = await self.ap.plugin_connector.get_plugin_assets(author, plugin_name, asset_path)
|
||||
except (ValueError, ActionCallError):
|
||||
return quart.Response('Asset not found', status=404)
|
||||
if not asset_data.get('asset_base64'):
|
||||
return quart.Response('Asset not found', status=404)
|
||||
asset_bytes = await asyncio.to_thread(base64.b64decode, asset_data['asset_base64'])
|
||||
@@ -654,14 +911,15 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
auth_type=group.AuthType.NONE,
|
||||
)
|
||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
||||
await self._require_public_plugin_runtime_context()
|
||||
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
|
||||
icon_base64 = icon_data['plugin_icon_base64']
|
||||
mime_type = icon_data['mime_type']
|
||||
|
||||
icon_data = await asyncio.to_thread(base64.b64decode, icon_base64)
|
||||
|
||||
return quart.Response(icon_data, mimetype=mime_type)
|
||||
try:
|
||||
await self._require_public_plugin_runtime_context()
|
||||
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
|
||||
if not icon_data or not icon_data.get('plugin_icon_base64'):
|
||||
return quart.Response('Icon not found', status=404)
|
||||
icon_bytes = await asyncio.to_thread(base64.b64decode, icon_data['plugin_icon_base64'])
|
||||
return quart.Response(icon_bytes, mimetype=icon_data.get('mime_type', 'image/svg+xml'))
|
||||
except (ValueError, ActionCallError, WorkspaceNotFoundError):
|
||||
return quart.Response('Icon not found', status=404)
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/assets/<path:filepath>',
|
||||
@@ -669,20 +927,23 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
auth_type=group.AuthType.NONE,
|
||||
)
|
||||
async def _(author: str, plugin_name: str, filepath: str) -> quart.Response:
|
||||
await self._require_public_plugin_runtime_context()
|
||||
asset_path = _normalize_plugin_asset_path(filepath)
|
||||
if asset_path is None:
|
||||
return quart.Response('Asset not found', status=404)
|
||||
try:
|
||||
await self._require_public_plugin_runtime_context()
|
||||
asset_path = _normalize_plugin_asset_path(filepath)
|
||||
if asset_path is None:
|
||||
return quart.Response('Asset not found', status=404)
|
||||
|
||||
asset_data = await self.ap.plugin_connector.get_plugin_assets(author, plugin_name, asset_path)
|
||||
if not asset_data.get('asset_base64'):
|
||||
asset_data = await self.ap.plugin_connector.get_plugin_assets(author, plugin_name, asset_path)
|
||||
if not asset_data.get('asset_base64'):
|
||||
return quart.Response('Asset not found', status=404)
|
||||
asset_bytes = await asyncio.to_thread(
|
||||
base64.b64decode,
|
||||
asset_data['asset_base64'],
|
||||
)
|
||||
mime_type = asset_data['mime_type']
|
||||
resp = quart.Response(asset_bytes, mimetype=mime_type)
|
||||
except (ValueError, ActionCallError, WorkspaceNotFoundError):
|
||||
return quart.Response('Asset not found', status=404)
|
||||
asset_bytes = await asyncio.to_thread(
|
||||
base64.b64decode,
|
||||
asset_data['asset_base64'],
|
||||
)
|
||||
mime_type = asset_data['mime_type']
|
||||
resp = quart.Response(asset_bytes, mimetype=mime_type)
|
||||
# CSP for HTML pages served to sandboxed iframes (opaque origin).
|
||||
# 'self' doesn't work in sandboxed iframes — use actual server origin.
|
||||
if mime_type and mime_type.startswith('text/html'):
|
||||
|
||||
@@ -59,6 +59,7 @@ SYSTEM_CAPABILITY_OPERATIONS = (
|
||||
'plugin.install.marketplace',
|
||||
'plugin.install.local',
|
||||
'plugin.upgrade',
|
||||
'plugin.space_upload',
|
||||
'plugin.get',
|
||||
'plugin.list',
|
||||
'plugin.config.get',
|
||||
@@ -227,7 +228,10 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
'enable_marketplace', True
|
||||
),
|
||||
'cloud_service_url': (
|
||||
self.ap.instance_config.data.get('space', {}).get('url', 'https://space.langbot.app')
|
||||
self.ap.instance_config.data.get('space', {}).get(
|
||||
'web_url',
|
||||
self.ap.instance_config.data.get('space', {}).get('url', 'https://space.langbot.app'),
|
||||
)
|
||||
),
|
||||
'allow_modify_login_info': self.ap.instance_config.data.get('system', {}).get(
|
||||
'allow_modify_login_info', True
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from collections import OrderedDict
|
||||
|
||||
from langbot.pkg.utils import httpclient
|
||||
import aiohttp
|
||||
import typing
|
||||
import datetime
|
||||
import time
|
||||
@@ -202,6 +203,33 @@ class SpaceService:
|
||||
return None
|
||||
return await self.get_user_info_raw(access_token)
|
||||
|
||||
async def get_creator_profile(self, access_token: str) -> typing.Tuple[bool, dict | None]:
|
||||
"""Fetch the authenticated user's Creator Profile from Space.
|
||||
|
||||
Returns (is_token_valid, creator_dict_or_none).
|
||||
"""
|
||||
if not access_token:
|
||||
return False, None
|
||||
space_url = self._get_space_config()['url'].rstrip('/')
|
||||
session = httpclient.get_session()
|
||||
try:
|
||||
async with session.get(
|
||||
f'{space_url}/api/v1/creator/profile',
|
||||
headers={'Authorization': f'Bearer {access_token}'},
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
return False, None
|
||||
if response.status != 200:
|
||||
return True, None
|
||||
payload = await httpclient.read_json_limited(response)
|
||||
if payload.get('code') != 0:
|
||||
return True, None
|
||||
data = payload.get('data') or {}
|
||||
return True, data.get('creator')
|
||||
except Exception:
|
||||
self.ap.logger.exception('Failed to fetch creator profile from Space')
|
||||
return True, None
|
||||
|
||||
async def get_credits(self, user_email: str, force_refresh: bool = False) -> int | None:
|
||||
"""Get Space credits for user with caching (60s TTL)"""
|
||||
now = time.time()
|
||||
@@ -285,6 +313,57 @@ class SpaceService:
|
||||
models.append(selection)
|
||||
return [SpaceModelSelection.model_validate(model) for model in models]
|
||||
|
||||
# === Plugin publishing ===
|
||||
|
||||
async def publish_plugin(
|
||||
self,
|
||||
access_token: str,
|
||||
package_bytes: bytes,
|
||||
*,
|
||||
filename: str = 'plugin.lbpkg',
|
||||
changelog: str = '',
|
||||
) -> typing.Dict:
|
||||
"""Publish a built ``.lbpkg`` to the LangBot Space marketplace.
|
||||
|
||||
Mirrors the CLI's ``POST /api/v1/marketplace/plugins/publish`` contract
|
||||
(multipart ``file`` + ``changelog``). The caller supplies a valid Space
|
||||
access token resolved from the Workspace owner.
|
||||
"""
|
||||
|
||||
if not access_token:
|
||||
raise ValueError('A LangBot Space account is required to publish plugins')
|
||||
|
||||
space_url = self._get_space_config()['url'].rstrip('/')
|
||||
|
||||
form = aiohttp.FormData()
|
||||
form.add_field('changelog', changelog or '')
|
||||
form.add_field(
|
||||
'file',
|
||||
package_bytes,
|
||||
filename=filename,
|
||||
content_type='application/octet-stream',
|
||||
)
|
||||
|
||||
session = httpclient.get_session()
|
||||
async with session.post(
|
||||
f'{space_url}/api/v1/marketplace/plugins/publish',
|
||||
data=form,
|
||||
headers={'Authorization': f'Bearer {access_token}'},
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
payload = await httpclient.read_json_limited(response)
|
||||
msg = payload.get('msg') or payload.get('message') or ''
|
||||
except Exception:
|
||||
msg = ''
|
||||
if not msg:
|
||||
msg = await httpclient.read_text_limited(response)
|
||||
raise ValueError(msg or 'Failed to publish plugin')
|
||||
data = await httpclient.read_json_limited(response)
|
||||
if data.get('code') != 0:
|
||||
raise ValueError(data.get('msg') or 'Failed to publish plugin')
|
||||
return data.get('data', {})
|
||||
|
||||
async def get_recommended_chat_model(self, context: typing.Any) -> dict:
|
||||
"""Resolve Space's first ranked chat model to a local Workspace model."""
|
||||
selection = await self.get_model_selection('chat')
|
||||
|
||||
@@ -78,6 +78,9 @@ class Metadata(pydantic.BaseModel):
|
||||
repository: typing.Optional[str] = None
|
||||
"""仓库"""
|
||||
|
||||
license: typing.Optional[str] = None
|
||||
"""开源协议(SPDX 标识符,如 MIT / Apache-2.0)"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
@@ -377,6 +377,14 @@ ACTION_RULE_TABLE: typing.Final[tuple[ActionRule, ...]] = (
|
||||
bucket='write',
|
||||
resource_type='plugin',
|
||||
),
|
||||
# Building a debug plugin package and/or syncing it to GitHub and publishing
|
||||
# it to LangBot Space.
|
||||
ActionRule(
|
||||
action='plugin_publish',
|
||||
category='extension',
|
||||
bucket='write',
|
||||
resource_type='plugin',
|
||||
),
|
||||
ActionRule(
|
||||
action='page_view',
|
||||
category='extension',
|
||||
@@ -609,6 +617,15 @@ _ROUTE_RULES: typing.Final[tuple[tuple[tuple[str, ...], str, str | None, str | N
|
||||
# opaque resource delete. Must precede the ``/config`` rule below, whose
|
||||
# fragment also matches ``config-files``.
|
||||
(('/plugins/', '/config-files'), 'plugin_view', 'plugin_config', 'plugin_config', 'plugin_view'),
|
||||
# The debug-plugin "upload to LangBot Space" surface: building/syncing is a
|
||||
# plugin change, never an opaque view. Must precede the bare plugin rules.
|
||||
(
|
||||
('/plugins/', '/space-upload'),
|
||||
'plugin_view',
|
||||
'plugin_publish',
|
||||
None,
|
||||
'plugin_view',
|
||||
),
|
||||
(('/plugins/', '/config'), 'plugin_view', 'plugin_config', None, 'plugin_view'),
|
||||
(('/plugins/', '/page-api'), 'page_view', 'page_view', None, 'page_view'),
|
||||
(('/plugins/', '/upgrade'), 'plugin_view', 'plugin_upgrade', None, 'plugin_view'),
|
||||
|
||||
@@ -2285,6 +2285,64 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
|
||||
return plugins
|
||||
|
||||
async def _debug_plugin_binding(
|
||||
self,
|
||||
author: str,
|
||||
plugin_name: str,
|
||||
) -> InstallationBinding | None:
|
||||
"""Locate a debug-connected (non-installed) plugin in this Workspace.
|
||||
|
||||
Debug plugins are not persisted as settings, so they are only reachable
|
||||
through the OSS compatibility bridge binding. Returns ``None`` when the
|
||||
identity is not present on that bridge, so the caller can distinguish a
|
||||
genuinely unknown plugin from a permission error.
|
||||
"""
|
||||
|
||||
if self.runtime_profile != 'oss_dev':
|
||||
return None
|
||||
execution_context = await self._current_execution_context()
|
||||
runtime_handler = self._runtime_handler()
|
||||
binding = self._legacy_oss_bridge_binding(execution_context)
|
||||
with runtime_handler.installation_scope(binding):
|
||||
info = await runtime_handler.get_plugin_info(author, plugin_name)
|
||||
if not isinstance(info, dict) or not info.get('manifest'):
|
||||
return None
|
||||
return binding
|
||||
|
||||
async def _plugin_operation_binding(
|
||||
self,
|
||||
author: str,
|
||||
plugin_name: str,
|
||||
) -> InstallationBinding:
|
||||
"""Resolve the binding that owns a plugin, installed or debug-connected."""
|
||||
|
||||
try:
|
||||
return await self._target_binding(author, plugin_name, require_enabled=False)
|
||||
except ValueError as exc:
|
||||
if str(exc) == f'Plugin {author}/{plugin_name} is not installed in this Workspace':
|
||||
binding = await self._debug_plugin_binding(author, plugin_name)
|
||||
if binding is not None:
|
||||
return binding
|
||||
raise
|
||||
|
||||
async def get_debug_plugin_info(
|
||||
self,
|
||||
author: str,
|
||||
plugin_name: str,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Return a debug-connected plugin's info without touching installs.
|
||||
|
||||
This is an additive read used only by the "upload to Space" surface; the
|
||||
installed-plugin lookup in :meth:`get_plugin_info` is left unchanged.
|
||||
"""
|
||||
|
||||
binding = await self._debug_plugin_binding(author, plugin_name)
|
||||
if binding is None:
|
||||
return None
|
||||
runtime_handler = self._runtime_handler()
|
||||
with runtime_handler.installation_scope(binding):
|
||||
return await runtime_handler.get_plugin_info(author, plugin_name)
|
||||
|
||||
async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any] | None:
|
||||
runtime_handler = self._runtime_handler()
|
||||
try:
|
||||
@@ -2397,6 +2455,52 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
return {}
|
||||
return await self._runtime_handler().get_debug_info(execution_context)
|
||||
|
||||
async def get_plugin_package(
|
||||
self,
|
||||
author: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
manifest_overrides: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a ``.lbpkg`` from a debug plugin's live source tree.
|
||||
|
||||
Only debug plugins can be packaged: an installed plugin's on-disk
|
||||
directory is an extracted artifact, not the developer's source tree.
|
||||
"""
|
||||
runtime_handler = self._runtime_handler()
|
||||
binding = await self._plugin_operation_binding(author, plugin_name)
|
||||
with runtime_handler.installation_scope(binding):
|
||||
return await runtime_handler.get_plugin_package(
|
||||
author,
|
||||
plugin_name,
|
||||
manifest_overrides=manifest_overrides,
|
||||
)
|
||||
|
||||
async def git_sync_plugin(
|
||||
self,
|
||||
author: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
repo_url: str = '',
|
||||
token: str = '',
|
||||
branch: str = '',
|
||||
commit_message: str = '',
|
||||
manifest_overrides: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Commit and push a debug plugin's working directory to GitHub."""
|
||||
runtime_handler = self._runtime_handler()
|
||||
binding = await self._plugin_operation_binding(author, plugin_name)
|
||||
with runtime_handler.installation_scope(binding):
|
||||
return await runtime_handler.git_sync_plugin(
|
||||
author,
|
||||
plugin_name,
|
||||
repo_url=repo_url,
|
||||
token=token,
|
||||
branch=branch,
|
||||
commit_message=commit_message,
|
||||
manifest_overrides=manifest_overrides,
|
||||
)
|
||||
|
||||
async def emit_event(
|
||||
self,
|
||||
event: events.BaseEventModel,
|
||||
|
||||
@@ -3294,6 +3294,65 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
)
|
||||
return result
|
||||
|
||||
async def get_plugin_package(
|
||||
self,
|
||||
plugin_author: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
manifest_overrides: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a ``.lbpkg`` from a debug plugin's live source tree.
|
||||
|
||||
The Runtime returns the package over the file-transfer channel; the
|
||||
bytes are read back here so callers receive the package inline.
|
||||
"""
|
||||
result = await self.call_action(
|
||||
_langbot_to_runtime_action('BUILD_PLUGIN_PACKAGE', 'build_plugin_package'),
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
'plugin_name': plugin_name,
|
||||
'manifest_overrides': manifest_overrides or {},
|
||||
},
|
||||
timeout=300,
|
||||
)
|
||||
package_file_key = result.get('package_file_key')
|
||||
package_bytes = b''
|
||||
if package_file_key:
|
||||
package_bytes = await self.read_local_file(package_file_key)
|
||||
await self.delete_local_file(package_file_key)
|
||||
return {
|
||||
'package': package_bytes,
|
||||
'filename': result.get('filename', ''),
|
||||
'metadata': result.get('metadata', {}),
|
||||
'manifest': result.get('manifest', {}),
|
||||
}
|
||||
|
||||
async def git_sync_plugin(
|
||||
self,
|
||||
plugin_author: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
repo_url: str = '',
|
||||
token: str = '',
|
||||
branch: str = '',
|
||||
commit_message: str = '',
|
||||
manifest_overrides: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Commit and push a debug plugin's working directory to GitHub."""
|
||||
return await self.call_action(
|
||||
_langbot_to_runtime_action('GIT_SYNC_PLUGIN', 'git_sync_plugin'),
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
'plugin_name': plugin_name,
|
||||
'repo_url': repo_url,
|
||||
'token': token,
|
||||
'branch': branch,
|
||||
'commit_message': commit_message,
|
||||
'manifest_overrides': manifest_overrides or {},
|
||||
},
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
# ================= RAG Capability Callers (LangBot -> Runtime) =================
|
||||
|
||||
async def rag_ingest_document(
|
||||
|
||||
@@ -530,6 +530,7 @@ async def test_api_key_context_returns_bound_identity_without_workspace_permissi
|
||||
'plugin.config.get',
|
||||
'plugin.config.update',
|
||||
'plugin.logs',
|
||||
'plugin.space_upload',
|
||||
'plugin.delete',
|
||||
'provider.list',
|
||||
'provider.get',
|
||||
|
||||
@@ -75,11 +75,14 @@ async def test_bundled_sensitive_words_fit_within_pattern_limit():
|
||||
@pytest.mark.asyncio
|
||||
async def test_mask_patterns_honors_explicit_pattern_count_cap():
|
||||
patterns = ['a'] * (safe_regex.MAX_PATTERN_COUNT + 6)
|
||||
# The wall-clock deadline is not what this test asserts, and 262 pattern
|
||||
# compilations do not fit the 50ms default on a loaded CI runner.
|
||||
found, masked = await safe_regex.mask_patterns(
|
||||
patterns,
|
||||
'hello',
|
||||
mask='*',
|
||||
mask_word='',
|
||||
timeout_seconds=30.0,
|
||||
max_pattern_count=len(patterns),
|
||||
)
|
||||
assert found is False
|
||||
|
||||
@@ -688,11 +688,14 @@ function NavItems({
|
||||
// Shared entity list renderer used by both popover and collapsible
|
||||
const renderEntityList = (inPopover: boolean) => {
|
||||
const sortedItems = isExtensionsCategory
|
||||
? [...items].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, {
|
||||
? [...items].sort((a, b) => {
|
||||
const aDebug = Boolean(a.debug);
|
||||
const bDebug = Boolean(b.debug);
|
||||
if (aDebug !== bDebug) return aDebug ? -1 : 1;
|
||||
return a.name.localeCompare(b.name, undefined, {
|
||||
sensitivity: 'base',
|
||||
}),
|
||||
)
|
||||
});
|
||||
})
|
||||
: sortByRecent(items);
|
||||
const isExpanded = expandedLists[section]?.[categoryId] ?? false;
|
||||
const maxItems = inPopover ? 10 : MAX_VISIBLE_ITEMS;
|
||||
@@ -833,6 +836,9 @@ function NavItems({
|
||||
{t('bots.legacyAdapterBadge')}
|
||||
</span>
|
||||
)}
|
||||
{item.debug && (
|
||||
<Bug className="size-3.5 shrink-0 !text-orange-500 dark:!text-orange-400 ml-auto" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -924,7 +930,7 @@ function NavItems({
|
||||
</span>
|
||||
)}
|
||||
{item.debug && (
|
||||
<Bug className="size-3.5 shrink-0 text-orange-400" />
|
||||
<Bug className="size-3.5 shrink-0 !text-orange-500 dark:!text-orange-400" />
|
||||
)}
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
|
||||
@@ -228,7 +228,13 @@ export function SidebarDataProvider({
|
||||
pluginMap.set(compositeKey, item);
|
||||
}
|
||||
}
|
||||
setPlugins(Array.from(pluginMap.values()));
|
||||
const pluginItems = Array.from(pluginMap.values()).sort((a, b) => {
|
||||
const aDebug = Boolean(a.debug);
|
||||
const bDebug = Boolean(b.debug);
|
||||
if (aDebug !== bDebug) return aDebug ? -1 : 1;
|
||||
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
setPlugins(pluginItems);
|
||||
|
||||
// Extract plugin pages from spec.pages (deduplicate by id)
|
||||
const pages: PluginPageItem[] = [];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ExtensionCardVO, ExtensionType } from './ExtensionCardVO';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
BugIcon,
|
||||
ExternalLink,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
Server,
|
||||
Sparkles,
|
||||
Puzzle,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { getCloudServiceClientSync, systemInfo } from '@/app/infra/http';
|
||||
import { useInstalledPluginIcon } from '@/app/infra/hooks/useInstalledPluginIcon';
|
||||
@@ -28,6 +30,8 @@ type ExtensionCardComponentProps = {
|
||||
onCardClick: () => void;
|
||||
onDeleteClick: (cardVO: ExtensionCardVO) => void;
|
||||
onUpgradeClick?: (cardVO: ExtensionCardVO) => void;
|
||||
/** Upload a debug-connected plugin to LangBot Space. */
|
||||
onUploadToSpace?: (cardVO: ExtensionCardVO) => void;
|
||||
};
|
||||
|
||||
export default function ExtensionCardComponent({
|
||||
@@ -35,6 +39,7 @@ export default function ExtensionCardComponent({
|
||||
onCardClick,
|
||||
onDeleteClick,
|
||||
onUpgradeClick,
|
||||
onUploadToSpace,
|
||||
}: ExtensionCardComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
@@ -53,6 +58,13 @@ export default function ExtensionCardComponent({
|
||||
const iconSrc = cardVO.type === 'plugin' ? pluginIconURL : cardVO.iconURL;
|
||||
const showFallback = iconFailed || !iconSrc;
|
||||
|
||||
useEffect(() => {
|
||||
setIconFailed(false);
|
||||
}, [iconSrc]);
|
||||
// Debug plugins can always be uploaded to LangBot Space; surface the same
|
||||
// persistent red-dot reminder as an available plugin update.
|
||||
const hasSpaceUpload = cardVO.type === 'plugin' && !!cardVO.debug;
|
||||
|
||||
const getTypeLabel = (type: ExtensionType) => {
|
||||
switch (type) {
|
||||
case 'mcp':
|
||||
@@ -101,15 +113,6 @@ export default function ExtensionCardComponent({
|
||||
v{cardVO.version}
|
||||
</Badge>
|
||||
{renderTypeBadge(cardVO.type)}
|
||||
{cardVO.debug && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400 flex-shrink-0"
|
||||
>
|
||||
<BugIcon className="w-4 h-4" />
|
||||
{t('plugins.debugging')}
|
||||
</Badge>
|
||||
)}
|
||||
{!cardVO.debug && (
|
||||
<>
|
||||
{cardVO.install_source === 'github' && (
|
||||
@@ -221,10 +224,51 @@ export default function ExtensionCardComponent({
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
className="w-full h-[10rem] py-5 px-5 cursor-pointer relative gap-0 shadow-xs transition-shadow duration-200 hover:shadow-md"
|
||||
className={cn(
|
||||
'w-full h-[10rem] cursor-pointer relative p-0 gap-0 shadow-xs transition-all duration-200 hover:shadow-md overflow-hidden flex flex-col',
|
||||
cardVO.debug
|
||||
? 'border-amber-500/40 dark:border-amber-500/35 hover:border-amber-500/70 dark:hover:border-amber-500/60'
|
||||
: '',
|
||||
)}
|
||||
onClick={() => onCardClick()}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
{cardVO.debug && (
|
||||
<div className="w-full h-7 px-4 bg-gradient-to-r from-amber-500/15 via-amber-500/10 to-amber-500/5 dark:from-amber-500/20 dark:via-amber-500/15 dark:to-transparent border-b border-amber-500/20 dark:border-amber-500/30 text-amber-800 dark:text-amber-200 text-xs font-medium flex items-center justify-between flex-shrink-0 select-none">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<BugIcon className="size-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
|
||||
<span className="font-medium text-[0.72rem] tracking-wide truncate">
|
||||
{t('plugins.debugging')}
|
||||
</span>
|
||||
<span className="relative flex size-1.5 flex-shrink-0 ml-0.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-amber-400 opacity-75" />
|
||||
<span className="relative inline-flex size-1.5 rounded-full bg-amber-500" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{cardVO.type === 'plugin' && onUploadToSpace && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-[0.7rem] font-medium text-amber-700 hover:text-amber-950 dark:text-amber-300 dark:hover:text-amber-100 transition-colors cursor-pointer group -mr-1 px-1.5 py-0.5 rounded hover:bg-amber-500/15 dark:hover:bg-amber-500/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUploadToSpace(cardVO);
|
||||
}}
|
||||
>
|
||||
<Upload className="size-3 transition-transform group-hover:-translate-y-0.5" />
|
||||
<span>{t('plugins.spaceUpload.menuAction')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex flex-row items-start justify-start gap-[1.2rem]',
|
||||
cardVO.debug
|
||||
? 'flex-1 min-h-0 px-5 pt-2.5 pb-3.5'
|
||||
: 'h-full py-5 px-5',
|
||||
)}
|
||||
>
|
||||
{showFallback ? (
|
||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center">
|
||||
<FallbackIcon className="w-12 h-12 text-blue-500" />
|
||||
@@ -264,12 +308,27 @@ export default function ExtensionCardComponent({
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</Button>
|
||||
{cardVO.hasUpdate && (
|
||||
{(cardVO.hasUpdate || hasSpaceUpload) && (
|
||||
<div className="absolute -top-0.5 -right-0.5 w-2.5 h-2.5 bg-destructive rounded-full border-2 border-card"></div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{cardVO.type === 'plugin' && cardVO.debug && (
|
||||
<DropdownMenuItem
|
||||
className="relative flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUploadToSpace?.(cardVO);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span>{t('plugins.spaceUpload.menuAction')}</span>
|
||||
{/* Persistent red-dot reminder, matching the update dot. */}
|
||||
<span className="ml-auto w-2 h-2 flex-shrink-0 rounded-full bg-destructive" />
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{cardVO.type === 'plugin' &&
|
||||
cardVO.install_source === 'marketplace' && (
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ExtensionCardVO, ExtensionType } from './ExtensionCardVO';
|
||||
import ExtensionCardComponent from './ExtensionCardComponent';
|
||||
import PluginSpaceUploadDialog from './PluginSpaceUploadDialog';
|
||||
import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
@@ -61,6 +62,19 @@ export const FilterOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
function sortExtensionsWithDebugFirst(
|
||||
items: ExtensionCardVO[],
|
||||
): ExtensionCardVO[] {
|
||||
return [...items].sort((a, b) => {
|
||||
const aDebug = Boolean(a.debug);
|
||||
const bDebug = Boolean(b.debug);
|
||||
if (aDebug !== bDebug) return aDebug ? -1 : 1;
|
||||
return (a.label || a.name).localeCompare(b.label || b.name, undefined, {
|
||||
sensitivity: 'base',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface PluginInstalledComponentProps {
|
||||
filterType: FilterType;
|
||||
groupByType: boolean;
|
||||
@@ -87,6 +101,8 @@ const PluginInstalledComponent = forwardRef<
|
||||
const [targetExtension, setTargetExtension] =
|
||||
useState<ExtensionCardVO | null>(null);
|
||||
const [deleteData, setDeleteData] = useState<boolean>(false);
|
||||
const [spaceUploadTarget, setSpaceUploadTarget] =
|
||||
useState<ExtensionCardVO | null>(null);
|
||||
|
||||
const asyncTask = useAsyncTask({
|
||||
onSuccess: () => {
|
||||
@@ -204,7 +220,7 @@ const PluginInstalledComponent = forwardRef<
|
||||
}
|
||||
}
|
||||
|
||||
setExtensionList(extensions);
|
||||
setExtensionList(sortExtensionsWithDebugFirst(extensions));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch extension list:', error);
|
||||
if (!silent) setExtensionList([]);
|
||||
@@ -326,13 +342,15 @@ const PluginInstalledComponent = forwardRef<
|
||||
// Match the query against the fields a user can actually see on the card
|
||||
// (label / name / author) plus the description, case-insensitively.
|
||||
const normalizedQuery = searchQuery.trim().toLowerCase();
|
||||
const filteredExtensions = extensionList.filter((ext) => {
|
||||
if (filterType !== 'all' && ext.type !== filterType) return false;
|
||||
if (!normalizedQuery) return true;
|
||||
return [ext.label, ext.name, ext.author, ext.description].some((field) =>
|
||||
(field || '').toLowerCase().includes(normalizedQuery),
|
||||
);
|
||||
});
|
||||
const filteredExtensions = sortExtensionsWithDebugFirst(
|
||||
extensionList.filter((ext) => {
|
||||
if (filterType !== 'all' && ext.type !== filterType) return false;
|
||||
if (!normalizedQuery) return true;
|
||||
return [ext.label, ext.name, ext.author, ext.description].some((field) =>
|
||||
(field || '').toLowerCase().includes(normalizedQuery),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const clearSearch = () => {
|
||||
onClearSearch?.();
|
||||
@@ -344,7 +362,9 @@ const PluginInstalledComponent = forwardRef<
|
||||
.map((type) => ({
|
||||
type,
|
||||
labelKey: FilterOptions.find((o) => o.value === type)!.labelKey,
|
||||
items: filteredExtensions.filter((ext) => ext.type === type),
|
||||
items: sortExtensionsWithDebugFirst(
|
||||
filteredExtensions.filter((ext) => ext.type === type),
|
||||
),
|
||||
}))
|
||||
.filter((g) => g.items.length > 0);
|
||||
|
||||
@@ -532,6 +552,7 @@ const PluginInstalledComponent = forwardRef<
|
||||
? () => handleExtensionUpdate(vo)
|
||||
: undefined
|
||||
}
|
||||
onUploadToSpace={() => setSpaceUploadTarget(vo)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -553,12 +574,28 @@ const PluginInstalledComponent = forwardRef<
|
||||
? () => handleExtensionUpdate(vo)
|
||||
: undefined
|
||||
}
|
||||
onUploadToSpace={() => setSpaceUploadTarget(vo)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{spaceUploadTarget && (
|
||||
<PluginSpaceUploadDialog
|
||||
author={spaceUploadTarget.author}
|
||||
name={spaceUploadTarget.name}
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSpaceUploadTarget(null);
|
||||
}}
|
||||
onUploaded={() => {
|
||||
getExtensionList(true);
|
||||
refreshPlugins();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+37
-13
@@ -1,7 +1,8 @@
|
||||
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
BugIcon,
|
||||
ExternalLink,
|
||||
@@ -34,21 +35,53 @@ export default function PluginCardComponent({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [iconFailed, setIconFailed] = useState(false);
|
||||
const iconURL = useInstalledPluginIcon(cardVO.author, cardVO.name);
|
||||
|
||||
useEffect(() => {
|
||||
setIconFailed(false);
|
||||
}, [iconURL]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] border border-[#e4e4e7] dark:border-[#27272a] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22] relative transition-all duration-200 hover:border-[#a1a1aa] dark:hover:border-[#3f3f46]"
|
||||
className={cn(
|
||||
'w-[100%] h-[10rem] bg-white rounded-[10px] border border-[#e4e4e7] dark:border-[#27272a] cursor-pointer dark:bg-[#1f1f22] relative transition-all duration-200 hover:border-[#a1a1aa] dark:hover:border-[#3f3f46] overflow-hidden flex flex-col p-0',
|
||||
cardVO.debug
|
||||
? 'border-amber-500/40 dark:border-amber-500/35 hover:border-amber-500/70 dark:hover:border-amber-500/60'
|
||||
: '',
|
||||
)}
|
||||
onClick={() => onCardClick()}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
{cardVO.debug && (
|
||||
<div className="w-full h-7 px-4 bg-gradient-to-r from-amber-500/15 via-amber-500/10 to-amber-500/5 dark:from-amber-500/20 dark:via-amber-500/15 dark:to-transparent border-b border-amber-500/20 dark:border-amber-500/30 text-amber-800 dark:text-amber-200 text-xs font-medium flex items-center justify-between flex-shrink-0 select-none">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<BugIcon className="size-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
|
||||
<span className="font-medium text-[0.72rem] tracking-wide truncate">
|
||||
{t('plugins.debugging')}
|
||||
</span>
|
||||
<span className="relative flex size-1.5 flex-shrink-0 ml-0.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-amber-400 opacity-75" />
|
||||
<span className="relative inline-flex size-1.5 rounded-full bg-amber-500" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex flex-row items-start justify-start gap-[1.2rem]',
|
||||
cardVO.debug
|
||||
? 'flex-1 min-h-0 px-5 pt-2.5 pb-3.5'
|
||||
: 'h-full p-[1.2rem]',
|
||||
)}
|
||||
>
|
||||
{/* Icon - fixed width */}
|
||||
{iconURL ? (
|
||||
{iconURL && !iconFailed ? (
|
||||
<img
|
||||
src={iconURL}
|
||||
alt="plugin icon"
|
||||
className="w-16 h-16 rounded-[8%] flex-shrink-0"
|
||||
onError={() => setIconFailed(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center">
|
||||
@@ -92,15 +125,6 @@ export default function PluginCardComponent({
|
||||
: t('market.typePlugin')}
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.debug && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400 flex-shrink-0"
|
||||
>
|
||||
<BugIcon className="w-4 h-4" />
|
||||
{t('plugins.debugging')}
|
||||
</Badge>
|
||||
)}
|
||||
{!cardVO.debug && (
|
||||
<>
|
||||
{cardVO.install_source === 'github' && (
|
||||
|
||||
+27
-35
@@ -11,6 +11,7 @@ import {
|
||||
Heart,
|
||||
Loader2,
|
||||
Check,
|
||||
ArrowUp,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -355,27 +356,29 @@ export default function PluginMarketCardComponent({
|
||||
className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0 overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-start gap-2 min-w-0 overflow-hidden">
|
||||
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem] flex-shrink-0">
|
||||
{showInstalledMark ? (
|
||||
// Hollow green ring enclosing a green check: the download
|
||||
// affordance becomes an "installed" mark in place.
|
||||
<span
|
||||
title={t('market.installed')}
|
||||
aria-label={t('market.installed')}
|
||||
className="flex h-4 w-4 sm:h-[1.2rem] sm:w-[1.2rem] flex-shrink-0 items-center justify-center rounded-full border-2 border-green-500 dark:border-green-400"
|
||||
>
|
||||
<Check
|
||||
className="h-2 w-2 text-green-500 dark:text-green-400 sm:h-2.5 sm:w-2.5"
|
||||
strokeWidth={3.5}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
{hasUpdate ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="h-5 px-1.5 sm:px-2 text-[0.65rem] sm:text-[0.7rem] flex items-center gap-1 font-medium flex-shrink-0 whitespace-nowrap text-amber-600 dark:text-amber-400 border-amber-500/40 bg-amber-500/10"
|
||||
>
|
||||
<ArrowUp className="w-3 h-3 text-amber-500 dark:text-amber-400" />
|
||||
<span>{t('market.updateAvailable')}</span>
|
||||
</Badge>
|
||||
) : showInstalledMark ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="h-5 px-1.5 sm:px-2 text-[0.65rem] sm:text-[0.7rem] flex items-center gap-1 font-medium flex-shrink-0 whitespace-nowrap text-green-600 dark:text-green-400 border-green-500/40 bg-green-500/10"
|
||||
>
|
||||
<Check
|
||||
className="w-3 h-3 text-green-500 dark:text-green-400"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span>{t('market.installed')}</span>
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem] flex-shrink-0">
|
||||
<svg
|
||||
className={`w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] flex-shrink-0 ${
|
||||
hasUpdate
|
||||
? 'text-amber-500 dark:text-amber-400'
|
||||
: 'text-[#2563eb] dark:text-[#5b8def]'
|
||||
}`}
|
||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] flex-shrink-0 text-[#2563eb] dark:text-[#5b8def]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -386,22 +389,11 @@ export default function PluginMarketCardComponent({
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
)}
|
||||
<div
|
||||
title={hasUpdate ? t('market.updateAvailable') : undefined}
|
||||
className={`text-xs sm:text-sm font-medium whitespace-nowrap ${
|
||||
showInstalledMark
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: hasUpdate
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-[#2563eb] dark:text-[#5b8def]'
|
||||
}`}
|
||||
>
|
||||
{showInstalledMark
|
||||
? t('market.installed')
|
||||
: (cardVO.installCount?.toLocaleString() ?? '0')}
|
||||
<div className="text-xs sm:text-sm font-medium whitespace-nowrap text-[#2563eb] dark:text-[#5b8def]">
|
||||
{cardVO.installCount?.toLocaleString() ?? '0'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cardVO.tags && cardVO.tags.length > 0 && visibleTags > 0 && (
|
||||
<div className="flex flex-row items-center gap-1.5 overflow-hidden flex-shrink min-w-0">
|
||||
|
||||
@@ -28,6 +28,71 @@ export interface PluginLogEntry {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** Editable plugin metadata for the "upload to LangBot Space" page. */
|
||||
export interface PluginManifestOverrides {
|
||||
/** A single string applies to all locales; an object targets specific ones. */
|
||||
label?: string | I18nObject;
|
||||
description?: string | I18nObject;
|
||||
version?: string;
|
||||
repository?: string;
|
||||
/** Reassigns the published plugin author (publishes under a new identity). */
|
||||
author?: string;
|
||||
/** SPDX license identifier (e.g. MIT, Apache-2.0). */
|
||||
license?: string;
|
||||
/** Archive-relative icon path (e.g. assets/icon.png). */
|
||||
icon?: string;
|
||||
/** A data URL of an uploaded replacement icon. */
|
||||
icon_base64?: string;
|
||||
}
|
||||
|
||||
/** Connection state + pre-fill data for the upload page. */
|
||||
export interface PluginSpaceUploadConfig {
|
||||
debug: boolean;
|
||||
metadata: {
|
||||
author: string;
|
||||
name: string;
|
||||
label?: I18nObject | string;
|
||||
description?: I18nObject | string;
|
||||
version?: string;
|
||||
repository?: string;
|
||||
license?: string;
|
||||
};
|
||||
space_connected: boolean;
|
||||
has_creator_profile: boolean;
|
||||
creator_slug?: string | null;
|
||||
author_matches: boolean;
|
||||
cloud_service_url: string;
|
||||
}
|
||||
|
||||
export interface PluginGithubSyncPayload {
|
||||
repo_url?: string;
|
||||
token?: string;
|
||||
branch?: string;
|
||||
commit_message?: string;
|
||||
manifest_overrides?: PluginManifestOverrides;
|
||||
}
|
||||
|
||||
export interface PluginGithubSyncResult {
|
||||
committed: boolean;
|
||||
pushed: boolean;
|
||||
branch: string;
|
||||
remote_url: string;
|
||||
commit_sha: string;
|
||||
message: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface PluginSpaceUploadPayload extends PluginGithubSyncPayload {
|
||||
sync_github?: boolean;
|
||||
changelog?: string;
|
||||
}
|
||||
|
||||
export interface PluginSpaceUploadResult {
|
||||
submission: Record<string, unknown>;
|
||||
git: PluginGithubSyncResult | null;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
// marketplace plugin v4
|
||||
export enum PluginV4Status {
|
||||
Any = 'any',
|
||||
|
||||
@@ -84,7 +84,14 @@ import {
|
||||
ApiRespLangBotModelAvailability,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Plugin } from '@/app/infra/entities/plugin';
|
||||
import type { PluginLogEntry } from '@/app/infra/entities/plugin';
|
||||
import type {
|
||||
PluginLogEntry,
|
||||
PluginSpaceUploadConfig,
|
||||
PluginSpaceUploadPayload,
|
||||
PluginSpaceUploadResult,
|
||||
PluginGithubSyncPayload,
|
||||
PluginGithubSyncResult,
|
||||
} from '@/app/infra/entities/plugin';
|
||||
import type { I18nObject } from '@/app/infra/entities/common';
|
||||
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
|
||||
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||
@@ -1507,6 +1514,37 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.get('/api/v1/plugins/debug-info');
|
||||
}
|
||||
|
||||
public getPluginSpaceUploadConfig(
|
||||
author: string,
|
||||
name: string,
|
||||
): Promise<PluginSpaceUploadConfig> {
|
||||
return this.get(
|
||||
`/api/v1/plugins/${encodeURIComponent(author)}/${encodeURIComponent(name)}/space-upload/config`,
|
||||
);
|
||||
}
|
||||
|
||||
public uploadPluginToSpace(
|
||||
author: string,
|
||||
name: string,
|
||||
payload: PluginSpaceUploadPayload,
|
||||
): Promise<PluginSpaceUploadResult> {
|
||||
return this.post(
|
||||
`/api/v1/plugins/${encodeURIComponent(author)}/${encodeURIComponent(name)}/space-upload`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
public syncPluginToGithub(
|
||||
author: string,
|
||||
name: string,
|
||||
payload: PluginGithubSyncPayload,
|
||||
): Promise<{ git: PluginGithubSyncResult }> {
|
||||
return this.post(
|
||||
`/api/v1/plugins/${encodeURIComponent(author)}/${encodeURIComponent(name)}/space-upload/git-sync`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
public getBoxRuntimeStatus(): Promise<ApiRespBoxStatus> {
|
||||
return this.get('/api/v1/box/runtime-status');
|
||||
}
|
||||
|
||||
@@ -1811,6 +1811,212 @@ const enUS = {
|
||||
uploadLocal: 'Upload Local',
|
||||
debugging: 'Debugging',
|
||||
uploadLocalPlugin: 'Upload Local Plugin',
|
||||
spaceUpload: {
|
||||
menuAction: 'Upload to Extension Market',
|
||||
title: 'Upload plugin to Extension Market',
|
||||
description:
|
||||
'Build the debugging plugin {{author}}/{{name}} and publish it to the Extension Market.',
|
||||
loadFailed: 'Failed to load plugin upload information',
|
||||
spaceNotConnected:
|
||||
'This Workspace is not bound to a LangBot Account. Bind one before uploading.',
|
||||
label: 'Name',
|
||||
version: 'Version',
|
||||
pluginDescription: 'Description',
|
||||
repository: 'Repository',
|
||||
repositoryPublicHint:
|
||||
'When syncing to GitHub, a missing repository is created and an existing private repository is switched to public.',
|
||||
author: 'Author',
|
||||
authorHint:
|
||||
'Publishing uses this author identity on the Extension Market.',
|
||||
license: 'License',
|
||||
licensePlaceholder: 'Select a license',
|
||||
licenseNone: 'Not specified',
|
||||
changelog: 'Changelog',
|
||||
syncGithub: 'Sync to GitHub',
|
||||
syncGithubHint:
|
||||
'Commit and push the plugin working directory. By default the local git configuration is reused; a repository URL and token below can override it. When a token is provided and the GitHub repository does not exist yet, it is created automatically.',
|
||||
repoUrl: 'Repository URL (optional)',
|
||||
repoUrlHint: 'Leave empty to reuse the local git origin.',
|
||||
token: 'Access token (optional)',
|
||||
tokenPlaceholder: 'Only used for this push',
|
||||
branch: 'Branch',
|
||||
commitMessage: 'Commit message',
|
||||
syncOnly: 'Sync to GitHub only',
|
||||
gitSynced: 'Synchronised to GitHub',
|
||||
gitCommittedNoRemote:
|
||||
'Committed locally, but no remote was configured to push to',
|
||||
gitNothing: 'No changes to synchronise',
|
||||
gitFailed: 'Git sync failed: ',
|
||||
uploadSuccess: 'Plugin uploaded to Extension Market',
|
||||
reviewOnSpace: 'Submit for review on Extension Market',
|
||||
uploadFailed: 'Failed to upload plugin: ',
|
||||
basicInfo: 'Basic information',
|
||||
changeIcon: 'Change icon',
|
||||
iconInvalidType: 'Please choose an image file',
|
||||
iconTooLarge: 'Icon must be smaller than 10 MB',
|
||||
labelRequired: 'Provide a name in at least one language',
|
||||
langEn: 'English',
|
||||
langZhHans: 'Chinese (Simplified)',
|
||||
langZhHant: 'Chinese (Traditional)',
|
||||
langJa: 'Japanese',
|
||||
langTh: 'Thai',
|
||||
langVi: 'Vietnamese',
|
||||
langEs: 'Spanish',
|
||||
uploadButton: 'Upload to Extension Market',
|
||||
notDebugHint:
|
||||
'Only plugins connected for debugging can be uploaded. Start debugging with `lbp run` first.',
|
||||
stepInfo: 'Plugin Info',
|
||||
stepSettings: 'Release Notes',
|
||||
stepConfirm: 'Confirm & Upload',
|
||||
stepResult: 'Upload Result',
|
||||
startUpload: 'Start Upload',
|
||||
uploadingTitle: 'Uploading to Extension Market...',
|
||||
uploadingDesc:
|
||||
'Packaging local plugin files, validating manifest, and syncing to LangBot Space...',
|
||||
uploadSuccessTitle: 'Plugin Uploaded Successfully!',
|
||||
uploadSuccessDesc:
|
||||
'Plugin {{author}}/{{name}} (v{{version}}) has been submitted to LangBot Space.',
|
||||
reviewNoticeTitle: 'Review Notice',
|
||||
reviewNoticeDesc:
|
||||
'The plugin has entered the review queue. Once approved, it will be publicly available in the Extension Market.',
|
||||
viewInSpace: 'View in Space Marketplace',
|
||||
viewMyPlugins: 'Manage My Plugins',
|
||||
finishAndClose: 'Done',
|
||||
retryUpload: 'Retry Upload',
|
||||
backToEdit: 'Back to Edit',
|
||||
manifestNotice:
|
||||
'The following information is read from the plugin manifest (manifest.yaml) for confirmation only. To modify it, please edit your manifest file and restart debugging.',
|
||||
manifestSource: 'Confirmed from manifest',
|
||||
nextStep: 'Next',
|
||||
prevStep: 'Back',
|
||||
nextToSettings: 'Next: Release Notes',
|
||||
nextToConfirm: 'Next: Confirm & Upload',
|
||||
confirmAndUpload: 'Confirm and Upload to Market',
|
||||
noDescription: 'No description provided',
|
||||
noRepository: 'No repository specified',
|
||||
noLicense: 'No license specified',
|
||||
changelogPlaceholder:
|
||||
'Enter release notes for this version (e.g., new features, bug fixes)...',
|
||||
changelogHint:
|
||||
'Changelog will be displayed in the plugin version history on the Extension Market.',
|
||||
reviewSummaryTitle: 'Pre-flight Review',
|
||||
reviewTargetMarket: 'Target Market',
|
||||
reviewTargetMarketValue: 'Extension Market (LangBot Space)',
|
||||
reviewAccountStatus: 'Account Status',
|
||||
reviewAccountConnected: 'Connected to LangBot Account',
|
||||
reviewAccountNotConnected: 'Account not connected (Upload will fail)',
|
||||
spaceNotConnectedTitle: 'Extension Market Account Not Bound',
|
||||
bindSpaceButton: 'Connect LangBot Account',
|
||||
bindRequiredToProceed: 'Please connect your account to proceed',
|
||||
prerequisitesTitle: 'Publishing Preparation',
|
||||
prerequisitesDescription:
|
||||
'Connect your LangBot Account and configure your creator identity before publishing.',
|
||||
req1Title: 'Connect LangBot Account',
|
||||
req1Desc: 'Link this local instance to your LangBot Account.',
|
||||
reqBound: 'Connected',
|
||||
reqCompleted: 'Ready',
|
||||
req2Title: 'Space Creator Profile',
|
||||
req2Desc:
|
||||
'Configure a creator profile on Space with a slug matching the plugin author name.',
|
||||
req2WaitSpace: 'Requires LangBot Account connection first',
|
||||
req2NoCreator:
|
||||
'No creator profile has been set up for this LangBot Account',
|
||||
req2Mismatch: 'Creator slug mismatch',
|
||||
req2MismatchDetail:
|
||||
'Space creator slug is @{{creator}}, but plugin manifest author is {{author}}. Please update author in manifest.yaml or switch accounts.',
|
||||
createCreatorButton: 'Set Up Creator Profile',
|
||||
manageCreatorButton: 'View Creator Settings',
|
||||
pendingReq: 'Pending',
|
||||
autoProceedTip: '',
|
||||
refreshStatus: 'Refresh Status',
|
||||
reviewGitSync: 'GitHub Sync',
|
||||
reviewGitSyncEnabled: 'Enabled (pushing to {{branch}})',
|
||||
reviewGitSyncDisabled: 'Disabled',
|
||||
reviewChangelog: 'Release Notes',
|
||||
reviewChangelogEmpty: '(No release notes provided)',
|
||||
pluginIdentifier: 'Plugin Identifier',
|
||||
banners: {
|
||||
guideTitle: 'How to resolve:',
|
||||
disabledTooltip:
|
||||
'Please follow the banner instructions above before uploading',
|
||||
bindButton: 'Connect LangBot Account',
|
||||
reauthButton: 'Re-authenticate Account',
|
||||
createCreatorButton: 'Create Creator Profile',
|
||||
manageCreatorButton: 'Manage Creator Profile',
|
||||
viewMyPluginsButton: 'View My Plugins on Space',
|
||||
refreshButton: 'Refresh Status',
|
||||
retryButton: 'Retry Upload',
|
||||
readyToUpload: {
|
||||
title: 'Ready to Publish',
|
||||
description:
|
||||
'LangBot Account and creator profile are ready. Please review details below and click confirm to upload.',
|
||||
},
|
||||
notConnected: {
|
||||
title: 'LangBot Account Not Connected',
|
||||
description:
|
||||
'This local instance is not yet connected to a LangBot Account. A LangBot Account is required to verify identity and publish plugins.',
|
||||
guide:
|
||||
'Please click "Connect LangBot Account" below to authorize sign-in. Once connected, you can publish plugins.',
|
||||
},
|
||||
noCreator: {
|
||||
title: 'Space Creator Profile Not Created',
|
||||
description:
|
||||
'Publishing plugins requires a Creator Profile to attribute ownership and showcase your public author page on the marketplace.',
|
||||
guide:
|
||||
'Please click "Create Creator Profile" below to configure your unique slug, bio, and avatar. Then return here and click "Refresh Status".',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: 'Plugin Author Does Not Match Space Creator Slug',
|
||||
description:
|
||||
'The author declared in your plugin manifest is "{{author}}", but your Space creator slug is "@{{creator}}". The marketplace requires them to match exactly.',
|
||||
guide:
|
||||
'How to resolve:\n1. Update manifest.yaml in your plugin directory and set metadata.author to "{{creator}}";\n2. Or switch your connected LangBot Account to the one matching "{{author}}". Then click "Refresh Status".',
|
||||
},
|
||||
pendingReview: {
|
||||
title: 'A Submission Is Already Pending Review',
|
||||
description:
|
||||
'A previous submission for this plugin is currently pending review. Please wait for the current review to complete before submitting another version.',
|
||||
guide:
|
||||
'Please click "View My Plugins on Space" below to check your submission review progress and reviewer feedback.',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'Space Authentication Session Expired',
|
||||
description:
|
||||
'The Space access token has expired or is invalid. Authentication failed.',
|
||||
guide:
|
||||
'Please click "Re-authenticate Account" below to sign in again, then retry uploading.',
|
||||
},
|
||||
validationFailed: {
|
||||
title: 'Plugin Package Validation Failed',
|
||||
description:
|
||||
'Package inspection or certification precheck failed: {{detail}}',
|
||||
guide:
|
||||
'Please check your manifest.yaml fields, semantic version format, and ensure all icon and README assets exist. Fix and retry.',
|
||||
},
|
||||
genericError: {
|
||||
title: 'Plugin Publish Failed',
|
||||
description: '{{detail}}',
|
||||
guide:
|
||||
'Please check your network connection or try again later. If the issue persists, visit the Space community.',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile:
|
||||
'Please set up a creator profile on Space before uploading plugins',
|
||||
authorMismatch:
|
||||
'Plugin author "{{author}}" does not match your Space creator slug "@{{slug}}"',
|
||||
pendingReview:
|
||||
'You already have a plugin submission pending review. Please wait for it to complete',
|
||||
sessionExpired:
|
||||
'LangBot Account login session has expired. Please re-authenticate',
|
||||
notConnected:
|
||||
'Please connect a LangBot Account before uploading plugins',
|
||||
packageFailed:
|
||||
'Failed to build the plugin package. Please check your plugin code and files',
|
||||
inspectFailed: 'Plugin manifest inspection failed: {{detail}}',
|
||||
certPrecheckFailed: 'Plugin certification precheck failed: {{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: 'Preview Local Plugin Package',
|
||||
unpacking: 'Unpacking package preview...',
|
||||
|
||||
@@ -1839,6 +1839,200 @@ const esES = {
|
||||
uploadLocal: 'Subir local',
|
||||
debugging: 'Depuración',
|
||||
uploadLocalPlugin: 'Subir plugin local',
|
||||
spaceUpload: {
|
||||
menuAction: 'Subir a la tienda de extensiones',
|
||||
title: 'Subir plugin a la tienda de extensiones',
|
||||
description:
|
||||
'Compila el plugin en depuración {{author}}/{{name}} y publícalo en la tienda de extensiones.',
|
||||
loadFailed: 'No se pudo cargar la información de subida del plugin',
|
||||
spaceNotConnected:
|
||||
'Este Workspace no está vinculado a una cuenta de LangBot. Vincúlala antes de subir.',
|
||||
label: 'Nombre',
|
||||
version: 'Versión',
|
||||
pluginDescription: 'Descripción',
|
||||
repository: 'Repositorio',
|
||||
repositoryPublicHint:
|
||||
'Al sincronizar con GitHub, un repositorio inexistente se crea y un repositorio privado existente pasa a ser público.',
|
||||
author: 'Autor',
|
||||
authorHint:
|
||||
'Al publicar se usa esta identidad de autor en la tienda de extensiones.',
|
||||
license: 'Licencia',
|
||||
licensePlaceholder: 'Selecciona una licencia',
|
||||
licenseNone: 'Sin especificar',
|
||||
changelog: 'Registro de cambios',
|
||||
syncGithub: 'Sincronizar con GitHub',
|
||||
syncGithubHint:
|
||||
'Confirma y sube el directorio de trabajo del plugin. Por defecto se reutiliza la configuración local de git; la URL y el token de abajo pueden sobrescribirla. Si indicas un token y el repositorio de GitHub no existe, se crea automáticamente.',
|
||||
repoUrl: 'URL del repositorio (opcional)',
|
||||
repoUrlHint: 'Déjala vacía para reutilizar el origin local de git.',
|
||||
token: 'Token de acceso (opcional)',
|
||||
tokenPlaceholder: 'Solo se usa para este push',
|
||||
branch: 'Rama',
|
||||
commitMessage: 'Mensaje de commit',
|
||||
syncOnly: 'Sincronizar solo con GitHub',
|
||||
gitSynced: 'Sincronizado con GitHub',
|
||||
gitCommittedNoRemote:
|
||||
'Confirmado localmente, pero no hay remoto configurado para subir',
|
||||
gitNothing: 'No hay cambios que sincronizar',
|
||||
gitFailed: 'Falló la sincronización con Git: ',
|
||||
uploadSuccess: 'Plugin subido a la tienda de extensiones',
|
||||
reviewOnSpace: 'Enviar a revisión en la tienda de extensiones',
|
||||
uploadFailed: 'No se pudo subir el plugin: ',
|
||||
basicInfo: 'Información básica',
|
||||
changeIcon: 'Cambiar icono',
|
||||
iconInvalidType: 'Elige un archivo de imagen',
|
||||
iconTooLarge: 'El icono debe pesar menos de 10 MB',
|
||||
labelRequired: 'Indica un nombre en al menos un idioma',
|
||||
langEn: 'English',
|
||||
langZhHans: 'Chino simplificado',
|
||||
langZhHant: 'Chino tradicional',
|
||||
langJa: 'Japonés',
|
||||
langTh: 'Tailandés',
|
||||
langVi: 'Vietnamita',
|
||||
langEs: 'Español',
|
||||
uploadButton: 'Subir a la tienda de extensiones',
|
||||
notDebugHint:
|
||||
'Solo se pueden subir plugins conectados para depuración. Inicia la depuración con `lbp run` primero.',
|
||||
stepInfo: 'Información del plugin',
|
||||
stepSettings: 'Notas de la versión',
|
||||
stepConfirm: 'Confirmar subida',
|
||||
manifestNotice:
|
||||
'La siguiente información se lee del manifiesto del plugin (manifest.yaml) solo para confirmación. Para modificarla, edite su archivo manifest y reinicie la depuración.',
|
||||
manifestSource: 'Confirmado del manifiesto',
|
||||
nextStep: 'Siguiente',
|
||||
prevStep: 'Atrás',
|
||||
nextToSettings: 'Siguiente: Notas de la versión',
|
||||
nextToConfirm: 'Siguiente: Confirmar subida',
|
||||
confirmAndUpload: 'Confirmar y subir al mercado de extensiones',
|
||||
noDescription: 'Sin descripción',
|
||||
noRepository: 'Sin repositorio especificado',
|
||||
noLicense: 'Sin licencia especificada',
|
||||
changelogPlaceholder:
|
||||
'Ingrese las notas de lanzamiento de esta versión (nuevas funciones, correcciones, etc.)...',
|
||||
changelogHint:
|
||||
'El registro de cambios se mostrará en el historial de versiones del mercado de extensiones.',
|
||||
reviewSummaryTitle: 'Revisión previa a la publicación',
|
||||
reviewTargetMarket: 'Mercado de destino',
|
||||
reviewTargetMarketValue: 'Mercado de extensiones (LangBot Space)',
|
||||
reviewAccountStatus: 'Estado de la cuenta',
|
||||
reviewAccountConnected: 'Conectado a la cuenta LangBot',
|
||||
reviewAccountNotConnected: 'Cuenta no conectada (la subida fallará)',
|
||||
spaceNotConnectedTitle: 'Cuenta del mercado no vinculada',
|
||||
bindSpaceButton: 'Vincular cuenta ahora',
|
||||
bindRequiredToProceed: 'Por favor vincula tu cuenta para continuar',
|
||||
prerequisitesTitle: 'Requisitos previos de publicación',
|
||||
prerequisitesDescription:
|
||||
'Antes de publicar este plugin en el mercado, asegúrate de cumplir con los siguientes requisitos.',
|
||||
req1Title: 'Requisito 1: Vincular cuenta de LangBot',
|
||||
req1Desc:
|
||||
'La instancia local de LangBot debe estar iniciada sesión o vinculada a LangBot Space.',
|
||||
reqBound: 'Vinculado',
|
||||
reqCompleted: 'Completado',
|
||||
req2Title: 'Requisito 2: Crear identidad de creador en Space',
|
||||
req2Desc:
|
||||
'La cuenta de LangBot vinculada debe tener una identidad de creador y coincidir con el autor del plugin.',
|
||||
req2WaitSpace:
|
||||
'Completa primero el Requisito 1 (Vincular cuenta de LangBot)',
|
||||
req2NoCreator:
|
||||
'Esta cuenta de LangBot aún no tiene una identidad de creador',
|
||||
req2Mismatch: 'Discrepancia en la identidad de creador',
|
||||
req2MismatchDetail:
|
||||
'Tu identificador de creador en Space es @{{creator}}, pero el autor del plugin es {{author}}.',
|
||||
createCreatorButton: 'Crear identidad de creador',
|
||||
manageCreatorButton: 'Ver ajustes de creador',
|
||||
pendingReq: 'Pendiente',
|
||||
autoProceedTip:
|
||||
'Una vez cumplidos los requisitos, se abrirá automáticamente el asistente de publicación.',
|
||||
refreshStatus: 'Actualizar estado',
|
||||
reviewGitSync: 'Sincronización con GitHub',
|
||||
reviewGitSyncEnabled: 'Habilitado (empujar a {{branch}})',
|
||||
reviewGitSyncDisabled: 'Deshabilitado',
|
||||
reviewChangelog: 'Notas de la versión',
|
||||
reviewChangelogEmpty: '(Sin notas de versión)',
|
||||
pluginIdentifier: 'Identificador del plugin',
|
||||
banners: {
|
||||
guideTitle: 'Instrucciones:',
|
||||
disabledTooltip:
|
||||
'Sigue las instrucciones del banner superior antes de subir',
|
||||
bindButton: 'Conectar cuenta de LangBot',
|
||||
reauthButton: 'Reautenticar cuenta',
|
||||
createCreatorButton: 'Crear perfil de creador',
|
||||
manageCreatorButton: 'Administrar perfil de creador',
|
||||
viewMyPluginsButton: 'Ver mis plugins en Space',
|
||||
refreshButton: 'Actualizar estado',
|
||||
retryButton: 'Reintentar subida',
|
||||
readyToUpload: {
|
||||
title: 'Listo para publicar',
|
||||
description:
|
||||
'La cuenta de LangBot y el perfil de creador están listos. Revisa los detalles a continuación y confirma la subida.',
|
||||
},
|
||||
notConnected: {
|
||||
title: 'Cuenta de LangBot no conectada',
|
||||
description:
|
||||
'Esta instancia local aún no está conectada a una cuenta de LangBot. Se requiere una cuenta de LangBot para verificar tu identidad.',
|
||||
guide:
|
||||
'Haz clic en "Conectar cuenta de LangBot" para iniciar sesión. Podrás publicar el plugin una vez conectada.',
|
||||
},
|
||||
noCreator: {
|
||||
title: 'Perfil de creador de Space no creado',
|
||||
description:
|
||||
'La publicación en el marketplace requiere un perfil de creador para atribuir la autoría y mostrar tu página pública.',
|
||||
guide:
|
||||
'Haz clic en "Crear perfil de creador" para configurar tu slug, biografía y avatar. Luego regresa y haz clic en "Actualizar estado".',
|
||||
},
|
||||
authorMismatch: {
|
||||
title:
|
||||
'El autor del plugin no coincide con el identificador de creador',
|
||||
description:
|
||||
'El autor en manifest.yaml es "{{author}}", pero tu identificador en Space es "@{{creator}}". Deben coincidir exactamente.',
|
||||
guide:
|
||||
'Soluciones:\n1. Actualiza manifest.yaml y establece metadata.author en "{{creator}}";\n2. O cambia a la cuenta de LangBot que coincida con "{{author}}". Luego haz clic en "Actualizar estado".',
|
||||
},
|
||||
pendingReview: {
|
||||
title: 'Ya existe un envío pendiente de revisión',
|
||||
description:
|
||||
'Un envío anterior de este plugin está actualmente en revisión. Espera a que finalice antes de enviar una nueva versión.',
|
||||
guide:
|
||||
'Haz clic en "Ver mis plugins en Space" para consultar el estado y los comentarios de la revisión.',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'La sesión de autenticación de Space ha expirado',
|
||||
description:
|
||||
'El token de acceso de Space ha expirado o es inválido. La autenticación ha fallado.',
|
||||
guide:
|
||||
'Haz clic en "Reautenticar cuenta" para iniciar sesión de nuevo e intenta subirlo otra vez.',
|
||||
},
|
||||
validationFailed: {
|
||||
title: 'La validación del paquete del plugin falló',
|
||||
description:
|
||||
'Error en la inspección o verificación del paquete: {{detail}}',
|
||||
guide:
|
||||
'Comprueba los campos de manifest.yaml, el formato de versión semántica y los archivos de icono y README.',
|
||||
},
|
||||
genericError: {
|
||||
title: 'Error al publicar el plugin',
|
||||
description: '{{detail}}',
|
||||
guide: 'Comprueba tu conexión de red o inténtalo más tarde.',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile:
|
||||
'Debes configurar un perfil de creador en Space antes de subir plugins',
|
||||
authorMismatch:
|
||||
'El autor del plugin "{{author}}" no coincide con tu identificador de creador en Space "@{slug}"',
|
||||
pendingReview:
|
||||
'Ya tienes un envío de plugin pendiente de revisión. Espera a que finalice',
|
||||
sessionExpired:
|
||||
'La sesión de Space ha expirado. Vuelve a iniciar sesión',
|
||||
notConnected: 'Conecta una cuenta de LangBot antes de subir plugins',
|
||||
packageFailed:
|
||||
'Error al compilar el paquete del plugin. Revisa el código y los archivos',
|
||||
inspectFailed:
|
||||
'Error al inspeccionar el manifiesto del plugin: {{detail}}',
|
||||
certPrecheckFailed:
|
||||
'Error en la verificación previa de certificación: {{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: 'Previsualizar paquete de plugin local',
|
||||
unpacking: 'Descomprimiendo vista previa del paquete...',
|
||||
|
||||
@@ -1822,6 +1822,196 @@ const jaJP = {
|
||||
uploadLocal: 'ローカルアップロード',
|
||||
debugging: 'デバッグ中',
|
||||
uploadLocalPlugin: 'ローカルプラグインのアップロード',
|
||||
spaceUpload: {
|
||||
menuAction: 'プラグインマーケットにアップロード',
|
||||
title: 'プラグインをプラグインマーケットにアップロード',
|
||||
description:
|
||||
'デバッグ中のプラグイン {{author}}/{{name}} をビルドしてプラグインマーケットに公開します。',
|
||||
loadFailed: 'プラグインのアップロード情報の読み込みに失敗しました',
|
||||
spaceNotConnected:
|
||||
'このワークスペースは LangBot アカウントに連携されていません。先に連携してください。',
|
||||
label: '名前',
|
||||
version: 'バージョン',
|
||||
pluginDescription: '説明',
|
||||
repository: 'リポジトリ',
|
||||
repositoryPublicHint:
|
||||
'GitHub へ同期する際、存在しないリポジトリは作成され、既存のプライベートリポジトリは公開に変更されます。',
|
||||
author: '作者',
|
||||
authorHint:
|
||||
'公開時はプラグインマーケット上でこの作者として登録されます。',
|
||||
license: 'ライセンス',
|
||||
licensePlaceholder: 'ライセンスを選択',
|
||||
licenseNone: '未指定',
|
||||
changelog: '変更履歴',
|
||||
syncGithub: 'GitHub に同期',
|
||||
syncGithubHint:
|
||||
'プラグインの作業ディレクトリをコミットしてプッシュします。既定ではローカルの git 設定を再利用し、下記のリポジトリ URL とトークンで上書きできます。トークンを指定していて GitHub リポジトリが存在しない場合は自動作成します。',
|
||||
repoUrl: 'リポジトリ URL(任意)',
|
||||
repoUrlHint: '空欄の場合はローカルの git origin を再利用します。',
|
||||
token: 'アクセストークン(任意)',
|
||||
tokenPlaceholder: 'このプッシュにのみ使用します',
|
||||
branch: 'ブランチ',
|
||||
commitMessage: 'コミットメッセージ',
|
||||
syncOnly: 'GitHub にのみ同期',
|
||||
gitSynced: 'GitHub に同期しました',
|
||||
gitCommittedNoRemote:
|
||||
'ローカルにコミットしましたが、プッシュ先が未設定です',
|
||||
gitNothing: '同期する変更はありません',
|
||||
gitFailed: 'Git 同期に失敗しました:',
|
||||
uploadSuccess: 'プラグインをプラグインマーケットにアップロードしました',
|
||||
reviewOnSpace: 'プラグインマーケットで審査を申請',
|
||||
uploadFailed: 'プラグインのアップロードに失敗しました:',
|
||||
basicInfo: '基本情報',
|
||||
changeIcon: 'アイコンを変更',
|
||||
iconInvalidType: '画像ファイルを選択してください',
|
||||
iconTooLarge: 'アイコンは 10 MB 未満にしてください',
|
||||
labelRequired: '少なくとも 1 つの言語で名前を入力してください',
|
||||
langEn: 'English',
|
||||
langZhHans: '簡体字中国語',
|
||||
langZhHant: '繁体字中国語',
|
||||
langJa: '日本語',
|
||||
langTh: 'タイ語',
|
||||
langVi: 'ベトナム語',
|
||||
langEs: 'スペイン語',
|
||||
uploadButton: 'プラグインマーケットにアップロード',
|
||||
notDebugHint:
|
||||
'アップロードできるのはデバッグ接続中のプラグインのみです。先に `lbp run` を実行してください。',
|
||||
stepInfo: 'プラグイン情報',
|
||||
stepSettings: '更新履歴',
|
||||
stepConfirm: 'アップロード確認',
|
||||
manifestNotice:
|
||||
'以下の情報はプラグインマニフェスト(manifest.yaml)から読み込まれた確認用です。変更が必要な場合は、マニフェストを直接編集してデバッグを再起動してください。',
|
||||
manifestSource: 'マニフェスト確認済み',
|
||||
nextStep: '次へ',
|
||||
prevStep: '前へ',
|
||||
nextToSettings: '次へ:更新履歴',
|
||||
nextToConfirm: '次へ:アップロード確認',
|
||||
confirmAndUpload: '拡張機能マーケットにアップロード確認',
|
||||
noDescription: '説明はありません',
|
||||
noRepository: 'リポジトリ未指定',
|
||||
noLicense: 'ライセンス未指定',
|
||||
changelogPlaceholder:
|
||||
'このバージョンの変更履歴を入力してください(新機能、バグ修正など)',
|
||||
changelogHint:
|
||||
'更新履歴は拡張機能マーケットのプラグインバージョン履歴に表示されます。',
|
||||
reviewSummaryTitle: 'リリース確認',
|
||||
reviewTargetMarket: '対象マーケット',
|
||||
reviewTargetMarketValue: '拡張機能マーケット (LangBot Space)',
|
||||
reviewAccountStatus: 'アカウント連携',
|
||||
reviewAccountConnected: 'LangBot アカウント連携済み',
|
||||
reviewAccountNotConnected: '未連携(アップロードに失敗します)',
|
||||
spaceNotConnectedTitle: 'マーケットアカウント未連携',
|
||||
bindSpaceButton: 'アカウントを連携',
|
||||
bindRequiredToProceed: '次へ進むにはアカウントを連携してください',
|
||||
prerequisitesTitle: '公開前の準備',
|
||||
prerequisitesDescription:
|
||||
'マーケットプレイスにプラグインを公開する前に、アカウントの連携とクリエイタープロフィールの設定を行ってください。',
|
||||
req1Title: 'LangBot アカウントを連携',
|
||||
req1Desc:
|
||||
'ローカルの LangBot インスタンスが LangBot Space クラウドにログインまたは連携されている必要があります。',
|
||||
reqBound: '連携済み',
|
||||
reqCompleted: '準備完了',
|
||||
req2Title: 'Space クリエイタープロフィールを設定',
|
||||
req2Desc:
|
||||
'連携された LangBot アカウントにクリエイターIDが存在し、プラグインの author と一致している必要があります。',
|
||||
req2WaitSpace: '先に LangBot アカウントの連携が必要です',
|
||||
req2NoCreator:
|
||||
'現在の LangBot アカウントにはクリエイターIDがまだ作成されていません',
|
||||
req2Mismatch: 'クリエイター識別子の不一致',
|
||||
req2MismatchDetail:
|
||||
'Space のクリエイターIDは @{{creator}} ですが、プラグインの author は {{author}} です。manifest.yaml の author を変更するかアカウントを切り替えてください。',
|
||||
createCreatorButton: 'クリエイタープロフィールを設定',
|
||||
manageCreatorButton: 'クリエイター設定を確認',
|
||||
pendingReq: '未完了',
|
||||
autoProceedTip: '',
|
||||
refreshStatus: '状態を更新',
|
||||
reviewGitSync: 'GitHub 同期',
|
||||
reviewGitSyncEnabled: '有効({{branch}} にプッシュ)',
|
||||
reviewGitSyncDisabled: '無効',
|
||||
reviewChangelog: '更新履歴',
|
||||
reviewChangelogEmpty: '(更新履歴の記入なし)',
|
||||
pluginIdentifier: 'プラグイン識別子',
|
||||
banners: {
|
||||
guideTitle: '操作手順:',
|
||||
disabledTooltip: '上のバナーの手順に従って操作を完了してください',
|
||||
bindButton: 'Spaceアカウントを連携',
|
||||
reauthButton: 'アカウントを再認証',
|
||||
createCreatorButton: 'クリエイタープロフィールを開設',
|
||||
manageCreatorButton: 'クリエイター設定を確認',
|
||||
viewMyPluginsButton: 'Spaceでマイプラグインを確認',
|
||||
refreshButton: '状態を更新',
|
||||
retryButton: '再試行',
|
||||
readyToUpload: {
|
||||
title: '公開準備が整いました',
|
||||
description:
|
||||
'Spaceアカウントとクリエイタープロフィールが確認されました。内容を確認し、下のボタンをクリックして公開してください。',
|
||||
},
|
||||
notConnected: {
|
||||
title: 'LangBot Spaceアカウントが未連携です',
|
||||
description:
|
||||
'このローカルインスタンスはLangBot Spaceに連携されていません。プラグインを公開するにはSpaceアカウントが必要です。',
|
||||
guide:
|
||||
'下の「Spaceアカウントを連携」をクリックして認証を完了してください。連携後に公開が可能になります。',
|
||||
},
|
||||
noCreator: {
|
||||
title: 'Spaceクリエイタープロフィールが未作成です',
|
||||
description:
|
||||
'プラグインをマーケットプレイスに公開するには、作者表示用のクリエイタープロフィールが必要です。',
|
||||
guide:
|
||||
'下のボタンをクリックしてSpaceでクリエイタースラッグ、自己紹介、アイコンを設定し、本画面で「状態を更新」してください。',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: 'プラグイン作者名とSpaceクリエイター識別子が一致しません',
|
||||
description:
|
||||
'マニフェストの作者名は「{{author}}」ですが、現在のSpaceクリエイター識別子は「@{{creator}}」です。マーケットプレイスの規定により完全一致が必要です。',
|
||||
guide:
|
||||
'解決方法:\n1. プラグインの manifest.yaml 内の metadata.author を「{{creator}}」に変更してください。\n2. または「{{author}}」に対応するSpaceアカウントに切り替えてください。',
|
||||
},
|
||||
pendingReview: {
|
||||
title: 'このプラグインには審査待ちのバージョンがあります',
|
||||
description:
|
||||
'すでに審査中の提出が存在します。前回の審査が完了するまで新しいバージョンは提出できません。',
|
||||
guide:
|
||||
'下の「Spaceでマイプラグインを確認」をクリックして、審査状況をご確認ください。',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'Spaceログインセッションが期限切れです',
|
||||
description:
|
||||
'Spaceのアクセストークンが無効または期限切れです。再認証が必要です。',
|
||||
guide:
|
||||
'下の「アカウントを再認証」をクリックして再度ログインしてください。',
|
||||
},
|
||||
validationFailed: {
|
||||
title: 'プラグインの仕様検証に失敗しました',
|
||||
description:
|
||||
'パッケージの解析または事前認証チェックに失敗しました: {{detail}}',
|
||||
guide:
|
||||
'manifest.yaml の記述内容、セマンティックバージョン、アイコン、READMEファイルを確認してください。',
|
||||
},
|
||||
genericError: {
|
||||
title: 'プラグインの公開に失敗しました',
|
||||
description: '{{detail}}',
|
||||
guide:
|
||||
'ネットワーク接続を確認して再試行してください。問題が続く場合はコミュニティへお問い合わせください。',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile:
|
||||
'プラグインをアップロードする前に、Spaceでクリエイタープロフィールを設定してください',
|
||||
authorMismatch:
|
||||
'プラグイン作者「{{author}}」はSpaceのクリエイター識別子「@{{slug}}」と一致しません',
|
||||
pendingReview:
|
||||
'現在審査待ちのプラグイン提出があります。審査完了までお待ちください',
|
||||
sessionExpired:
|
||||
'Spaceのログインセッションが期限切れです。再連携してください',
|
||||
notConnected:
|
||||
'プラグインをアップロードする前に、LangBot Spaceアカウントを連携してください',
|
||||
packageFailed:
|
||||
'プラグインパッケージのビルドに失敗しました。コードと構成を確認してください',
|
||||
inspectFailed: 'マニフェスト検査に失敗しました: {{detail}}',
|
||||
certPrecheckFailed: '認証事前チェックに失敗しました: {{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: 'ローカルプラグインパッケージをプレビュー',
|
||||
unpacking: 'パッケージを展開してプレビュー中...',
|
||||
|
||||
@@ -1835,6 +1835,194 @@ const ruRU = {
|
||||
uploadLocal: 'Загрузить локально',
|
||||
debugging: 'Отладка',
|
||||
uploadLocalPlugin: 'Загрузить локальный плагин',
|
||||
spaceUpload: {
|
||||
menuAction: 'Загрузить в маркетплейс',
|
||||
title: 'Загрузить плагин в маркетплейс',
|
||||
description:
|
||||
'Соберите отлаживаемый плагин {{author}}/{{name}} и опубликуйте его в маркетплейсе.',
|
||||
loadFailed: 'Не удалось загрузить информацию о загрузке плагина',
|
||||
spaceNotConnected:
|
||||
'Это Workspace не привязан к аккаунту LangBot. Привяжите его перед загрузкой.',
|
||||
label: 'Название',
|
||||
version: 'Версия',
|
||||
pluginDescription: 'Описание',
|
||||
repository: 'Репозиторий',
|
||||
repositoryPublicHint:
|
||||
'При синхронизации с GitHub отсутствующий репозиторий создаётся, а существующий приватный становится публичным.',
|
||||
author: 'Автор',
|
||||
authorHint:
|
||||
'При публикации используется эта личность автора в маркетплейсе.',
|
||||
license: 'Лицензия',
|
||||
licensePlaceholder: 'Выберите лицензию',
|
||||
licenseNone: 'Не указана',
|
||||
changelog: 'Список изменений',
|
||||
syncGithub: 'Синхронизировать с GitHub',
|
||||
syncGithubHint:
|
||||
'Зафиксируйте и отправьте рабочий каталог плагина. По умолчанию используется локальная конфигурация git; URL репозитория и токен ниже могут её переопределить. Если указан токен и репозитория GitHub ещё нет, он будет создан автоматически.',
|
||||
repoUrl: 'URL репозитория (необязательно)',
|
||||
repoUrlHint: 'Оставьте пустым, чтобы использовать локальный git origin.',
|
||||
token: 'Токен доступа (необязательно)',
|
||||
tokenPlaceholder: 'Используется только для этой отправки',
|
||||
branch: 'Ветка',
|
||||
commitMessage: 'Сообщение коммита',
|
||||
syncOnly: 'Синхронизировать только с GitHub',
|
||||
gitSynced: 'Синхронизировано с GitHub',
|
||||
gitCommittedNoRemote:
|
||||
'Зафиксировано локально, но удалённый репозиторий для отправки не настроен',
|
||||
gitNothing: 'Нет изменений для синхронизации',
|
||||
gitFailed: 'Ошибка синхронизации Git: ',
|
||||
uploadSuccess: 'Плагин загружен в маркетплейс',
|
||||
reviewOnSpace: 'Отправить на проверку в маркетплейс',
|
||||
uploadFailed: 'Не удалось загрузить плагин: ',
|
||||
basicInfo: 'Основная информация',
|
||||
changeIcon: 'Изменить иконку',
|
||||
iconInvalidType: 'Выберите файл изображения',
|
||||
iconTooLarge: 'Иконка должна быть меньше 10 МБ',
|
||||
labelRequired: 'Укажите название хотя бы на одном языке',
|
||||
langEn: 'English',
|
||||
langZhHans: 'Китайский (упрощённый)',
|
||||
langZhHant: 'Китайский (традиционный)',
|
||||
langJa: 'Японский',
|
||||
langTh: 'Тайский',
|
||||
langVi: 'Вьетнамский',
|
||||
langEs: 'Испанский',
|
||||
uploadButton: 'Загрузить в маркетплейс',
|
||||
notDebugHint:
|
||||
'Загружать можно только плагины, подключённые для отладки. Сначала запустите отладку через `lbp run`.',
|
||||
stepInfo: 'Информация о плагине',
|
||||
stepSettings: 'Примечания к выпуску',
|
||||
stepConfirm: 'Подтверждение загрузки',
|
||||
manifestNotice:
|
||||
'Следующая информация загружена из манифеста плагина (manifest.yaml) только для подтверждения. Для изменения отредактируйте файл манифеста и перезапустите отладку.',
|
||||
manifestSource: 'Подтверждено из манифеста',
|
||||
nextStep: 'Далее',
|
||||
prevStep: 'Назад',
|
||||
nextToSettings: 'Далее: Примечания к выпуску',
|
||||
nextToConfirm: 'Далее: Подтверждение загрузки',
|
||||
confirmAndUpload: 'Подтвердить и загрузить в маркет расширений',
|
||||
noDescription: 'Описание отсутствует',
|
||||
noRepository: 'Репозиторий не указан',
|
||||
noLicense: 'Лицензия не указана',
|
||||
changelogPlaceholder:
|
||||
'Укажите примечания к выпуску этой версии (новые функции, исправления и т.д.)...',
|
||||
changelogHint:
|
||||
'Список изменений будет отображаться в истории версий плагина на маркете расширений.',
|
||||
reviewSummaryTitle: 'Проверка перед публикацией',
|
||||
reviewTargetMarket: 'Целевой маркет',
|
||||
reviewTargetMarketValue: 'Маркет расширений (LangBot Space)',
|
||||
reviewAccountStatus: 'Статус аккаунта',
|
||||
reviewAccountConnected: 'Аккаунт LangBot подключен',
|
||||
reviewAccountNotConnected: 'Аккаунт не подключен (загрузка не удастся)',
|
||||
spaceNotConnectedTitle: 'Аккаунт маркета не привязан',
|
||||
bindSpaceButton: 'Привязать аккаунт сейчас',
|
||||
bindRequiredToProceed: 'Привяжите аккаунт, чтобы продолжить',
|
||||
prerequisitesTitle: 'Предварительные требования для публикации',
|
||||
prerequisitesDescription:
|
||||
'Перед публикацией плагина в каталоге убедитесь, что выполнены следующие требования.',
|
||||
req1Title: 'Требование 1: Привязать аккаунт LangBot Space',
|
||||
req1Desc:
|
||||
'Локальный инстанс LangBot должен быть привязан к облачной платформе LangBot Space.',
|
||||
reqBound: 'Привязано',
|
||||
reqCompleted: 'Готово',
|
||||
req2Title: 'Требование 2: Создать профиль создателя в Space',
|
||||
req2Desc:
|
||||
'Аккаунт LangBot должен иметь профиль создателя, а его идентификатор должен совпадать с автором плагина.',
|
||||
req2WaitSpace:
|
||||
'Сначала выполните Требование 1 (Привязка аккаунта LangBot)',
|
||||
req2NoCreator: 'В этом аккаунте Space еще не создан профиль создателя',
|
||||
req2Mismatch: 'Идентификатор создателя не совпадает',
|
||||
req2MismatchDetail:
|
||||
'Ваш идентификатор создателя Space — @{{creator}}, а автор плагина — {{author}}.',
|
||||
createCreatorButton: 'Создать профиль создателя',
|
||||
manageCreatorButton: 'Настройки создателя',
|
||||
pendingReq: 'Ожидается',
|
||||
autoProceedTip:
|
||||
'После выполнения всех требований мастер публикации откроется автоматически.',
|
||||
refreshStatus: 'Обновить статус',
|
||||
reviewGitSync: 'Синхронизация с GitHub',
|
||||
reviewGitSyncEnabled: 'Включено (отправка в {{branch}})',
|
||||
reviewGitSyncDisabled: 'Отключено',
|
||||
reviewChangelog: 'Примечания к выпуску',
|
||||
reviewChangelogEmpty: '(Примечания к выпуску не указаны)',
|
||||
pluginIdentifier: 'Идентификатор плагина',
|
||||
banners: {
|
||||
guideTitle: 'Инструкция:',
|
||||
disabledTooltip: 'Выполните действия из баннера выше перед публикацией',
|
||||
bindButton: 'Подключить аккаунт LangBot',
|
||||
reauthButton: 'Повторно авторизоваться',
|
||||
createCreatorButton: 'Создать профиль автора',
|
||||
manageCreatorButton: 'Настройки автора',
|
||||
viewMyPluginsButton: 'Мои плагины в Space',
|
||||
refreshButton: 'Обновить статус',
|
||||
retryButton: 'Повторить попытку',
|
||||
readyToUpload: {
|
||||
title: 'Все готово к публикации',
|
||||
description:
|
||||
'Аккаунт LangBot и профиль автора подтверждены. Проверьте данные и подтвердите публикацию.',
|
||||
},
|
||||
notConnected: {
|
||||
title: 'Аккаунт LangBot не подключен',
|
||||
description:
|
||||
'Локальный экземпляр не подключен к LangBot Space. Для публикации требуется аккаунт LangBot.',
|
||||
guide:
|
||||
'Нажмите «Подключить аккаунт LangBot» для авторизации. После подключения вы сможете продолжить публикацию.',
|
||||
},
|
||||
noCreator: {
|
||||
title: 'Профиль автора в Space не создан',
|
||||
description:
|
||||
'Для публикации плагинов в каталоге требуется профиль автора (Creator Profile) для указания авторства.',
|
||||
guide:
|
||||
'Нажмите кнопку ниже, чтобы создать профиль (идентификатор, описание и аватар). Затем нажмите «Обновить статус».',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: 'Автор плагина не совпадает с профилем автора в Space',
|
||||
description:
|
||||
'В manifest.yaml указан автор «{{author}}», а ваш идентификатор в Space — «@{{creator}}». Они должны совпадать.',
|
||||
guide:
|
||||
'Решение:\n1. Измените metadata.author в manifest.yaml на «{{creator}}»;\n2. Либо переключитесь на аккаунт LangBot, соответствующий «{{author}}».',
|
||||
},
|
||||
pendingReview: {
|
||||
title: 'Предыдущая версия ожидает проверки',
|
||||
description:
|
||||
'Предыдущая версия этого плагина находится на рассмотрении. Дождитесь завершения проверки перед отправкой новой версии.',
|
||||
guide: 'Нажмите «Мои плагины в Space», чтобы узнать статус проверки.',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'Сессия авторизации Space истекла',
|
||||
description:
|
||||
'Токен доступа Space недействителен или истек. Не удалось выполнить авторизацию.',
|
||||
guide:
|
||||
'Нажмите «Повторно авторизоваться», чтобы войти в аккаунт и повторить попытку.',
|
||||
},
|
||||
validationFailed: {
|
||||
title: 'Проверка пакета плагина не пройдена',
|
||||
description: 'Ошибка проверки пакета: {{detail}}',
|
||||
guide:
|
||||
'Проверьте поля manifest.yaml, формат версии SemVer, наличие иконки и файла README.',
|
||||
},
|
||||
genericError: {
|
||||
title: 'Не удалось опубликовать плагин',
|
||||
description: '{{detail}}',
|
||||
guide: 'Проверьте сетевое подключение или повторите попытку позже.',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile:
|
||||
'Создайте профиль автора в Space перед загрузкой плагинов',
|
||||
authorMismatch:
|
||||
'Автор плагина «{{author}}» не совпадает с идентификатором автора в Space «@{{slug}}»',
|
||||
pendingReview:
|
||||
'У вас уже есть версия плагина на рассмотрении. Дождитесь завершения проверки',
|
||||
sessionExpired:
|
||||
'Сессия Space истекла. Пожалуйста, выполните повторный вход',
|
||||
notConnected:
|
||||
'Подключите учетную запись LangBot Space перед загрузкой плагинов',
|
||||
packageFailed:
|
||||
'Не удалось собрать пакет плагина. Проверьте структуру файлов и код',
|
||||
inspectFailed: 'Ошибка проверки манифеста плагина: {{detail}}',
|
||||
certPrecheckFailed: 'Ошибка предварительной сертификации: {{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: 'Предпросмотр локального пакета плагина',
|
||||
unpacking: 'Распаковка пакета для предпросмотра...',
|
||||
|
||||
@@ -1772,6 +1772,193 @@ const thTH = {
|
||||
uploadLocal: 'อัปโหลดจากเครื่อง',
|
||||
debugging: 'ดีบัก',
|
||||
uploadLocalPlugin: 'อัปโหลดปลั๊กอินจากเครื่อง',
|
||||
spaceUpload: {
|
||||
menuAction: 'อัปโหลดไปยังตลาดปลั๊กอิน',
|
||||
title: 'อัปโหลดปลั๊กอินไปยังตลาดปลั๊กอิน',
|
||||
description:
|
||||
'บิลด์ปลั๊กอินที่กำลังดีบัก {{author}}/{{name}} แล้วเผยแพร่ไปยังตลาดปลั๊กอิน',
|
||||
loadFailed: 'โหลดข้อมูลการอัปโหลดปลั๊กอินไม่สำเร็จ',
|
||||
spaceNotConnected:
|
||||
'Workspace นี้ยังไม่ได้ผูกกับบัญชี LangBot กรุณาผูกก่อนอัปโหลด',
|
||||
label: 'ชื่อ',
|
||||
version: 'เวอร์ชัน',
|
||||
pluginDescription: 'คำอธิบาย',
|
||||
repository: 'ที่เก็บโค้ด',
|
||||
repositoryPublicHint:
|
||||
'เมื่อซิงค์ไปยัง GitHub ที่เก็บที่ยังไม่มีจะถูกสร้าง และที่เก็บส่วนตัวที่มีอยู่จะถูกเปลี่ยนเป็นสาธารณะ',
|
||||
author: 'ผู้พัฒนา',
|
||||
authorHint: 'เมื่อเผยแพร่จะใช้ชื่อผู้พัฒนานี้บนตลาดปลั๊กอิน',
|
||||
license: 'สัญญาอนุญาต',
|
||||
licensePlaceholder: 'เลือกสัญญาอนุญาต',
|
||||
licenseNone: 'ไม่ระบุ',
|
||||
changelog: 'บันทึกการเปลี่ยนแปลง',
|
||||
syncGithub: 'ซิงค์ไปยัง GitHub',
|
||||
syncGithubHint:
|
||||
'คอมมิตและพุชไดเรกทอรีทำงานของปลั๊กอิน โดยค่าเริ่มต้นจะใช้การตั้งค่า git ในเครื่อง คุณสามารถระบุ URL และโทเคนด้านล่างเพื่อแทนที่ได้ หากระบุโทเคนและยังไม่มีที่เก็บ GitHub ระบบจะสร้างให้อัตโนมัติ',
|
||||
repoUrl: 'URL ที่เก็บ (ไม่บังคับ)',
|
||||
repoUrlHint: 'เว้นว่างเพื่อใช้ origin ของ git ในเครื่อง',
|
||||
token: 'โทเคนการเข้าถึง (ไม่บังคับ)',
|
||||
tokenPlaceholder: 'ใช้เฉพาะการพุชครั้งนี้',
|
||||
branch: 'สาขา',
|
||||
commitMessage: 'ข้อความคอมมิต',
|
||||
syncOnly: 'ซิงค์ไปยัง GitHub เท่านั้น',
|
||||
gitSynced: 'ซิงค์ไปยัง GitHub แล้ว',
|
||||
gitCommittedNoRemote:
|
||||
'คอมมิตในเครื่องแล้ว แต่ยังไม่ได้ตั้งค่ารีโมตให้พุช',
|
||||
gitNothing: 'ไม่มีสิ่งที่ต้องซิงค์',
|
||||
gitFailed: 'ซิงค์ Git ล้มเหลว: ',
|
||||
uploadSuccess: 'อัปโหลดปลั๊กอินไปยังตลาดปลั๊กอินแล้ว',
|
||||
reviewOnSpace: 'ส่งขอตรวจสอบบนตลาดปลั๊กอิน',
|
||||
uploadFailed: 'อัปโหลดปลั๊กอินล้มเหลว: ',
|
||||
basicInfo: 'ข้อมูลพื้นฐาน',
|
||||
changeIcon: 'เปลี่ยนไอคอน',
|
||||
iconInvalidType: 'กรุณาเลือกไฟล์รูปภาพ',
|
||||
iconTooLarge: 'ไอคอนต้องมีขนาดน้อยกว่า 10 MB',
|
||||
labelRequired: 'กรอกชื่ออย่างน้อยหนึ่งภาษา',
|
||||
langEn: 'English',
|
||||
langZhHans: 'จีนตัวย่อ',
|
||||
langZhHant: 'จีนตัวเต็ม',
|
||||
langJa: 'ญี่ปุ่น',
|
||||
langTh: 'ไทย',
|
||||
langVi: 'เวียดนาม',
|
||||
langEs: 'สเปน',
|
||||
uploadButton: 'อัปโหลดไปยังตลาดปลั๊กอิน',
|
||||
notDebugHint:
|
||||
'อัปโหลดได้เฉพาะปลั๊กอินที่เชื่อมต่อเพื่อดีบักเท่านั้น กรุณาเริ่มดีบักด้วย `lbp run` ก่อน',
|
||||
stepInfo: 'ข้อมูลปลั๊กอิน',
|
||||
stepSettings: 'บันทึกการเปลี่ยนแปลง',
|
||||
stepConfirm: 'ยืนยันการอัปโหลด',
|
||||
manifestNotice:
|
||||
'ข้อมูลต่อไปนี้อ่านจากไฟล์ manifest (manifest.yaml) เพื่อยืนยันเท่านั้น หากต้องการแก้ไขโปรดแก้ไขไฟล์ manifest แล้วเริ่มการดีบักใหม่',
|
||||
manifestSource: 'ยืนยันจาก manifest',
|
||||
nextStep: 'ถัดไป',
|
||||
prevStep: 'ย้อนกลับ',
|
||||
nextToSettings: 'ถัดไป: บันทึกการเปลี่ยนแปลง',
|
||||
nextToConfirm: 'ถัดไป: ยืนยันการอัปโหลด',
|
||||
confirmAndUpload: 'ยืนยันและอัปโหลดไปยังตลาดส่วนขยาย',
|
||||
noDescription: 'ไม่มีคำอธิบาย',
|
||||
noRepository: 'ไม่ได้ระบุคลังรหัส',
|
||||
noLicense: 'ไม่ได้ระบุสัญญาอนุญาต',
|
||||
changelogPlaceholder:
|
||||
'กรอกบันทึกการเปลี่ยนแปลงสำหรับเวอร์ชันนี้ (เช่น คุณสมบัติใหม่ การแก้ไขข้อบกพร่อง)...',
|
||||
changelogHint:
|
||||
'บันทึกการเปลี่ยนแปลงจะแสดงในประวัติเวอร์ชันของตลาดส่วนขยาย',
|
||||
reviewSummaryTitle: 'ตรวจสอบก่อนเผยแพร่',
|
||||
reviewTargetMarket: 'ตลาดเป้าหมาย',
|
||||
reviewTargetMarketValue: 'ตลาดส่วนขยาย (LangBot Space)',
|
||||
reviewAccountStatus: 'สถานะบัญชี',
|
||||
reviewAccountConnected: 'เชื่อมต่อกับบัญชี LangBot แล้ว',
|
||||
reviewAccountNotConnected:
|
||||
'ยังไม่ได้เชื่อมต่อบัญชี (การอัปโหลดจะล้มเหลว)',
|
||||
spaceNotConnectedTitle: 'ยังไม่ได้ผูกบัญชีตลาดส่วนขยาย',
|
||||
bindSpaceButton: 'ผูกบัญชีทันที',
|
||||
bindRequiredToProceed: 'กรุณาผูกบัญชีก่อนดำเนินการต่อ',
|
||||
prerequisitesTitle: 'ข้อกำหนดเบื้องต้นสำหรับการเผยแพร่',
|
||||
prerequisitesDescription:
|
||||
'ก่อนเผยแพร่ปลั๊กอินไปยังตลาดส่วนขยาย โปรดตรวจสอบว่าคุณมีคุณสมบัติตรงตามข้อกำหนดต่อไปนี้',
|
||||
req1Title: 'ข้อกำหนดที่ 1: ผูกบัญชี LangBot Space',
|
||||
req1Desc:
|
||||
'อินสแตนซ์ LangBot ในเครื่องต้องเข้าสู่ระบบหรือผูกกับ LangBot Space',
|
||||
reqBound: 'ผูกบัญชีแล้ว',
|
||||
reqCompleted: 'พร้อมใช้งาน',
|
||||
req2Title: 'ข้อกำหนดที่ 2: สร้างตัวตนผู้สร้างใน Space',
|
||||
req2Desc:
|
||||
'บัญชี LangBot ต้องมีตัวตนผู้สร้างและชื่อระบุตัวตนต้องตรงกับชื่อผู้เขียนปลั๊กอิน',
|
||||
req2WaitSpace: 'โปรดทำข้อกำหนดที่ 1 (ผูกบัญชี LangBot) ให้เสร็จก่อน',
|
||||
req2NoCreator: 'บัญชี LangBot นี้ยังไม่ได้สร้างตัวตนผู้สร้าง',
|
||||
req2Mismatch: 'ชื่อระบุตัวตนผู้สร้างไม่ตรงกัน',
|
||||
req2MismatchDetail:
|
||||
'ชื่อผู้สร้าง Space ของคุณคือ @{{creator}} แต่ชื่อผู้เขียนปลั๊กอินคือ {{author}}',
|
||||
createCreatorButton: 'ไปสร้างตัวตนผู้สร้าง',
|
||||
manageCreatorButton: 'ดูการตั้งค่าผู้สร้าง',
|
||||
pendingReq: 'รอดำเนินการ',
|
||||
autoProceedTip:
|
||||
'เมื่อผ่านข้อกำหนดทั้งหมดแล้ว ตัวช่วยอัปโหลดจะเปิดขึ้นโดยอัตโนมัติ',
|
||||
refreshStatus: 'รีเฟรชสถานะ',
|
||||
reviewGitSync: 'การซิงค์ GitHub',
|
||||
reviewGitSyncEnabled: 'เปิดใช้งาน (ส่งไปยัง {{branch}})',
|
||||
reviewGitSyncDisabled: 'ปิดใช้งาน',
|
||||
reviewChangelog: 'บันทึกการเปลี่ยนแปลง',
|
||||
reviewChangelogEmpty: '(ไม่ได้กรอกบันทึกการเปลี่ยนแปลง)',
|
||||
pluginIdentifier: 'รหัสระบุปลั๊กอิน',
|
||||
banners: {
|
||||
guideTitle: 'คำแนะนำการดำเนินการ:',
|
||||
disabledTooltip: 'โปรดทำตามคำแนะนำในแบนเนอร์ด้านบนก่อนอัปโหลด',
|
||||
bindButton: 'เชื่อมต่อบัญชี LangBot',
|
||||
reauthButton: 'ยืนยันตัวตนบัญชีใหม่',
|
||||
createCreatorButton: 'สร้างโปรไฟล์ผู้สร้าง',
|
||||
manageCreatorButton: 'จัดการโปรไฟล์ผู้สร้าง',
|
||||
viewMyPluginsButton: 'ดูปลั๊กอินของฉันบน Space',
|
||||
refreshButton: 'รีเฟรชสถานะ',
|
||||
retryButton: 'ลองใหม่อีกครั้ง',
|
||||
readyToUpload: {
|
||||
title: 'พร้อมสำหรับการเผยแพร่',
|
||||
description:
|
||||
'บัญชี LangBot และโปรไฟล์ผู้สร้างพร้อมแล้ว โปรดตรวจสอบรายละเอียดด้านล่างและยืนยันการอัปโหลด',
|
||||
},
|
||||
notConnected: {
|
||||
title: 'ยังไม่ได้เชื่อมต่อบัญชี LangBot',
|
||||
description:
|
||||
'อินสแตนซ์นี้ยังไม่ได้เชื่อมต่อกับ LangBot Space จำเป็นต้องมีบัญชี LangBot เพื่อเผยแพร่ปลั๊กอิน',
|
||||
guide:
|
||||
'โปรดคลิก "เชื่อมต่อบัญชี LangBot" ด้านล่างเพื่อเข้าสู่ระบบ เมื่อเชื่อมต่อแล้วจะสามารถเผยแพร่ปลั๊กอินได้',
|
||||
},
|
||||
noCreator: {
|
||||
title: 'ยังไม่ได้สร้างโปรไฟล์ผู้สร้างบน Space',
|
||||
description:
|
||||
'การเผยแพร่ปลั๊กอินต้องมีโปรไฟล์ผู้สร้าง (Creator Profile) เพื่อแสดงข้อมูลผู้เขียนในตลาด',
|
||||
guide:
|
||||
'โปรดคลิกปุ่มด้านล่างเพื่อตั้งค่า Slug, ประวัติย่อ และรูปโปรไฟล์บน Space จากนั้นกลับมาคลิก "รีเฟรชสถานะ"',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: 'ผู้เขียนปลั๊กอินไม่ตรงกับรหัสผู้สร้าง Space',
|
||||
description:
|
||||
'ผู้เขียนที่ระบุใน manifest คือ "{{author}}" แต่รหัสผู้สร้างของคุณคือ "@{{creator}}" ทั้งสองต้องตรงกันทุกประการ',
|
||||
guide:
|
||||
'วิธีแก้ไข:\n1. แก้ไข metadata.author ใน manifest.yaml เป็น "{{creator}}"\n2. หรือเปลี่ยนไปใช้บัญชี LangBot ที่ตรงกับ "{{author}}"',
|
||||
},
|
||||
pendingReview: {
|
||||
title: 'มีเวอร์ชันที่อยู่ระหว่างรอการตรวจสอบแล้ว',
|
||||
description:
|
||||
'มีเวอร์ชันก่อนหน้าของปลั๊กอินนี้ที่อยู่ระหว่างการตรวจสอบ โปรดรอให้การตรวจสอบเสร็จสิ้นก่อนส่งเวอร์ชันใหม่',
|
||||
guide:
|
||||
'โปรดคลิก "ดูปลั๊กอินของฉันบน Space" เพื่อตรวจสอบสถานะและข้อเสนอแนะ',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'เซสชันการเข้าสู่ระบบ Space หมดอายุ',
|
||||
description:
|
||||
'โทเค็นการเข้าถึง Space หมดอายุหรือไม่ถูกต้อง การยืนยันตัวตนล้มเหลว',
|
||||
guide:
|
||||
'โปรดคลิก "ยืนยันตัวตนบัญชีใหม่" ด้านล่างเพื่อเข้าสู่ระบบใหม่อีกครั้ง',
|
||||
},
|
||||
validationFailed: {
|
||||
title: 'การตรวจสอบแพ็กเกจปลั๊กอินไม่ผ่าน',
|
||||
description: 'การตรวจสอบล้มเหลว: {{detail}}',
|
||||
guide:
|
||||
'โปรดตรวจสอบไฟล์ manifest.yaml, รูปแบบเวอร์ชัน SemVer, รูปไอคอน และไฟล์ README',
|
||||
},
|
||||
genericError: {
|
||||
title: 'การเผยแพร่ปลั๊กอินล้มเหลว',
|
||||
description: '{{detail}}',
|
||||
guide:
|
||||
'โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตหรือลองใหม่อีกครั้งในภายหลัง',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile:
|
||||
'โปรดตั้งค่าโปรไฟล์ผู้สร้างบน Space ก่อนอัปโหลดปลั๊กอิน',
|
||||
authorMismatch:
|
||||
'ผู้เขียนปลั๊กอิน "{{author}}" ไม่ตรงกับรหัสผู้สร้างบน Space ของคุณ "@{{slug}}"',
|
||||
pendingReview:
|
||||
'คุณมีปลั๊กอินที่อยู่ระหว่างการตรวจสอบแล้ว โปรดรอให้การตรวจสอบเสร็จสิ้น',
|
||||
sessionExpired: 'เซสชัน Space หมดอายุแล้ว โปรดเข้าสู่ระบบใหม่อีกครั้ง',
|
||||
notConnected: 'โปรดเชื่อมต่อบัญชี LangBot Space ก่อนอัปโหลดปลั๊กอิน',
|
||||
packageFailed:
|
||||
'สร้างแพ็กเกจปลั๊กอินไม่สำเร็จ โปรดตรวจสอบโค้ดและโครงสร้างไฟล์',
|
||||
inspectFailed: 'การตรวจสอบไฟล์กำกับปลั๊กอินล้มเหลว: {{detail}}',
|
||||
certPrecheckFailed: 'การตรวจสอบสิทธิ์ล่วงหน้าล้มเหลว: {{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: 'ดูตัวอย่างแพ็กเกจปลั๊กอินในเครื่อง',
|
||||
unpacking: 'กำลังแตกแพ็กเกจเพื่อดูตัวอย่าง...',
|
||||
|
||||
@@ -1800,6 +1800,196 @@ const viVN = {
|
||||
uploadLocal: 'Tải lên cục bộ',
|
||||
debugging: 'Gỡ lỗi',
|
||||
uploadLocalPlugin: 'Tải lên Plugin cục bộ',
|
||||
spaceUpload: {
|
||||
menuAction: 'Tải lên chợ ứng dụng',
|
||||
title: 'Tải plugin lên chợ ứng dụng',
|
||||
description:
|
||||
'Build plugin đang gỡ lỗi {{author}}/{{name}} và phát hành lên chợ ứng dụng.',
|
||||
loadFailed: 'Không tải được thông tin tải lên của plugin',
|
||||
spaceNotConnected:
|
||||
'Workspace này chưa liên kết với tài khoản LangBot. Hãy liên kết trước khi tải lên.',
|
||||
label: 'Tên',
|
||||
version: 'Phiên bản',
|
||||
pluginDescription: 'Mô tả',
|
||||
repository: 'Kho lưu trữ',
|
||||
repositoryPublicHint:
|
||||
'Khi đồng bộ lên GitHub, kho chưa tồn tại sẽ được tạo và kho riêng tư hiện có sẽ được chuyển sang công khai.',
|
||||
author: 'Tác giả',
|
||||
authorHint:
|
||||
'Khi phát hành sẽ dùng danh tính tác giả này trên chợ ứng dụng.',
|
||||
license: 'Giấy phép',
|
||||
licensePlaceholder: 'Chọn giấy phép',
|
||||
licenseNone: 'Không chỉ định',
|
||||
changelog: 'Nhật ký thay đổi',
|
||||
syncGithub: 'Đồng bộ lên GitHub',
|
||||
syncGithubHint:
|
||||
'Commit và push thư mục làm việc của plugin. Mặc định dùng cấu hình git cục bộ; URL kho và token bên dưới có thể ghi đè. Nếu có token và kho GitHub chưa tồn tại, hệ thống sẽ tự tạo.',
|
||||
repoUrl: 'URL kho (tùy chọn)',
|
||||
repoUrlHint: 'Để trống để dùng origin git cục bộ.',
|
||||
token: 'Token truy cập (tùy chọn)',
|
||||
tokenPlaceholder: 'Chỉ dùng cho lần push này',
|
||||
branch: 'Nhánh',
|
||||
commitMessage: 'Thông điệp commit',
|
||||
syncOnly: 'Chỉ đồng bộ lên GitHub',
|
||||
gitSynced: 'Đã đồng bộ lên GitHub',
|
||||
gitCommittedNoRemote:
|
||||
'Đã commit cục bộ nhưng chưa cấu hình remote để push',
|
||||
gitNothing: 'Không có thay đổi để đồng bộ',
|
||||
gitFailed: 'Đồng bộ Git thất bại: ',
|
||||
uploadSuccess: 'Đã tải plugin lên chợ ứng dụng',
|
||||
reviewOnSpace: 'Gửi duyệt trên chợ ứng dụng',
|
||||
uploadFailed: 'Tải plugin lên thất bại: ',
|
||||
basicInfo: 'Thông tin cơ bản',
|
||||
changeIcon: 'Đổi biểu tượng',
|
||||
iconInvalidType: 'Vui lòng chọn một tệp hình ảnh',
|
||||
iconTooLarge: 'Biểu tượng phải nhỏ hơn 10 MB',
|
||||
labelRequired: 'Nhập tên bằng ít nhất một ngôn ngữ',
|
||||
langEn: 'English',
|
||||
langZhHans: 'Tiếng Trung giản thể',
|
||||
langZhHant: 'Tiếng Trung phồn thể',
|
||||
langJa: 'Tiếng Nhật',
|
||||
langTh: 'Tiếng Thái',
|
||||
langVi: 'Tiếng Việt',
|
||||
langEs: 'Tiếng Tây Ban Nha',
|
||||
uploadButton: 'Tải lên chợ ứng dụng',
|
||||
notDebugHint:
|
||||
'Chỉ có thể tải lên plugin đang kết nối gỡ lỗi. Hãy chạy `lbp run` để bắt đầu gỡ lỗi trước.',
|
||||
stepInfo: 'Thông tin plugin',
|
||||
stepSettings: 'Ghi chú phát hành',
|
||||
stepConfirm: 'Xác nhận tải lên',
|
||||
manifestNotice:
|
||||
'Thông tin dưới đây được đọc từ tệp manifest (manifest.yaml) chỉ để xác nhận. Nếu cần sửa đổi, vui lòng chỉnh sửa tệp manifest và khởi động lại gỡ lỗi.',
|
||||
manifestSource: 'Đã xác nhận từ manifest',
|
||||
nextStep: 'Tiếp theo',
|
||||
prevStep: 'Quay lại',
|
||||
nextToSettings: 'Tiếp theo: Ghi chú phát hành',
|
||||
nextToConfirm: 'Tiếp theo: Xác nhận tải lên',
|
||||
confirmAndUpload: 'Xác nhận tải lên Chợ Tiện ích mở rộng',
|
||||
noDescription: 'Chưa có mô tả',
|
||||
noRepository: 'Chưa chỉ định kho mã nguồn',
|
||||
noLicense: 'Chưa chỉ định giấy phép',
|
||||
changelogPlaceholder:
|
||||
'Nhập ghi chú phát hành cho phiên bản này (ví dụ tính năng mới, sửa lỗi)...',
|
||||
changelogHint:
|
||||
'Nhật ký thay đổi sẽ hiển thị trong lịch sử phiên bản của Chợ Tiện ích mở rộng.',
|
||||
reviewSummaryTitle: 'Kiểm tra trước khi phát hành',
|
||||
reviewTargetMarket: 'Chợ mục tiêu',
|
||||
reviewTargetMarketValue: 'Chợ Tiện ích mở rộng (LangBot Space)',
|
||||
reviewAccountStatus: 'Trạng thái tài khoản',
|
||||
reviewAccountConnected: 'Đã kết nối tài khoản LangBot',
|
||||
reviewAccountNotConnected: 'Chưa kết nối tài khoản (tải lên sẽ thất bại)',
|
||||
spaceNotConnectedTitle: 'Chưa liên kết tài khoản chợ',
|
||||
bindSpaceButton: 'Liên kết tài khoản ngay',
|
||||
bindRequiredToProceed: 'Vui lòng liên kết tài khoản để tiếp tục',
|
||||
prerequisitesTitle: 'Yêu cầu tiên quyết để phát hành',
|
||||
prerequisitesDescription:
|
||||
'Trước khi tải plugin lên chợ tiện ích mở rộng, vui lòng đảm bảo đáp ứng các yêu cầu sau.',
|
||||
req1Title: 'Yêu cầu 1: Liên kết tài khoản LangBot Space',
|
||||
req1Desc:
|
||||
'Cài đặt LangBot cục bộ phải được đăng nhập hoặc liên kết với LangBot Space.',
|
||||
reqBound: 'Đã liên kết',
|
||||
reqCompleted: 'Đã sẵn sàng',
|
||||
req2Title: 'Yêu cầu 2: Tạo danh tính người sáng tạo trên Space',
|
||||
req2Desc:
|
||||
'Tài khoản LangBot phải có danh tính người sáng tạo và định danh phải khớp với tác giả plugin.',
|
||||
req2WaitSpace: 'Vui lòng hoàn thành Yêu cầu 1 trước',
|
||||
req2NoCreator: 'Tài khoản LangBot này chưa tạo danh tính người sáng tạo',
|
||||
req2Mismatch: 'Định danh người sáng tạo không khớp',
|
||||
req2MismatchDetail:
|
||||
'Định danh Space của bạn là @{{creator}}, nhưng tác giả plugin là {{author}}.',
|
||||
createCreatorButton: 'Đi đến tạo danh tính',
|
||||
manageCreatorButton: 'Xem cài đặt người sáng tạo',
|
||||
pendingReq: 'Đang chờ',
|
||||
autoProceedTip:
|
||||
'Khi hoàn thành tất cả các yêu cầu, quy trình tải lên sẽ tự động mở ra.',
|
||||
refreshStatus: 'Làm mới trạng thái',
|
||||
reviewGitSync: 'Đồng bộ GitHub',
|
||||
reviewGitSyncEnabled: 'Đã bật (đẩy lên {{branch}})',
|
||||
reviewGitSyncDisabled: 'Đã tắt',
|
||||
reviewChangelog: 'Ghi chú phát hành',
|
||||
reviewChangelogEmpty: '(Chưa nhập ghi chú phát hành)',
|
||||
pluginIdentifier: 'Định danh plugin',
|
||||
banners: {
|
||||
guideTitle: 'Hướng dẫn thực hiện:',
|
||||
disabledTooltip:
|
||||
'Vui lòng làm theo hướng dẫn ở biểu ngữ phía trên trước khi tải lên',
|
||||
bindButton: 'Kết nối tài khoản LangBot',
|
||||
reauthButton: 'Xác thực lại tài khoản',
|
||||
createCreatorButton: 'Mở hồ sơ người sáng tạo',
|
||||
manageCreatorButton: 'Quản lý hồ sơ người sáng tạo',
|
||||
viewMyPluginsButton: 'Xem plugin của tôi trên Space',
|
||||
refreshButton: 'Làm mới trạng thái',
|
||||
retryButton: 'Thử tải lên lại',
|
||||
readyToUpload: {
|
||||
title: 'Đã sẵn sàng xuất bản',
|
||||
description:
|
||||
'Tài khoản LangBot và hồ sơ người sáng tạo đã sẵn sàng. Vui lòng kiểm tra chi tiết bên dưới và xác nhận tải lên.',
|
||||
},
|
||||
notConnected: {
|
||||
title: 'Chưa kết nối tài khoản LangBot Space',
|
||||
description:
|
||||
'Phiên bản cục bộ này chưa được kết nối với LangBot Space. Cần có tài khoản LangBot để xác minh danh tính và xuất bản plugin.',
|
||||
guide:
|
||||
'Vui lòng nhấp vào "Kết nối tài khoản LangBot" bên dưới để đăng nhập. Sau khi kết nối, bạn có thể xuất bản plugin.',
|
||||
},
|
||||
noCreator: {
|
||||
title: 'Chưa tạo hồ sơ người sáng tạo trên Space',
|
||||
description:
|
||||
'Việc xuất bản plugin lên chợ ứng dụng yêu cầu hồ sơ người sáng tạo (Creator Profile) để hiển thị thông tin tác giả.',
|
||||
guide:
|
||||
'Vui lòng nhấp vào nút bên dưới để thiết lập slug, tiểu sử và ảnh đại diện trên Space. Sau đó quay lại và nhấp "Làm mới trạng thái".',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: 'Tác giả plugin không khớp với mã định danh người sáng tạo',
|
||||
description:
|
||||
'Tác giả trong tệp manifest là "{{author}}", nhưng mã định danh người sáng tạo trên Space là "@{{creator}}". Cả hai phải hoàn toàn khớp nhau.',
|
||||
guide:
|
||||
'Cách giải quyết:\n1. Cập nhật manifest.yaml và đặt metadata.author thành "{{creator}}";\n2. Hoặc chuyển sang tài khoản LangBot khớp với "{{author}}". Sau đó nhấp "Làm mới trạng thái".',
|
||||
},
|
||||
pendingReview: {
|
||||
title: 'Đã có bản đệ trình đang chờ xem xét',
|
||||
description:
|
||||
'Bản đệ trình trước đó cho plugin này hiện đang chờ xem xét. Vui lòng đợi bản xem xét hoàn tất trước khi gửi phiên bản mới.',
|
||||
guide:
|
||||
'Vui lòng nhấp vào "Xem plugin của tôi trên Space" bên dưới để kiểm tra tiến trình xem xét.',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'Phiên xác thực Space đã hết hạn',
|
||||
description:
|
||||
'Mã truy cập Space đã hết hạn hoặc không hợp lệ. Xác thực thất bại.',
|
||||
guide:
|
||||
'Vui lòng nhấp vào "Xác thực lại tài khoản" bên dưới để đăng nhập lại và thử lại.',
|
||||
},
|
||||
validationFailed: {
|
||||
title: 'Xác thực gói plugin thất bại',
|
||||
description: 'Kiểm tra gói plugin thất bại: {{detail}}',
|
||||
guide:
|
||||
'Vui lòng kiểm tra các trường trong manifest.yaml, định dạng phiên bản SemVer và các tệp biểu tượng, README.',
|
||||
},
|
||||
genericError: {
|
||||
title: 'Xuất bản plugin thất bại',
|
||||
description: '{{detail}}',
|
||||
guide:
|
||||
'Vui lòng kiểm tra kết nối mạng hoặc thử lại sau. Nếu sự cố vẫn tiếp diễn, hãy liên hệ cộng đồng Space.',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile:
|
||||
'Vui lòng thiết lập hồ sơ người sáng tạo trên Space trước khi tải lên plugin',
|
||||
authorMismatch:
|
||||
'Tác giả plugin "{{author}}" không khớp với mã định danh người sáng tạo trên Space "@{slug}"',
|
||||
pendingReview:
|
||||
'Bạn đã có bản đệ trình plugin đang chờ xem xét. Vui lòng đợi hoàn tất',
|
||||
sessionExpired:
|
||||
'Phiên đăng nhập Space đã hết hạn. Vui lòng xác thực lại',
|
||||
notConnected:
|
||||
'Vui lòng kết nối tài khoản LangBot Space trước khi tải lên plugin',
|
||||
packageFailed:
|
||||
'Đóng gói plugin thất bại. Vui lòng kiểm tra mã nguồn và thư mục',
|
||||
inspectFailed: 'Kiểm tra tệp khai báo plugin thất bại: {{detail}}',
|
||||
certPrecheckFailed: 'Kiểm tra chứng nhận trước thất bại: {{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: 'Xem trước gói plugin cục bộ',
|
||||
unpacking: 'Đang giải nén để xem trước gói...',
|
||||
|
||||
@@ -1698,6 +1698,199 @@ const zhHans = {
|
||||
uploadLocal: '本地上传',
|
||||
debugging: '调试中',
|
||||
uploadLocalPlugin: '上传本地插件',
|
||||
spaceUpload: {
|
||||
menuAction: '上传到扩展市场',
|
||||
title: '上传插件到扩展市场',
|
||||
description: '构建正在调试的插件 {{author}}/{{name}} 并发布到扩展市场。',
|
||||
loadFailed: '加载插件上传信息失败',
|
||||
spaceNotConnected: '当前工作区未绑定 LangBot 账号,请先绑定后再上传。',
|
||||
label: '名称',
|
||||
version: '版本',
|
||||
pluginDescription: '描述',
|
||||
repository: '代码仓库',
|
||||
repositoryPublicHint:
|
||||
'同步到 GitHub 时,仓库不存在会自动创建,已存在的私有仓库也会自动改为公开。',
|
||||
author: '作者',
|
||||
authorHint: '发布到扩展市场时将使用此作者身份。',
|
||||
license: '开源协议',
|
||||
licensePlaceholder: '选择开源协议',
|
||||
licenseNone: '未指定',
|
||||
changelog: '更新说明',
|
||||
syncGithub: '同步到 GitHub',
|
||||
syncGithubHint:
|
||||
'提交并推送插件工作目录。默认复用本地 git 配置;也可在下方填写仓库地址与 token 覆盖。填写 token 且 GitHub 仓库尚不存在时,会自动创建该仓库。',
|
||||
repoUrl: '仓库地址(可选)',
|
||||
repoUrlHint: '留空则复用本地 git 的 origin。',
|
||||
token: '访问令牌(可选)',
|
||||
tokenPlaceholder: '仅用于本次推送',
|
||||
branch: '分支',
|
||||
commitMessage: '提交信息',
|
||||
syncOnly: '仅同步到 GitHub',
|
||||
gitSynced: '已同步到 GitHub',
|
||||
gitCommittedNoRemote: '已在本地提交,但未配置可推送的远程仓库',
|
||||
gitNothing: '没有需要同步的变更',
|
||||
gitFailed: 'Git 同步失败:',
|
||||
uploadSuccess: '插件已上传到扩展市场',
|
||||
reviewOnSpace: '前往扩展市场提交审核',
|
||||
uploadFailed: '上传插件失败:',
|
||||
basicInfo: '基本信息',
|
||||
changeIcon: '更换图标',
|
||||
iconInvalidType: '请选择图片文件',
|
||||
iconTooLarge: '图标需小于 10 MB',
|
||||
labelRequired: '至少填写一种语言的名称',
|
||||
langEn: 'English',
|
||||
langZhHans: '简体中文',
|
||||
langZhHant: '繁體中文',
|
||||
langJa: '日本語',
|
||||
langTh: '泰语',
|
||||
langVi: '越南语',
|
||||
langEs: '西班牙语',
|
||||
uploadButton: '上传到扩展市场',
|
||||
notDebugHint: '仅支持上传处于调试中的插件,请先通过 `lbp run` 启动调试。',
|
||||
stepInfo: '插件信息',
|
||||
stepSettings: '更新说明',
|
||||
stepConfirm: '确认上传',
|
||||
stepResult: '发布结果',
|
||||
startUpload: '开始上传',
|
||||
uploadingTitle: '正在上传到扩展市场...',
|
||||
uploadingDesc:
|
||||
'正在打包本地插件资源、验证规范并同步至 LangBot Space,请稍候...',
|
||||
uploadSuccessTitle: '插件发布成功!',
|
||||
uploadSuccessDesc:
|
||||
'插件 {{author}}/{{name}} (v{{version}}) 已成功提交至 LangBot Space 扩展市场。',
|
||||
reviewNoticeTitle: '审核流程提示',
|
||||
reviewNoticeDesc:
|
||||
'插件提交后将进入 Space 扩展市场审核队列。审核通过后,扩展市场中的所有用户均可直接浏览、安装与更新该插件。',
|
||||
viewInSpace: '前往扩展市场查看',
|
||||
viewMyPlugins: '管理我的插件',
|
||||
finishAndClose: '完成',
|
||||
retryUpload: '重试上传',
|
||||
backToEdit: '返回上一步修改',
|
||||
manifestNotice:
|
||||
'以下信息已从插件清单(manifest.yaml)读取,仅供确认。如需修改,请直接修改插件配置文件后重新调试。',
|
||||
manifestSource: '清单已确认',
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
nextToSettings: '下一步:更新说明',
|
||||
nextToConfirm: '下一步:确认上传',
|
||||
confirmAndUpload: '确认上传到扩展市场',
|
||||
noDescription: '暂无描述',
|
||||
noRepository: '未指定代码仓库',
|
||||
noLicense: '未指定开源协议',
|
||||
changelogPlaceholder:
|
||||
'请填写本次发布的更新说明(例如新增功能、修复的问题等)',
|
||||
changelogHint: '更新说明将展示在扩展市场的插件版本历史中。',
|
||||
reviewSummaryTitle: '发布信息核对',
|
||||
reviewTargetMarket: '发布目标',
|
||||
reviewTargetMarketValue: '扩展市场 (LangBot Space)',
|
||||
reviewAccountStatus: '账号绑定',
|
||||
reviewAccountConnected: '已绑定 LangBot 账号',
|
||||
reviewAccountNotConnected: '未绑定账号(无法上传)',
|
||||
spaceNotConnectedTitle: '未绑定扩展市场账号',
|
||||
bindSpaceButton: '关联账号',
|
||||
bindRequiredToProceed: '请先关联账号后再进入下一步',
|
||||
prerequisitesTitle: '发布前准备',
|
||||
prerequisitesDescription:
|
||||
'在上传插件至扩展市场前,需确认完成账号关联与创作者身份设置。',
|
||||
req1Title: '关联 LangBot Space 账号',
|
||||
req1Desc: '将当前本地实例关联至 LangBot Space 云端平台。',
|
||||
reqBound: '已关联',
|
||||
reqCompleted: '已就绪',
|
||||
req2Title: '设置 Space 创作者身份',
|
||||
req2Desc:
|
||||
'在 Space 开通创作者身份,且创作者标识(Slug)需与插件作者名一致。',
|
||||
req2WaitSpace: '需先完成 Space 账号关联',
|
||||
req2NoCreator: '当前 Space 账号尚未开通创作者身份',
|
||||
req2Mismatch: '创作者标识不匹配',
|
||||
req2MismatchDetail:
|
||||
'当前 Space 创作者标识为 @{{creator}},与插件声明的作者 {{author}} 不一致。请修改插件作者名或切换对应创作者账号。',
|
||||
createCreatorButton: '开通创作者身份',
|
||||
manageCreatorButton: '查看创作者设置',
|
||||
pendingReq: '待完成',
|
||||
autoProceedTip: '',
|
||||
refreshStatus: '刷新状态',
|
||||
reviewGitSync: 'GitHub 代码同步',
|
||||
reviewGitSyncEnabled: '已启用(推送至 {{branch}})',
|
||||
reviewGitSyncDisabled: '未启用',
|
||||
reviewChangelog: '更新说明',
|
||||
reviewChangelogEmpty: '(未填写更新说明)',
|
||||
pluginIdentifier: '插件标识',
|
||||
banners: {
|
||||
guideTitle: '操作指引:',
|
||||
disabledTooltip: '请先根据上方横幅指引完成操作',
|
||||
bindButton: '关联 Space 账号',
|
||||
reauthButton: '重新关联账号',
|
||||
createCreatorButton: '前往开通创作者身份',
|
||||
manageCreatorButton: '查看创作者设置',
|
||||
viewMyPluginsButton: '前往 Space 查看我的插件',
|
||||
refreshButton: '刷新状态',
|
||||
retryButton: '重试上传',
|
||||
readyToUpload: {
|
||||
title: '发布前准备已就绪',
|
||||
description:
|
||||
'Space 账号与创作者身份均已就绪,请核对信息后点击下方按钮确认发布。',
|
||||
},
|
||||
notConnected: {
|
||||
title: '未关联 LangBot Space 账号',
|
||||
description:
|
||||
'当前本地实例尚未关联 LangBot Space 平台账号。发布插件需要使用 Space 账号验证创作者身份并托管发布。',
|
||||
guide:
|
||||
'请点击下方「关联 Space 账号」完成登录授权,关联成功后即可继续发布。',
|
||||
},
|
||||
noCreator: {
|
||||
title: '尚未开通 Space 创作者身份',
|
||||
description:
|
||||
'扩展市场要求发布者必须拥有独立的创作者身份(Creator Profile),用于在市场上署名及展示您的公开作者主页。',
|
||||
guide:
|
||||
'请点击下方「前往开通创作者身份」设置您的唯一标识(Slug)、简介和头像。开通后返回本页面点击「刷新状态」即可。',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: '插件作者与 Space 创作者标识不匹配',
|
||||
description:
|
||||
'插件清单中声明的作者为 “{{author}}”,而您当前 Space 账号的创作者标识为 “@{{creator}}”。扩展市场要求二者必须完全一致。',
|
||||
guide:
|
||||
'解决方案(任选其一):\n1. 修改本地插件工作目录下的 manifest.yaml,将 metadata.author 改为 “{{creator}}”;\n2. 或切换登录至与 “{{author}}” 对应的 Space 创作者账号。修改后点击「刷新状态」。',
|
||||
},
|
||||
pendingReview: {
|
||||
title: '该插件已有正在等待审核的版本',
|
||||
description:
|
||||
'扩展市场中已存在该插件提交的新版本,当前正处于审核流程中。为了确保发布流程有序,需等待上一版本审核完成后方可提交新版本。',
|
||||
guide:
|
||||
'请点击下方「前往 Space 查看我的插件」查看当前的审核进度及审核意见。',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'LangBot 账号登录授权已过期',
|
||||
description:
|
||||
'本地保存的 Space 访问令牌已失效或已在云端被撤销,无法完成本次发布鉴权。',
|
||||
guide:
|
||||
'请点击下方「重新关联账号」完成重新授权登录,然后再次尝试发布。',
|
||||
},
|
||||
validationFailed: {
|
||||
title: '插件包规范校验未通过',
|
||||
description: '插件包解析或规范预检失败:{{detail}}',
|
||||
guide:
|
||||
'请检查插件清单(manifest.yaml)的字段格式、版本号语义规范(SemVer)以及所引用的图标和 README 文件是否完整。修复后重试。',
|
||||
},
|
||||
genericError: {
|
||||
title: '插件发布失败',
|
||||
description: '{{detail}}',
|
||||
guide:
|
||||
'请检查网络连接或稍后重试。若问题持续出现,请前往 Space 社区寻求帮助。',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile: '请先在 Space 开通创作者身份后再上传插件',
|
||||
authorMismatch:
|
||||
'插件作者名 “{{author}}” 与 Space 创作者标识 “@{{slug}}” 不一致',
|
||||
pendingReview:
|
||||
'该插件已有正在等待审核的提交,请等待审核完成后再发布新版本',
|
||||
sessionExpired: 'LangBot 账号登录状态已过期,请重新关联登录',
|
||||
notConnected: '当前工作区尚未关联 LangBot Space 账号,请先完成关联',
|
||||
packageFailed: '插件打包失败,请检查插件代码与目录结构',
|
||||
inspectFailed: '插件清单检验失败:{{detail}}',
|
||||
certPrecheckFailed: '插件认证预检失败:{{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: '预览本地插件包',
|
||||
unpacking: '正在解包预览...',
|
||||
|
||||
@@ -1695,6 +1695,183 @@ const zhHant = {
|
||||
uploadLocal: '本地上傳',
|
||||
debugging: '調試中',
|
||||
uploadLocalPlugin: '上傳本地插件',
|
||||
spaceUpload: {
|
||||
menuAction: '上傳到外掛市場',
|
||||
title: '上傳插件到外掛市場',
|
||||
description: '建置正在偵錯的插件 {{author}}/{{name}} 並發佈到外掛市場。',
|
||||
loadFailed: '載入插件上傳資訊失敗',
|
||||
spaceNotConnected: '目前工作區未綁定 LangBot 帳號,請先綁定後再上傳。',
|
||||
label: '名稱',
|
||||
version: '版本',
|
||||
pluginDescription: '描述',
|
||||
repository: '程式碼倉庫',
|
||||
repositoryPublicHint:
|
||||
'同步到 GitHub 時,倉庫不存在會自動建立,已存在的私有倉庫也會自動改為公開。',
|
||||
author: '作者',
|
||||
authorHint: '發佈到外掛市場時將使用此作者身分。',
|
||||
license: '開源授權條款',
|
||||
licensePlaceholder: '選擇開源授權條款',
|
||||
licenseNone: '未指定',
|
||||
changelog: '更新說明',
|
||||
syncGithub: '同步到 GitHub',
|
||||
syncGithubHint:
|
||||
'提交並推送插件工作目錄。預設沿用本地 git 設定;也可在下方填寫倉庫位址與 token 覆寫。填寫 token 且 GitHub 倉庫尚不存在時,會自動建立該倉庫。',
|
||||
repoUrl: '倉庫位址(可選)',
|
||||
repoUrlHint: '留空則沿用本地 git 的 origin。',
|
||||
token: '存取權杖(可選)',
|
||||
tokenPlaceholder: '僅用於本次推送',
|
||||
branch: '分支',
|
||||
commitMessage: '提交訊息',
|
||||
syncOnly: '僅同步到 GitHub',
|
||||
gitSynced: '已同步到 GitHub',
|
||||
gitCommittedNoRemote: '已在本機提交,但未設定可推送的遠端倉庫',
|
||||
gitNothing: '沒有需要同步的變更',
|
||||
gitFailed: 'Git 同步失敗:',
|
||||
uploadSuccess: '插件已上傳到外掛市場',
|
||||
reviewOnSpace: '前往外掛市場提交審核',
|
||||
uploadFailed: '上傳插件失敗:',
|
||||
basicInfo: '基本資訊',
|
||||
changeIcon: '更換圖示',
|
||||
iconInvalidType: '請選擇圖片檔案',
|
||||
iconTooLarge: '圖示需小於 10 MB',
|
||||
labelRequired: '至少填寫一種語言的名稱',
|
||||
langEn: 'English',
|
||||
langZhHans: '簡體中文',
|
||||
langZhHant: '繁體中文',
|
||||
langJa: '日本語',
|
||||
langTh: '泰文',
|
||||
langVi: '越南文',
|
||||
langEs: '西班牙文',
|
||||
uploadButton: '上傳到外掛市場',
|
||||
notDebugHint: '僅支援上傳處於偵錯中的插件,請先以 `lbp run` 啟動偵錯。',
|
||||
stepInfo: '外掛程式資訊',
|
||||
stepSettings: '更新說明',
|
||||
stepConfirm: '確認上傳',
|
||||
manifestNotice:
|
||||
'以下資訊已從外掛程式清單(manifest.yaml)讀取,僅供確認。如需修改,請直接修改外掛程式設定檔後重新偵錯。',
|
||||
manifestSource: '清單已確認',
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
nextToSettings: '下一步:更新說明',
|
||||
nextToConfirm: '下一步:確認上傳',
|
||||
confirmAndUpload: '確認上傳到擴充市場',
|
||||
noDescription: '暫無描述',
|
||||
noRepository: '未指定代碼倉庫',
|
||||
noLicense: '未指定開源協議',
|
||||
changelogPlaceholder:
|
||||
'請填寫本次發布的更新說明(例如新增功能、修復的問題等)',
|
||||
changelogHint: '更新說明將展示在擴充市場的外掛程式版本歷史中。',
|
||||
reviewSummaryTitle: '發布資訊核對',
|
||||
reviewTargetMarket: '發布目標',
|
||||
reviewTargetMarketValue: '擴充市場 (LangBot Space)',
|
||||
reviewAccountStatus: '帳號綁定',
|
||||
reviewAccountConnected: '已綁定 LangBot 帳號',
|
||||
reviewAccountNotConnected: '未綁定帳號(上傳將失敗)',
|
||||
spaceNotConnectedTitle: '未綁定擴充市場帳號',
|
||||
bindSpaceButton: '關聯帳號',
|
||||
bindRequiredToProceed: '請先關聯帳號後再進入下一步',
|
||||
prerequisitesTitle: '發布前準備',
|
||||
prerequisitesDescription:
|
||||
'在上傳外掛至擴充市場前,需確認完成帳號關聯與創作者身分設定。',
|
||||
req1Title: '關聯 LangBot Space 帳號',
|
||||
req1Desc: '將目前本地實例關聯至 LangBot Space 雲端平台。',
|
||||
reqBound: '已關聯',
|
||||
reqCompleted: '已就緒',
|
||||
req2Title: '設定 Space 創作者身分',
|
||||
req2Desc:
|
||||
'在 Space 開通創作者身分,且創作者標識(Slug)需與外掛作者名稱一致。',
|
||||
req2WaitSpace: '需先完成 Space 帳號關聯',
|
||||
req2NoCreator: '目前 Space 帳號尚未開通創作者身分',
|
||||
req2Mismatch: '創作者標識不相符',
|
||||
req2MismatchDetail:
|
||||
'目前 Space 創作者標識為 @{{creator}},與外掛聲明的作者 {{author}} 不一致。請修改外掛作者名稱或切換對應創作者帳號。',
|
||||
createCreatorButton: '開通創作者身分',
|
||||
manageCreatorButton: '查看創作者設定',
|
||||
pendingReq: '待完成',
|
||||
autoProceedTip: '',
|
||||
refreshStatus: '重新整理狀態',
|
||||
reviewGitSync: 'GitHub 代碼同步',
|
||||
reviewGitSyncEnabled: '已啟用(推送至 {{branch}})',
|
||||
reviewGitSyncDisabled: '未啟用',
|
||||
reviewChangelog: '更新說明',
|
||||
reviewChangelogEmpty: '(未填寫更新說明)',
|
||||
pluginIdentifier: '外掛程式標識',
|
||||
banners: {
|
||||
guideTitle: '操作指引:',
|
||||
disabledTooltip: '請先根據上方橫幅指引完成操作',
|
||||
bindButton: '關聯 Space 帳號',
|
||||
reauthButton: '重新關聯帳號',
|
||||
createCreatorButton: '前往開通創作者身份',
|
||||
manageCreatorButton: '查看創作者設定',
|
||||
viewMyPluginsButton: '前往 Space 查看我的外掛程式',
|
||||
refreshButton: '重新整理狀態',
|
||||
retryButton: '重試上傳',
|
||||
readyToUpload: {
|
||||
title: '發布前準備已就緒',
|
||||
description:
|
||||
'Space 帳號與創作者身份均已就緒,請核對資訊後點選下方按鈕確認發布。',
|
||||
},
|
||||
notConnected: {
|
||||
title: '未關聯 LangBot Space 帳號',
|
||||
description:
|
||||
'當前本地執行個體尚未關聯 LangBot Space 平台帳號。發布外掛程式需要使用 Space 帳號驗證創作者身份並代管發布。',
|
||||
guide:
|
||||
'請點選下方「關聯 Space 帳號」完成登入授權,關聯成功後即可繼續發布。',
|
||||
},
|
||||
noCreator: {
|
||||
title: '尚未開通 Space 創作者身份',
|
||||
description:
|
||||
'擴充市場要求發布者必須擁有獨立的創作者身份(Creator Profile),用於在市場上署名及展示您的公開作者首頁。',
|
||||
guide:
|
||||
'請點選下方「前往開通創作者身份」設定您的唯一標識(Slug)、簡介和頭像。開通後返回本頁面點選「重新整理狀態」即可。',
|
||||
},
|
||||
authorMismatch: {
|
||||
title: '外掛程式作者與 Space 創作者標識不相符',
|
||||
description:
|
||||
'外掛程式清單中宣告的作者為「{{author}}」,而您當前 Space 帳號的創作者標識為「@{{creator}}」。擴充市場要求兩者必須完全一致。',
|
||||
guide:
|
||||
'解決方案(任選其一):\n1. 修改本地外掛程式工作目錄下的 manifest.yaml,將 metadata.author 改為「{{creator}}」;\n2. 或切換登入至與「{{author}}」對應的 Space 創作者帳號。修改後點選「重新整理狀態」。',
|
||||
},
|
||||
pendingReview: {
|
||||
title: '該外掛程式已有正在等待審核的版本',
|
||||
description:
|
||||
'擴充市場中已存在該外掛程式提交的新版本,當前正處於審核流程中。為了確保發布流程有序,需等待上一版本審核完成後方可提交新版本。',
|
||||
guide:
|
||||
'請點選下方「前往 Space 查看我的外掛程式」查看當前的審核進度及審核意見。',
|
||||
},
|
||||
sessionExpired: {
|
||||
title: 'Space 登入授權已過期',
|
||||
description:
|
||||
'本地儲存的 Space 存取權杖已失效或已在雲端被撤銷,無法完成本次發布鑑權。',
|
||||
guide:
|
||||
'請點選下方「重新關聯帳號」完成重新授權登入,然後再次嘗試發布。',
|
||||
},
|
||||
validationFailed: {
|
||||
title: '外掛程式套件規範校驗未通過',
|
||||
description: '外掛程式套件解析或規範預檢失敗:{{detail}}',
|
||||
guide:
|
||||
'請檢查外掛程式清單(manifest.yaml)的欄位格式、版本號語意規範(SemVer)以及所引用的圖示和 README 檔案是否完整。修復後重試。',
|
||||
},
|
||||
genericError: {
|
||||
title: '外掛程式發布失敗',
|
||||
description: '{{detail}}',
|
||||
guide:
|
||||
'請檢查網路連線或稍後重試。若問題持續出現,請前往 Space 社群尋求協助。',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noCreatorProfile: '請先在 Space 開通創作者身份後再上傳外掛程式',
|
||||
authorMismatch:
|
||||
'外掛程式作者名「{{author}}」與 Space 創作者標識「@{{slug}}」不一致',
|
||||
pendingReview:
|
||||
'該外掛程式已有正在等待審核的提交,請等待審核完成後再發布新版本',
|
||||
sessionExpired: 'Space 登入狀態已過期,請重新關聯登入',
|
||||
notConnected: '當前工作區尚未關聯 LangBot Space 帳號,請先完成關聯',
|
||||
packageFailed: '外掛程式打包失敗,請檢查外掛程式代碼與目錄結構',
|
||||
inspectFailed: '外掛程式清單檢驗失敗:{{detail}}',
|
||||
certPrecheckFailed: '外掛程式認證預檢失敗:{{detail}}',
|
||||
},
|
||||
},
|
||||
localPreview: {
|
||||
title: '預覽本地外掛包',
|
||||
unpacking: '正在解包預覽...',
|
||||
|
||||
Reference in New Issue
Block a user