feat: support hot reload after plugin load failure (#5043)

* add :Support hot reload after plugin load failure

* Apply suggestions from code review

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* fix:reformat code

* fix:reformat code

---------

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
This commit is contained in:
Waterwzy
2026-02-13 18:37:20 +08:00
committed by GitHub
parent 7d1eff3ec4
commit 0faf109c2a
3 changed files with 109 additions and 1 deletions
+30
View File
@@ -62,6 +62,9 @@ class PluginManager:
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())
@@ -327,6 +330,28 @@ class PluginManager:
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):
"""重新加载插件
@@ -663,6 +688,11 @@ class PluginManager:
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[:]:
+33
View File
@@ -54,11 +54,13 @@ class PluginRoute(Route):
"/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
@@ -75,6 +77,33 @@ class PluginRoute(Route):
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 (
@@ -334,6 +363,10 @@ class PluginRoute(Route):
.__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 = []
+46 -1
View File
@@ -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();
@@ -1273,6 +1309,15 @@ watch(activeTab, (newTab) => {
</p>
</v-card-text>
<v-card-actions>
<v-btn
color="error"
variant="tonal"
prepend-icon="mdi-refresh"
@click="handleReloadAllFailed"
>
尝试一键重载修复
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="primary"