Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions astrbot/core/star/star_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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):
"""重新加载插件

Expand Down Expand Up @@ -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[:]:
Expand Down
33 changes: 33 additions & 0 deletions astrbot/dashboard/routes/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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 = []
Expand Down
47 changes: 46 additions & 1 deletion dashboard/src/views/ExtensionPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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();
Expand Down Expand Up @@ -1257,6 +1293,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"
Expand Down
Loading