From 3f59bfac5cfe298ba2608fb551a434abcbcbee85 Mon Sep 17 00:00:00 2001 From: Bruce Date: Mon, 22 Sep 2025 22:49:22 +0800 Subject: [PATCH] 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 --- pkg/api/http/controller/groups/system.py | 23 +++++ pkg/plugin/connector.py | 18 +++- templates/config.yaml | 1 + web/src/app/home/plugins/page.tsx | 111 +++++++++++++++++++++-- web/src/app/infra/entities/api/index.ts | 6 ++ web/src/app/infra/http/BackendClient.ts | 5 + web/src/i18n/locales/en-US.ts | 11 +++ web/src/i18n/locales/ja-JP.ts | 11 +++ web/src/i18n/locales/zh-Hans.ts | 8 ++ web/src/i18n/locales/zh-Hant.ts | 8 ++ 10 files changed, 194 insertions(+), 8 deletions(-) diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index 94cf4f6e..1fe04136 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -91,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, + } + ) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 4d441583..52c809ef 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -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(): @@ -103,6 +111,12 @@ 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, @@ -175,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']) @@ -205,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() diff --git a/templates/config.yaml b/templates/config.yaml index 00de2a9b..13916d9f 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -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' \ No newline at end of file diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index fb751c4c..bac823cb 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -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(null); const [githubURL, setGithubURL] = useState(''); const [isDragOver, setIsDragOver] = useState(false); + const [pluginSystemStatus, setPluginSystemStatus] = + useState(null); + const [statusLoading, setStatusLoading] = useState(true); const pluginInstalledRef = useRef(null); const fileInputRef = useRef(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 = () => ( +
+ +

+ {t('plugins.systemDisabled')} +

+

+ {t('plugins.systemDisabledDesc')} +

+
+ ); + + // 插件系统连接异常的状态显示 + const renderPluginConnectionErrorState = () => ( +
+ + + + +

+ {t('plugins.connectionError')} +

+

+ {t('plugins.connectionErrorDesc')} +

+
+ ); + + // 加载状态显示 + const renderLoadingState = () => ( +
+

+ {t('plugins.loadingStatus')} +

+
+ ); + + // 根据状态返回不同的内容 + if (statusLoading) { + return renderLoadingState(); + } + + if (!pluginSystemStatus?.is_enable) { + return renderPluginDisabledState(); + } + + if (!pluginSystemStatus?.is_connected) { + return renderPluginConnectionErrorState(); + } + return (
{ + return this.get('/api/v1/system/status/plugin-system'); + } + // ============ User API ============ public checkIfInited(): Promise<{ initialized: boolean }> { return this.get('/api/v1/user/init'); diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 32a86763..7196a63a 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -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', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 36b17f00..128020c6 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -183,6 +183,17 @@ const jaJP = { pluginSortSuccess: 'プラグインの並び替えに成功しました', pluginSortError: 'プラグインの並び替えに失敗しました:', pluginNoConfig: 'プラグインに設定項目がありません。', + systemDisabled: 'プラグインシステムが無効になっています', + systemDisabledDesc: + 'プラグインシステムが無効になっています。プラグインシステムを有効にするか、ドキュメントに従って設定を変更してください', + connectionError: 'プラグインシステム接続エラー', + connectionErrorDesc: + 'プラグインシステム設定を確認するか、管理者に連絡してください', + errorDetails: 'エラー詳細', + loadingStatus: 'プラグインシステム状態を確認中...', + failedToGetStatus: 'プラグインシステム状態の取得に失敗しました', + pluginSystemNotReady: + 'プラグインシステムが準備されていません。この操作を実行できません', deleting: '削除中...', deletePlugin: 'プラグインを削除', cancel: 'キャンセル', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index f891dee9..6b26b767 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -178,6 +178,14 @@ const zhHans = { pluginSortSuccess: '插件排序成功', pluginSortError: '插件排序失败:', pluginNoConfig: '插件没有配置项。', + systemDisabled: '插件系统未启用', + systemDisabledDesc: '尚未启用插件系统,请根据文档修改配置', + connectionError: '插件系统连接异常', + connectionErrorDesc: '请检查插件系统配置或联系管理员', + errorDetails: '错误详情', + loadingStatus: '正在检查插件系统状态...', + failedToGetStatus: '获取插件系统状态失败', + pluginSystemNotReady: '插件系统未就绪,无法执行此操作', deleting: '删除中...', deletePlugin: '删除插件', cancel: '取消', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 5f9ce702..89b3d57a 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -178,6 +178,14 @@ const zhHant = { pluginSortSuccess: '外掛排序成功', pluginSortError: '外掛排序失敗:', pluginNoConfig: '外掛沒有設定項目。', + systemDisabled: '外掛系統未啟用', + systemDisabledDesc: '尚未啟用外掛系統,請根據文檔修改配置', + connectionError: '外掛系統連接異常', + connectionErrorDesc: '請檢查外掛系統配置或聯絡管理員', + errorDetails: '錯誤詳情', + loadingStatus: '正在檢查外掛系統狀態...', + failedToGetStatus: '取得外掛系統狀態失敗', + pluginSystemNotReady: '外掛系統未就緒,無法執行此操作', deleting: '刪除中...', deletePlugin: '刪除外掛', cancel: '取消',