From cdf501a1adedaff72feb952a93018da8fda9f2f8 Mon Sep 17 00:00:00 2001 From: unknown <2916963017@qq.com> Date: Wed, 11 Feb 2026 21:44:02 +0800 Subject: [PATCH 1/4] add :Support hot reload after plugin load failure --- astrbot/core/star/star_manager.py | 31 ++++++++++++++++++ astrbot/dashboard/routes/plugin.py | 33 +++++++++++++++++++ dashboard/src/views/ExtensionPage.vue | 47 ++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 587808956..080f21b49 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -62,6 +62,9 @@ def __init__(self, context: Context, config: AstrBotConfig) -> None: self._pm_lock = asyncio.Lock() """StarManager操作互斥锁""" + self.failed_plugin_dict = {} + """加载失败插件的信息,用于后续可能的热重载""" + self.failed_plugin_info = "" if os.getenv("ASTRBOT_RELOAD", "0") == "1": asyncio.create_task(self._watch_plugins_changes()) @@ -296,6 +299,29 @@ def _purge_modules( except KeyError: logger.warning(f"模块 {module_name} 未载入") + async def reload_failed_plugin( self , dir_name ) : + """ + 重新加载未注册(加载失败)的插件 + Args: + dir_name (str): 要重载的特定插件名称。 + Returns: + tuple: 返回 load() 方法的结果,包含 (success, error_message) + - success (bool): 重载是否成功 + - error_message (str|None): 错误信息,成功时为 None + """ + async with self._pm_lock : + if dir_name in self.failed_plugin_dict : + success,error = await self.load( specified_dir_name=dir_name ) + if success : + self.failed_plugin_dict.pop( dir_name , None ) + if not self.failed_plugin_dict : + self.failed_plugin_info = "" + return success,None + else : + return False,error + return False,"插件不存在于失败列表中" + + async def reload(self, specified_plugin_name=None): """重新加载插件 @@ -630,6 +656,11 @@ async def load(self, specified_module_path=None, specified_dir_name=None): logger.error(f"| {line}") logger.error("----------------------------------") fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}。\n" + self.failed_plugin_dict[ root_dir_name ] = { + "error" : str(e) , + "traceback" : errors + } + #记录注册失败的插件名称,以便后续重载插件 # 清除 pip.main 导致的多余的 logging handlers for handler in logging.root.handlers[:]: diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index f9f8961b4..6911ebb45 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -53,11 +53,13 @@ def __init__( "/plugin/market_list": ("GET", self.get_online_plugins), "/plugin/off": ("POST", self.off_plugin), "/plugin/on": ("POST", self.on_plugin), + "/plugin/reload-failed": ("POST", self.reload_failed_plugins), "/plugin/reload": ("POST", self.reload_plugins), "/plugin/readme": ("GET", self.get_plugin_readme), "/plugin/changelog": ("GET", self.get_plugin_changelog), "/plugin/source/get": ("GET", self.get_custom_source), "/plugin/source/save": ("POST", self.save_custom_source), + "/plugin/source/get-failed-plugins": ("GET", self.get_failed_plugins), } self.core_lifecycle = core_lifecycle self.plugin_manager = plugin_manager @@ -74,6 +76,33 @@ def __init__( self._logo_cache = {} + async def reload_failed_plugins(self) : + if DEMO_MODE : + return( + Response() + .error("You are not permitted to do this operation in demo mode") + .__dict__ + ) + try: + data = await request.get_json() + dir_name = data.get("dir_name") # 这里拿的是目录名,不是插件名 + + if not dir_name: + return Response().error("缺少插件目录名").__dict__ + + # 调用 star_manager.py 中的函数 + # 注意:传入的是目录名 + success, err = await self.plugin_manager.reload_failed_plugin(dir_name) + + if success: + return Response().ok(None, f"插件 {dir_name} 重载成功。").__dict__ + else: + return Response().error(f"重载失败: {err}").__dict__ + + except Exception as e: + logger.error(f"/api/plugin/reload-failed: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ + async def reload_plugins(self): if DEMO_MODE: return ( @@ -332,6 +361,10 @@ async def get_plugins(self): .ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info) .__dict__ ) + + async def get_failed_plugins(self): + """专门获取加载失败的插件列表(字典格式)""" + return Response().ok(self.plugin_manager.failed_plugin_dict).__dict__ async def get_plugin_handlers_info(self, handler_full_names: list[str]): """解析插件行为""" diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index c62637e3c..ce6b19d4d 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -357,11 +357,17 @@ const onLoadingDialogResult = (statusCode, result, timeToClose = 2000) => { setTimeout(resetLoadingDialog, timeToClose); }; +const failedPluginsDict = ref({}); + const getExtensions = async () => { loading_.value = true; try { - const res = await axios.get("/api/plugin/get"); + const res = await axios.get("/api/plugin/get"); Object.assign(extension_data, res.data); + + const failRes = await axios.get("/api/plugin/source/get-failed-plugins"); + failedPluginsDict.value = failRes.data.data || {}; + checkUpdate(); } catch (err) { toast(err, "error"); @@ -370,6 +376,36 @@ const getExtensions = async () => { } }; +const handleReloadAllFailed = async () => { + const dirNames = Object.keys(failedPluginsDict.value); + if (dirNames.length === 0) { + toast("没有需要重载的失败插件", "info"); + return; + } + + loading_.value = true; + try { + const promises = dirNames.map(dir => + axios.post("/api/plugin/reload-failed", { dir_name: dir }) + ); + await Promise.all(promises); + + toast("已尝试重载所有失败插件", "success"); + + // 清空 message 关闭对话框 + extension_data.message = ""; + + // 刷新列表 + await getExtensions(); + + } catch (e) { + console.error("重载失败:", e); + toast("批量重载过程中出现错误", "error"); + } finally { + loading_.value = false; + } +}; + const checkUpdate = () => { const onlinePluginsMap = new Map(); const onlinePluginsNameMap = new Map(); @@ -1257,6 +1293,15 @@ watch(activeTab, (newTab) => {

+ + 尝试一键重载修复 + Date: Wed, 11 Feb 2026 22:02:53 +0800 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- dashboard/src/views/ExtensionPage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index ce6b19d4d..9b53fd80b 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -1298,7 +1298,7 @@ watch(activeTab, (newTab) => { color="error" variant="tonal" prepend-icon="mdi-refresh" - @click="handleReloadAllFailed(isActive)" + @click="handleReloadAllFailed" > 尝试一键重载修复 From 0250f18099a48a0f532adf9e3dd31c4fd546a178 Mon Sep 17 00:00:00 2001 From: unknown <2916963017@qq.com> Date: Thu, 12 Feb 2026 07:57:17 +0800 Subject: [PATCH 3/4] fix:reformat code --- astrbot/dashboard/routes/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 6911ebb45..e12c0ccf7 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -86,19 +86,19 @@ async def reload_failed_plugins(self) : try: data = await request.get_json() dir_name = data.get("dir_name") # 这里拿的是目录名,不是插件名 - + if not dir_name: return Response().error("缺少插件目录名").__dict__ # 调用 star_manager.py 中的函数 # 注意:传入的是目录名 success, err = await self.plugin_manager.reload_failed_plugin(dir_name) - + if success: return Response().ok(None, f"插件 {dir_name} 重载成功。").__dict__ else: return Response().error(f"重载失败: {err}").__dict__ - + except Exception as e: logger.error(f"/api/plugin/reload-failed: {traceback.format_exc()}") return Response().error(str(e)).__dict__ @@ -361,7 +361,7 @@ async def get_plugins(self): .ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info) .__dict__ ) - + async def get_failed_plugins(self): """专门获取加载失败的插件列表(字典格式)""" return Response().ok(self.plugin_manager.failed_plugin_dict).__dict__ From ca15230ffce1792d7575f8a1b51cccb470c056e5 Mon Sep 17 00:00:00 2001 From: unknown <2916963017@qq.com> Date: Thu, 12 Feb 2026 10:26:25 +0800 Subject: [PATCH 4/4] fix:reformat code --- astrbot/core/star/star_manager.py | 31 +++++++++++++++--------------- astrbot/dashboard/routes/plugin.py | 8 ++++---- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 080f21b49..0b20d55e9 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -299,7 +299,7 @@ def _purge_modules( except KeyError: logger.warning(f"模块 {module_name} 未载入") - async def reload_failed_plugin( self , dir_name ) : + async def reload_failed_plugin(self, dir_name): """ 重新加载未注册(加载失败)的插件 Args: @@ -309,18 +309,17 @@ async def reload_failed_plugin( self , dir_name ) : - success (bool): 重载是否成功 - error_message (str|None): 错误信息,成功时为 None """ - async with self._pm_lock : - if dir_name in self.failed_plugin_dict : - success,error = await self.load( specified_dir_name=dir_name ) - if success : - self.failed_plugin_dict.pop( dir_name , None ) - if not self.failed_plugin_dict : + async with self._pm_lock: + if dir_name in self.failed_plugin_dict: + success, error = await self.load(specified_dir_name=dir_name) + if success: + self.failed_plugin_dict.pop(dir_name, None) + if not self.failed_plugin_dict: self.failed_plugin_info = "" - return success,None - else : - return False,error - return False,"插件不存在于失败列表中" - + return success, None + else: + return False, error + return False, "插件不存在于失败列表中" async def reload(self, specified_plugin_name=None): """重新加载插件 @@ -656,11 +655,11 @@ async def load(self, specified_module_path=None, specified_dir_name=None): logger.error(f"| {line}") logger.error("----------------------------------") fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}。\n" - self.failed_plugin_dict[ root_dir_name ] = { - "error" : str(e) , - "traceback" : errors + self.failed_plugin_dict[root_dir_name] = { + "error": str(e), + "traceback": errors, } - #记录注册失败的插件名称,以便后续重载插件 + # 记录注册失败的插件名称,以便后续重载插件 # 清除 pip.main 导致的多余的 logging handlers for handler in logging.root.handlers[:]: diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index e12c0ccf7..8af8a44ac 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -76,16 +76,16 @@ def __init__( self._logo_cache = {} - async def reload_failed_plugins(self) : - if DEMO_MODE : - return( + async def reload_failed_plugins(self): + if DEMO_MODE: + return ( Response() .error("You are not permitted to do this operation in demo mode") .__dict__ ) try: data = await request.get_json() - dir_name = data.get("dir_name") # 这里拿的是目录名,不是插件名 + dir_name = data.get("dir_name") # 这里拿的是目录名,不是插件名 if not dir_name: return Response().error("缺少插件目录名").__dict__