Compare commits

...

16 Commits

Author SHA1 Message Date
Junyan Qin
2891708060 chore: bump version v4.3.3 2025-09-22 22:53:10 +08:00
Bruce
3f59bfac5c feat: add plugin enable config (#1678)
* add plugin enable config

* fix logic error

* improve codes

* feat: add plugin system status checking api

* perf: add ui displaying plugin system status

* chore: fix linter errors

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-09-22 22:49:22 +08:00
Junyan Qin
ee24582dd3 fix: bad At construction in respback (#1676) 2025-09-22 10:59:10 +08:00
Junyan Qin
0ffb4d5792 perf: use file transfer in getting icon and installing local plugins (#1674) 2025-09-19 19:38:27 +08:00
Junyan Qin
5a6206f148 doc: update docker command in READMEs 2025-09-19 16:39:13 +08:00
Junyan Qin
b1014313d6 fix: telegram event converter bug 2025-09-18 00:44:30 +08:00
Junyan Qin
fcc2f6a195 fix: bad message chain init in command 2025-09-17 17:12:39 +08:00
Junyan Qin (Chin)
c8ffc79077 perf: disable long message processing as default (#1670) 2025-09-17 17:09:12 +08:00
Junyan Qin
1a13a41168 bump version in pyproject.toml 2025-09-17 14:10:41 +08:00
Junyan Qin
bf279049c0 chore: bump version 4.3.2 2025-09-17 13:57:45 +08:00
Junyan Qin
05cc58f2d7 fix: bad plugin runtime ws url in migration 2025-09-17 13:55:59 +08:00
Junyan Qin
d887881ea0 chore: bump version 4.3.1 2025-09-17 09:52:07 +08:00
Junyan Qin
8bb2f3e745 fix: migration bug of plugin config 2025-09-16 17:04:44 +08:00
Junyan Qin
e7e6eeda61 feat: remove legacy plugin deps checking 2025-09-16 15:11:10 +08:00
Junyan Qin
b6ff2be4df chore: remove docker-compose.yaml in root dir 2025-09-16 15:00:43 +08:00
Junyan Qin
a2ea185602 chore: bump langbot_plugin to 0.1.1 2025-09-16 12:36:39 +08:00
28 changed files with 259 additions and 55 deletions

View File

@@ -35,7 +35,7 @@ LangBot 是一个开源的大语言模型原生即时通信机器人开发平台
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot
cd LangBot/docker
docker compose up -d
```

View File

@@ -29,7 +29,7 @@ LangBot is an open-source LLM native instant messaging robot development platfor
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot
cd LangBot/docker
docker compose up -d
```

View File

@@ -29,7 +29,7 @@ LangBot は、エージェント、RAG、MCP などの LLM アプリケーショ
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot
cd LangBot/docker
docker compose up -d
```

View File

@@ -31,7 +31,7 @@ LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot
cd LangBot/docker
docker compose up -d
```

View File

@@ -1,17 +0,0 @@
# This file is deprecated, and will be replaced by docker/docker-compose.yaml in next version.
version: "3"
services:
langbot:
image: rockchin/langbot:latest
container_name: langbot
volumes:
- ./data:/app/data
- ./plugins:/app/plugins
restart: on-failure
environment:
- TZ=Asia/Shanghai
ports:
- 5300:5300 # 供 WebUI 使用
- 2280-2290:2280-2290 # 供消息平台适配器方向连接
# 根据具体环境配置网络

View File

@@ -18,7 +18,6 @@ asciiart = r"""
async def main_entry(loop: asyncio.AbstractEventLoop):
parser = argparse.ArgumentParser(description='LangBot')
parser.add_argument('--skip-plugin-deps-check', action='store_true', help='跳过插件依赖项检查', default=False)
parser.add_argument('--standalone-runtime', action='store_true', help='使用独立插件运行时', default=False)
args = parser.parse_args()
@@ -49,10 +48,6 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
print('The missing dependencies have been installed automatically, please restart the program.')
sys.exit(0)
# check plugin deps
if not args.skip_plugin_deps_check:
await deps.precheck_plugin_deps()
# # 检查pydantic版本如果没有 pydantic.v1则把 pydantic 映射为 v1
# import pydantic.version

View File

@@ -128,10 +128,8 @@ class PluginsRouterGroup(group.RouterGroup):
file_bytes = file.read()
file_base64 = base64.b64encode(file_bytes).decode('utf-8')
data = {
'plugin_file': file_base64,
'plugin_file': file_bytes,
}
ctx = taskmgr.TaskContext.new()

View File

@@ -14,10 +14,14 @@ class SystemRouterGroup(group.RouterGroup):
'version': constants.semantic_version,
'debug': constants.debug_mode,
'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()),
'enable_marketplace': self.ap.instance_config.data['plugin'].get('enable_marketplace', True),
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
'enable_marketplace', True
),
'cloud_service_url': (
self.ap.instance_config.data['plugin']['cloud_service_url']
if 'cloud_service_url' in self.ap.instance_config.data['plugin']
self.ap.instance_config.data.get('plugin', {}).get(
'cloud_service_url', 'https://space.langbot.app'
)
if 'cloud_service_url' in self.ap.instance_config.data.get('plugin', {})
else 'https://space.langbot.app'
),
}
@@ -87,3 +91,26 @@ class SystemRouterGroup(group.RouterGroup):
)
return self.success(data=resp)
@self.route(
'/status/plugin-system',
methods=['GET'],
auth_type=group.AuthType.USER_TOKEN,
)
async def _() -> str:
plugin_connector_error = 'ok'
is_connected = True
try:
await self.ap.plugin_connector.ping_plugin_runtime()
except Exception as e:
plugin_connector_error = str(e)
is_connected = False
return self.success(
data={
'is_enable': self.ap.plugin_connector.is_enable_plugin,
'is_connected': is_connected,
'plugin_connector_error': plugin_connector_error,
}
)

View File

@@ -1,7 +1,7 @@
from .. import migration
@migration.migration_class(4)
@migration.migration_class(8)
class DBMigratePluginConfig(migration.DBMigration):
"""插件配置"""
@@ -10,7 +10,9 @@ class DBMigratePluginConfig(migration.DBMigration):
if 'plugin' not in self.ap.instance_config.data:
self.ap.instance_config.data['plugin'] = {
'runtime_ws_url': 'ws://localhost:5400/control/ws',
'runtime_ws_url': 'ws://langbot_plugin_runtime:5400/control/ws',
'enable_marketplace': True,
'cloud_service_url': 'https://space.langbot.app',
}
await self.ap.instance_config.dump_config()

View File

@@ -21,10 +21,15 @@ class LongTextProcessStage(stage.PipelineStage):
- resp_message_chain
"""
strategy_impl: strategy.LongTextStrategy
strategy_impl: strategy.LongTextStrategy | None
async def initialize(self, pipeline_config: dict):
config = pipeline_config['output']['long-text-processing']
if config['strategy'] == 'none':
self.strategy_impl = None
return
if config['strategy'] == 'image':
use_font = config['font-path']
try:
@@ -67,6 +72,10 @@ class LongTextProcessStage(stage.PipelineStage):
await self.strategy_impl.initialize()
async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
if self.strategy_impl is None:
self.ap.logger.debug('Long message processing strategy is not set, skip long message processing.')
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
# 检查是否包含非 Plain 组件
contains_non_plain = False

View File

@@ -26,7 +26,7 @@ class ForwardComponentStrategy(strategy_model.LongTextStrategy):
platform_message.ForwardMessageNode(
sender_id=query.adapter.bot_account_id,
sender_name='User',
message_chain=platform_message.MessageChain([message]),
message_chain=platform_message.MessageChain([platform_message.Plain(text=message)]),
)
]

View File

@@ -60,7 +60,9 @@ class CommandHandler(handler.MessageHandler):
else:
if event_ctx.event.alter is not None:
query.message_chain = platform_message.MessageChain([platform_message.Plain(event_ctx.event.alter)])
query.message_chain = platform_message.MessageChain(
[platform_message.Plain(text=event_ctx.event.alter)]
)
session = await self.ap.sess_mgr.get_session(query)

View File

@@ -33,7 +33,7 @@ class SendResponseBackStage(stage.PipelineStage):
if query.pipeline_config['output']['misc']['at-sender'] and isinstance(
query.message_event, platform_events.GroupMessage
):
query.resp_message_chain[-1].insert(0, platform_message.At(query.message_event.sender.id))
query.resp_message_chain[-1].insert(0, platform_message.At(target=query.message_event.sender.id))
quote_origin = query.pipeline_config['output']['misc']['quote-origin']

View File

@@ -102,7 +102,7 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
sender=platform_entities.Friend(
id=event.effective_chat.id,
nickname=event.effective_chat.first_name,
remark=event.effective_chat.id,
remark=str(event.effective_chat.id),
),
message_chain=lb_message,
time=event.message.date.timestamp(),

View File

@@ -40,6 +40,9 @@ class PluginRuntimeConnector:
[PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None]
]
is_enable_plugin: bool = True
"""Mark if the plugin system is enabled"""
def __init__(
self,
ap: app.Application,
@@ -49,8 +52,13 @@ class PluginRuntimeConnector:
):
self.ap = ap
self.runtime_disconnect_callback = runtime_disconnect_callback
self.is_enable_plugin = self.ap.instance_config.data.get('plugin', {}).get('enable', True)
async def initialize(self):
if not self.is_enable_plugin:
self.ap.logger.info('Plugin system is disabled.')
return
async def new_connection_callback(connection: base_connection.Connection):
async def disconnect_callback(rchandler: handler.RuntimeConnectionHandler) -> bool:
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime():
@@ -73,7 +81,9 @@ class PluginRuntimeConnector:
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime(): # use websocket
self.ap.logger.info('use websocket to connect to plugin runtime')
ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url']
ws_url = self.ap.instance_config.data.get('plugin', {}).get(
'runtime_ws_url', 'ws://langbot_plugin_runtime:5400/control/ws'
)
async def make_connection_failed_callback(ctrl: ws_client_controller.WebSocketClientController) -> None:
self.ap.logger.error('Failed to connect to plugin runtime, trying to reconnect...')
@@ -101,12 +111,26 @@ class PluginRuntimeConnector:
async def initialize_plugins(self):
pass
async def ping_plugin_runtime(self):
if not hasattr(self, 'handler'):
raise Exception('Plugin runtime is not connected')
return await self.handler.ping()
async def install_plugin(
self,
install_source: PluginInstallSource,
install_info: dict[str, Any],
task_context: taskmgr.TaskContext | None = None,
):
if install_source == PluginInstallSource.LOCAL:
# transfer file before install
file_bytes = install_info['plugin_file']
file_key = await self.handler.send_file(file_bytes, 'lbpkg')
install_info['plugin_file_key'] = file_key
del install_info['plugin_file']
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
async for ret in self.handler.install_plugin(install_source.value, install_info):
current_action = ret.get('current_action', None)
if current_action is not None:
@@ -165,6 +189,8 @@ class PluginRuntimeConnector:
) -> context.EventContext:
event_ctx = context.EventContext.from_event(event)
if not self.is_enable_plugin:
return event_ctx
event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=True))
event_ctx = context.EventContext.model_validate(event_ctx_result['event_context'])
@@ -195,6 +221,6 @@ class PluginRuntimeConnector:
yield cmd_ret
def dispose(self):
if isinstance(self.ctrl, stdio_client_controller.StdioClientController):
if self.is_enable_plugin and isinstance(self.ctrl, stdio_client_controller.StdioClientController):
self.ap.logger.info('Terminating plugin runtime process...')
self.ctrl.process.terminate()

