diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 587808956..0b20d55e9 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,28 @@ 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 +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, + } + # 记录注册失败的插件名称,以便后续重载插件 # 清除 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..8af8a44ac 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 ( @@ -333,6 +362,10 @@ async def get_plugins(self): .__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]): """解析插件行为""" handlers = [] diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index c62637e3c..9b53fd80b 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) => {