Skip to content

Commit b1eb88a

Browse files
committed
feat: improve plugin failure handling and extension list UX
1 parent 63ff234 commit b1eb88a

7 files changed

Lines changed: 458 additions & 106 deletions

File tree

astrbot/core/star/star_manager.py

Lines changed: 153 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,58 @@ def _cleanup_plugin_state(self, dir_name: str) -> None:
415415
llm_tools.func_list.remove(tool)
416416
logger.info(f"清理工具: {tool.name}")
417417

418+
def _build_failed_plugin_record(
419+
self,
420+
*,
421+
root_dir_name: str,
422+
plugin_dir_path: str,
423+
reserved: bool,
424+
error: Exception | str,
425+
error_trace: str,
426+
) -> dict:
427+
record: dict = {
428+
"name": root_dir_name,
429+
"error": str(error),
430+
"traceback": error_trace,
431+
"reserved": reserved,
432+
}
433+
try:
434+
metadata = self._load_plugin_metadata(plugin_path=plugin_dir_path)
435+
if metadata:
436+
record.update(
437+
{
438+
"name": metadata.name,
439+
"author": metadata.author,
440+
"desc": metadata.desc,
441+
"version": metadata.version,
442+
"repo": metadata.repo,
443+
"display_name": metadata.display_name,
444+
"support_platforms": metadata.support_platforms,
445+
"astrbot_version": metadata.astrbot_version,
446+
}
447+
)
448+
except Exception as metadata_error:
449+
logger.debug(
450+
f"读取失败插件 {root_dir_name} 元数据失败: {metadata_error!s}",
451+
)
452+
453+
return record
454+
455+
def _rebuild_failed_plugin_info(self) -> None:
456+
if not self.failed_plugin_dict:
457+
self.failed_plugin_info = ""
458+
return
459+
460+
lines = []
461+
for dir_name, info in self.failed_plugin_dict.items():
462+
if isinstance(info, dict):
463+
error = info.get("error", "未知错误")
464+
else:
465+
error = str(info)
466+
lines.append(f"加载 {dir_name} 插件时出现问题,原因 {error}。")
467+
468+
self.failed_plugin_info = "\n".join(lines) + "\n"
469+
418470
async def reload_failed_plugin(self, dir_name):
419471
"""
420472
重新加载未注册(加载失败)的插件
@@ -435,8 +487,7 @@ async def reload_failed_plugin(self, dir_name):
435487
success, error = await self.load(specified_dir_name=dir_name)
436488
if success:
437489
self.failed_plugin_dict.pop(dir_name, None)
438-
if not self.failed_plugin_dict:
439-
self.failed_plugin_info = ""
490+
self._rebuild_failed_plugin_info()
440491
return success, None
441492
else:
442493
return False, error
@@ -567,10 +618,15 @@ async def load(
567618
logger.error(error_trace)
568619
logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}")
569620
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}\n"
570-
self.failed_plugin_dict[root_dir_name] = {
571-
"error": str(e),
572-
"traceback": error_trace,
573-
}
621+
self.failed_plugin_dict[root_dir_name] = (
622+
self._build_failed_plugin_record(
623+
root_dir_name=root_dir_name,
624+
plugin_dir_path=plugin_dir_path,
625+
reserved=reserved,
626+
error=e,
627+
error_trace=error_trace,
628+
)
629+
)
574630
if path in star_map:
575631
logger.info("失败插件依旧在插件列表中,正在清理...")
576632
metadata = star_map.pop(path)
@@ -837,10 +893,15 @@ async def load(
837893
logger.error(f"| {line}")
838894
logger.error("----------------------------------")
839895
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}\n"
840-
self.failed_plugin_dict[root_dir_name] = {
841-
"error": str(e),
842-
"traceback": errors,
843-
}
896+
self.failed_plugin_dict[root_dir_name] = (
897+
self._build_failed_plugin_record(
898+
root_dir_name=root_dir_name,
899+
plugin_dir_path=plugin_dir_path,
900+
reserved=reserved,
901+
error=e,
902+
error_trace=errors,
903+
)
904+
)
844905
# 记录注册失败的插件名称,以便后续重载插件
845906
if path in star_map:
846907
logger.info("失败插件依旧在插件列表中,正在清理...")
@@ -857,10 +918,10 @@ async def load(
857918
logger.error(f"同步指令配置失败: {e!s}")
858919
logger.error(traceback.format_exc())
859920

921+
self._rebuild_failed_plugin_info()
860922
if not fail_rec:
861923
return True, None
862-
self.failed_plugin_info = fail_rec
863-
return False, fail_rec
924+
return False, self.failed_plugin_info
864925

865926
async def _cleanup_failed_plugin_install(
866927
self,
@@ -934,10 +995,8 @@ async def install_plugin(
934995
async with self._pm_lock:
935996
plugin_path = ""
936997
dir_name = ""
937-
cleanup_required = False
938998
try:
939999
plugin_path = await self.updator.install(repo_url, proxy)
940-
cleanup_required = True
9411000

9421001
# reload the plugin
9431002
dir_name = os.path.basename(plugin_path)
@@ -985,10 +1044,9 @@ async def install_plugin(
9851044

9861045
return plugin_info
9871046
except Exception:
988-
if cleanup_required and dir_name and plugin_path:
989-
await self._cleanup_failed_plugin_install(
990-
dir_name=dir_name,
991-
plugin_path=plugin_path,
1047+
if dir_name and plugin_path:
1048+
logger.warning(
1049+
f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}",
9921050
)
9931051
raise
9941052

@@ -1086,6 +1144,80 @@ async def uninstall_plugin(
10861144
except Exception as e:
10871145
logger.warning(f"删除插件持久化数据失败 (plugins_data): {e!s}")
10881146

1147+
async def uninstall_failed_plugin(
1148+
self,
1149+
dir_name: str,
1150+
delete_config: bool = False,
1151+
delete_data: bool = False,
1152+
) -> None:
1153+
"""卸载加载失败的插件(按目录名)。"""
1154+
async with self._pm_lock:
1155+
failed_info = self.failed_plugin_dict.get(dir_name)
1156+
if not failed_info:
1157+
raise Exception("插件不存在于失败列表中。")
1158+
1159+
if isinstance(failed_info, dict) and failed_info.get("reserved"):
1160+
raise Exception("该插件是 AstrBot 保留插件,无法卸载。")
1161+
1162+
plugin_path = os.path.join(self.plugin_store_path, dir_name)
1163+
if not os.path.exists(plugin_path):
1164+
raise Exception("插件目录不存在。")
1165+
1166+
self._cleanup_plugin_state(dir_name)
1167+
1168+
try:
1169+
remove_dir(plugin_path)
1170+
except Exception as e:
1171+
raise Exception(
1172+
f"移除失败插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
1173+
)
1174+
1175+
if delete_config:
1176+
config_file = os.path.join(
1177+
self.plugin_config_path,
1178+
f"{dir_name}_config.json",
1179+
)
1180+
if os.path.exists(config_file):
1181+
try:
1182+
os.remove(config_file)
1183+
logger.info(f"已删除失败插件 {dir_name} 的配置文件")
1184+
except Exception as e:
1185+
logger.warning(f"删除失败插件配置文件失败: {e!s}")
1186+
1187+
if delete_data:
1188+
data_base_dir = os.path.dirname(self.plugin_store_path)
1189+
1190+
plugin_data_dir = os.path.join(data_base_dir, "plugin_data", dir_name)
1191+
if os.path.exists(plugin_data_dir):
1192+
try:
1193+
remove_dir(plugin_data_dir)
1194+
logger.info(
1195+
f"已删除失败插件 {dir_name} 的持久化数据 (plugin_data)",
1196+
)
1197+
except Exception as e:
1198+
logger.warning(
1199+
f"删除失败插件持久化数据失败 (plugin_data): {e!s}",
1200+
)
1201+
1202+
plugins_data_dir = os.path.join(
1203+
data_base_dir,
1204+
"plugins_data",
1205+
dir_name,
1206+
)
1207+
if os.path.exists(plugins_data_dir):
1208+
try:
1209+
remove_dir(plugins_data_dir)
1210+
logger.info(
1211+
f"已删除失败插件 {dir_name} 的持久化数据 (plugins_data)",
1212+
)
1213+
except Exception as e:
1214+
logger.warning(
1215+
f"删除失败插件持久化数据失败 (plugins_data): {e!s}",
1216+
)
1217+
1218+
self.failed_plugin_dict.pop(dir_name, None)
1219+
self._rebuild_failed_plugin_info()
1220+
10891221
async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str) -> None:
10901222
"""解绑并移除一个插件。
10911223
@@ -1267,7 +1399,6 @@ async def install_plugin_from_file(
12671399
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
12681400
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
12691401
desti_dir = os.path.join(self.plugin_store_path, dir_name)
1270-
cleanup_required = False
12711402

12721403
# 第一步:检查是否已安装同目录名的插件,先终止旧插件
12731404
existing_plugin = None
@@ -1289,7 +1420,6 @@ async def install_plugin_from_file(
12891420

12901421
try:
12911422
self.updator.unzip_file(zip_file_path, desti_dir)
1292-
cleanup_required = True
12931423

12941424
# 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件
12951425
try:
@@ -1369,9 +1499,7 @@ async def install_plugin_from_file(
13691499

13701500
return plugin_info
13711501
except Exception:
1372-
if cleanup_required:
1373-
await self._cleanup_failed_plugin_install(
1374-
dir_name=dir_name,
1375-
plugin_path=desti_dir,
1376-
)
1502+
logger.warning(
1503+
f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}",
1504+
)
13771505
raise

astrbot/dashboard/routes/plugin.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(
5858
"/plugin/update": ("POST", self.update_plugin),
5959
"/plugin/update-all": ("POST", self.update_all_plugins),
6060
"/plugin/uninstall": ("POST", self.uninstall_plugin),
61+
"/plugin/uninstall-failed": ("POST", self.uninstall_failed_plugin),
6162
"/plugin/market_list": ("GET", self.get_online_plugins),
6263
"/plugin/off": ("POST", self.off_plugin),
6364
"/plugin/on": ("POST", self.on_plugin),
@@ -565,6 +566,34 @@ async def uninstall_plugin(self):
565566
logger.error(traceback.format_exc())
566567
return Response().error(str(e)).__dict__
567568

569+
async def uninstall_failed_plugin(self):
570+
if DEMO_MODE:
571+
return (
572+
Response()
573+
.error("You are not permitted to do this operation in demo mode")
574+
.__dict__
575+
)
576+
577+
post_data = await request.get_json()
578+
dir_name = post_data.get("dir_name", "")
579+
delete_config = post_data.get("delete_config", False)
580+
delete_data = post_data.get("delete_data", False)
581+
if not dir_name:
582+
return Response().error("缺少失败插件目录名").__dict__
583+
584+
try:
585+
logger.info(f"正在卸载失败插件 {dir_name}")
586+
await self.plugin_manager.uninstall_failed_plugin(
587+
dir_name,
588+
delete_config=delete_config,
589+
delete_data=delete_data,
590+
)
591+
logger.info(f"卸载失败插件 {dir_name} 成功")
592+
return Response().ok(None, "卸载成功").__dict__
593+
except Exception as e:
594+
logger.error(traceback.format_exc())
595+
return Response().error(str(e)).__dict__
596+
568597
async def update_plugin(self):
569598
if DEMO_MODE:
570599
return (

dashboard/src/components/shared/ExtensionCard.vue

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -189,28 +189,15 @@ const viewChangelog = () => {
189189
class="ml-2"
190190
icon="mdi-update"
191191
size="small"
192+
style="cursor: pointer"
193+
@click.stop="updateExtension"
192194
></v-icon>
193195
</template>
194196
<span
195197
>{{ tm("card.status.hasUpdate") }}:
196198
{{ extension.online_version }}</span
197199
>
198200
</v-tooltip>
199-
<v-tooltip
200-
location="top"
201-
v-if="!extension.activated && !marketMode"
202-
>
203-
<template v-slot:activator="{ props: tooltipProps }">
204-
<v-icon
205-
v-bind="tooltipProps"
206-
color="error"
207-
class="ml-2"
208-
icon="mdi-cancel"
209-
size="small"
210-
></v-icon>
211-
</template>
212-
<span>{{ tm("card.status.disabled") }}</span>
213-
</v-tooltip>
214201
</p>
215202

216203
<template v-if="!marketMode">
@@ -299,6 +286,8 @@ const viewChangelog = () => {
299286
color="warning"
300287
label
301288
size="small"
289+
style="cursor: pointer"
290+
@click="updateExtension"
302291
>
303292
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
304293
{{ extension.online_version }}

dashboard/src/i18n/locales/en-US/features/extension.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
"titles": {
1212
"installedAstrBotPlugins": "Installed AstrBot Plugins"
1313
},
14+
"failedPlugins": {
15+
"title": "Failed to Load Plugins ({count})",
16+
"hint": "These plugins failed to load. You can try reload or uninstall them directly.",
17+
"columns": {
18+
"plugin": "Plugin",
19+
"error": "Error"
20+
}
21+
},
1422
"search": {
1523
"placeholder": "Search extensions...",
1624
"marketPlaceholder": "Search market extensions..."

dashboard/src/i18n/locales/zh-CN/features/extension.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
"titles": {
1212
"installedAstrBotPlugins": "已安装的 AstrBot 插件"
1313
},
14+
"failedPlugins": {
15+
"title": "加载失败插件({count})",
16+
"hint": "这些插件加载失败,仍可尝试重载或直接卸载。",
17+
"columns": {
18+
"plugin": "插件",
19+
"error": "错误"
20+
}
21+
},
1422
"search": {
1523
"placeholder": "搜索插件...",
1624
"marketPlaceholder": "搜索市场插件..."

0 commit comments

Comments
 (0)