View File

@@ -560,7 +560,18 @@ class RuntimeConnectionHandler(handler.Handler):
'plugin_name': plugin_name,
},
)
return result
plugin_icon_file_key = result['plugin_icon_file_key']
mime_type = result['mime_type']
plugin_icon_bytes = await self.read_local_file(plugin_icon_file_key)
await self.delete_local_file(plugin_icon_file_key)
return {
'plugin_icon_base64': base64.b64encode(plugin_icon_bytes).decode('utf-8'),
'mime_type': mime_type,
}
async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]:
"""Call tool"""

View File

@@ -1,6 +1,6 @@
semantic_version = 'v4.3.0'
semantic_version = 'v4.3.3'
required_database_version = 7
required_database_version = 8
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
debug_mode = False

View File

@@ -1,6 +1,6 @@
[project]
name = "langbot"
version = "4.3.0"
version = "4.3.3"
description = "Easy-to-use global IM bot platform designed for LLM era"
readme = "README.md"
requires-python = ">=3.10.1,<4.0"
@@ -62,7 +62,7 @@ dependencies = [
"langchain>=0.2.0",
"chromadb>=0.4.24",
"qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.1.1b8",
"langbot-plugin==0.1.2",
"asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0"
]

View File

@@ -38,6 +38,7 @@ vdb:
port: 6333
api_key: ''
plugin:
enable: true
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
enable_marketplace: true
cloud_service_url: 'https://space.langbot.app'

