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:
@@ -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[:]:
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user