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:
Type_rBody
2026-10-28 18:23:09 +08:00
committed by GitHub
co-authored by multica-agent Hyu RockChinQ
parent 4687fc96e7
commit 4b537973f9
28 changed files with 3809 additions and 109 deletions
+3
View File
@@ -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
+79
View File
@@ -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')
+3
View File
@@ -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'),
+104
View File
@@ -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,
+59
View File
@@ -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(
+1
View File
@@ -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
@@ -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' && (
@@ -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',
+39 -1
View File
@@ -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');
}
+206
View File
@@ -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...',
+194
View File
@@ -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...',
+190
View File
@@ -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: 'パッケージを展開してプレビュー中...',
+188
View File
@@ -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: 'Распаковка пакета для предпросмотра...',
+187
View File
@@ -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: 'กำลังแตกแพ็กเกจเพื่อดูตัวอย่าง...',
+190
View File
@@ -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...',
+193
View File
@@ -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: '正在解包预览...',
+177
View File
@@ -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: '正在解包預覽...',