View File

@@ -83,7 +83,7 @@
"output": {
"long-text-processing": {
"threshold": 1000,
"strategy": "forward",
"strategy": "none",
"font-path": ""
},
"force-delay": {

View File

@@ -27,7 +27,7 @@ stages:
zh_Hans: 长文本的处理策略
type: select
required: true
default: forward
default: none
options:
- name: forward
label:
@@ -37,6 +37,10 @@ stages:
label:
en_US: Convert to Image
zh_Hans: 转换为图片
- name: none
label:
en_US: None
zh_Hans: 不处理
- name: font-path
label:
en_US: Font Path

View File

@@ -13,6 +13,7 @@ import {
UploadIcon,
StoreIcon,
Download,
Power,
} from 'lucide-react';
import {
DropdownMenu,
@@ -28,12 +29,13 @@ import {
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { useState, useRef, useCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { PluginV4 } from '@/app/infra/entities/plugin';
import { systemInfo } from '@/app/infra/http/HttpClient';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
enum PluginInstallStatus {
WAIT_INPUT = 'wait_input',
@@ -54,9 +56,29 @@ export default function PluginConfigPage() {
const [installError, setInstallError] = useState<string | null>(null);
const [githubURL, setGithubURL] = useState('');
const [isDragOver, setIsDragOver] = useState(false);
const [pluginSystemStatus, setPluginSystemStatus] =
useState<ApiRespPluginSystemStatus | null>(null);
const [statusLoading, setStatusLoading] = useState(true);
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const fetchPluginSystemStatus = async () => {
try {
setStatusLoading(true);
const status = await httpClient.getPluginSystemStatus();
setPluginSystemStatus(status);
} catch (error) {
console.error('Failed to fetch plugin system status:', error);
toast.error(t('plugins.failedToGetStatus'));
} finally {
setStatusLoading(false);
}
};
fetchPluginSystemStatus();
}, [t]);
function watchTask(taskId: number) {
let alreadySuccess = false;
console.log('taskId:', taskId);
@@ -140,6 +162,11 @@ export default function PluginConfigPage() {
const uploadPluginFile = useCallback(
async (file: File) => {
if (!pluginSystemStatus?.is_enable || !pluginSystemStatus?.is_connected) {
toast.error(t('plugins.pluginSystemNotReady'));
return;
}
if (!validateFileType(file)) {
toast.error(t('plugins.unsupportedFileType'));
return;
@@ -150,7 +177,7 @@ export default function PluginConfigPage() {
setInstallError(null);
installPlugin('local', { file });
},
[t],
[t, pluginSystemStatus],
);
const handleFileSelect = useCallback(() => {
@@ -171,10 +198,18 @@ export default function PluginConfigPage() {
[uploadPluginFile],
);
const handleDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
setIsDragOver(true);
}, []);
const isPluginSystemReady =
pluginSystemStatus?.is_enable && pluginSystemStatus?.is_connected;
const handleDragOver = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
if (isPluginSystemReady) {
setIsDragOver(true);
}
},
[isPluginSystemReady],
);
const handleDragLeave = useCallback((event: React.DragEvent) => {
event.preventDefault();
@@ -186,14 +221,76 @@ export default function PluginConfigPage() {
event.preventDefault();
setIsDragOver(false);
if (!isPluginSystemReady) {
toast.error(t('plugins.pluginSystemNotReady'));
return;
}
const files = Array.from(event.dataTransfer.files);
if (files.length > 0) {
uploadPluginFile(files[0]);
}
},
[uploadPluginFile],
[uploadPluginFile, isPluginSystemReady, t],
);
// 插件系统未启用的状态显示
const renderPluginDisabledState = () => (
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
<Power className="w-16 h-16 text-gray-400 mb-4" />
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t('plugins.systemDisabled')}
</h2>
<p className="text-gray-500 dark:text-gray-400 max-w-md">
{t('plugins.systemDisabledDesc')}
</p>
</div>
);
// 插件系统连接异常的状态显示
const renderPluginConnectionErrorState = () => (
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="72"
height="72"
fill="#BDBDBD"
>
<path d="M17.657 14.8284L16.2428 13.4142L17.657 12C19.2191 10.4379 19.2191 7.90526 17.657 6.34316C16.0949 4.78106 13.5622 4.78106 12.0001 6.34316L10.5859 7.75737L9.17171 6.34316L10.5859 4.92895C12.9291 2.5858 16.7281 2.5858 19.0712 4.92895C21.4143 7.27209 21.4143 11.0711 19.0712 13.4142L17.657 14.8284ZM14.8286 17.6569L13.4143 19.0711C11.0712 21.4142 7.27221 21.4142 4.92907 19.0711C2.58592 16.7279 2.58592 12.9289 4.92907 10.5858L6.34328 9.17159L7.75749 10.5858L6.34328 12C4.78118 13.5621 4.78118 16.0948 6.34328 17.6569C7.90538 19.219 10.438 19.219 12.0001 17.6569L13.4143 16.2427L14.8286 17.6569ZM14.8286 7.75737L16.2428 9.17159L9.17171 16.2427L7.75749 14.8284L14.8286 7.75737ZM5.77539 2.29291L7.70724 1.77527L8.74252 5.63897L6.81067 6.15661L5.77539 2.29291ZM15.2578 18.3611L17.1896 17.8434L18.2249 21.7071L16.293 22.2248L15.2578 18.3611ZM2.29303 5.77527L6.15673 6.81054L5.63909 8.7424L1.77539 7.70712L2.29303 5.77527ZM18.3612 15.2576L22.2249 16.2929L21.7072 18.2248L17.8435 17.1895L18.3612 15.2576Z"></path>
</svg>
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t('plugins.connectionError')}
</h2>
<p className="text-gray-500 dark:text-gray-400 max-w-md mb-4">
{t('plugins.connectionErrorDesc')}
</p>
</div>
);
// 加载状态显示
const renderLoadingState = () => (
<div className="flex flex-col items-center justify-center h-[60vh] pt-[10vh]">
<p className="text-gray-500 dark:text-gray-400">
{t('plugins.loadingStatus')}
</p>
</div>
);
// 根据状态返回不同的内容
if (statusLoading) {
return renderLoadingState();
}
if (!pluginSystemStatus?.is_enable) {
return renderPluginDisabledState();
}
if (!pluginSystemStatus?.is_connected) {
return renderPluginConnectionErrorState();
}
return (
<div
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}

View File

@@ -215,6 +215,12 @@ export interface ApiRespSystemInfo {
enable_marketplace: boolean;
}
export interface ApiRespPluginSystemStatus {
is_enable: boolean;
is_connected: boolean;
plugin_connector_error: string;
}
export interface ApiRespAsyncTasks {
tasks: AsyncTask[];
}

View File

@@ -32,6 +32,7 @@ import {
ApiRespProviderEmbeddingModels,
ApiRespProviderEmbeddingModel,
EmbeddingModel,
ApiRespPluginSystemStatus,
} from '@/app/infra/entities/api';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
@@ -500,6 +501,10 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/system/tasks/${id}`);
}
public getPluginSystemStatus(): Promise<ApiRespPluginSystemStatus> {
return this.get('/api/v1/system/status/plugin-system');
}
// ============ User API ============
public checkIfInited(): Promise<{ initialized: boolean }> {
return this.get('/api/v1/user/init');

View File

@@ -182,6 +182,17 @@ const enUS = {
pluginSortSuccess: 'Plugin sort successful',
pluginSortError: 'Plugin sort failed: ',
pluginNoConfig: 'The plugin has no configuration items.',
systemDisabled: 'Plugin System Disabled',
systemDisabledDesc:
'Plugin system is not enabled, please modify the configuration according to the documentation',
connectionError: 'Plugin System Connection Error',
connectionErrorDesc:
'Please check the plugin system configuration or contact the administrator.',
errorDetails: 'Error Details',
loadingStatus: 'Checking plugin system status...',
failedToGetStatus: 'Failed to get plugin system status',
pluginSystemNotReady:
'Plugin system is not ready, cannot perform this operation',
deleting: 'Deleting...',
deletePlugin: 'Delete Plugin',
cancel: 'Cancel',

View File

@@ -183,6 +183,17 @@ const jaJP = {
pluginSortSuccess: 'プラグインの並び替えに成功しました',
pluginSortError: 'プラグインの並び替えに失敗しました:',
pluginNoConfig: 'プラグインに設定項目がありません。',
systemDisabled: 'プラグインシステムが無効になっています',
systemDisabledDesc:
'プラグインシステムが無効になっています。プラグインシステムを有効にするか、ドキュメントに従って設定を変更してください',
connectionError: 'プラグインシステム接続エラー',
connectionErrorDesc:
'プラグインシステム設定を確認するか、管理者に連絡してください',
errorDetails: 'エラー詳細',
loadingStatus: 'プラグインシステム状態を確認中...',
failedToGetStatus: 'プラグインシステム状態の取得に失敗しました',
pluginSystemNotReady:
'プラグインシステムが準備されていません。この操作を実行できません',
deleting: '削除中...',
deletePlugin: 'プラグインを削除',
cancel: 'キャンセル',

View File

@@ -178,6 +178,14 @@ const zhHans = {
pluginSortSuccess: '插件排序成功',
pluginSortError: '插件排序失败:',
pluginNoConfig: '插件没有配置项。',
systemDisabled: '插件系统未启用',
systemDisabledDesc: '尚未启用插件系统,请根据文档修改配置',
connectionError: '插件系统连接异常',
connectionErrorDesc: '请检查插件系统配置或联系管理员',
errorDetails: '错误详情',
loadingStatus: '正在检查插件系统状态...',
failedToGetStatus: '获取插件系统状态失败',
pluginSystemNotReady: '插件系统未就绪,无法执行此操作',
deleting: '删除中...',
deletePlugin: '删除插件',
cancel: '取消',

View File

@@ -178,6 +178,14 @@ const zhHant = {
pluginSortSuccess: '外掛排序成功',
pluginSortError: '外掛排序失敗:',
pluginNoConfig: '外掛沒有設定項目。',
systemDisabled: '外掛系統未啟用',
systemDisabledDesc: '尚未啟用外掛系統,請根據文檔修改配置',
connectionError: '外掛系統連接異常',
connectionErrorDesc: '請檢查外掛系統配置或聯絡管理員',
errorDetails: '錯誤詳情',
loadingStatus: '正在檢查外掛系統狀態...',
failedToGetStatus: '取得外掛系統狀態失敗',
pluginSystemNotReady: '外掛系統未就緒,無法執行此操作',
deleting: '刪除中...',
deletePlugin: '刪除外掛',
cancel: '取消',