Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
188 changes: 158 additions & 30 deletions astrbot/core/star/star_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,58 @@ def _cleanup_plugin_state(self, dir_name: str) -> None:
llm_tools.func_list.remove(tool)
logger.info(f"清理工具: {tool.name}")

def _build_failed_plugin_record(
self,
*,
root_dir_name: str,
plugin_dir_path: str,
reserved: bool,
error: Exception | str,
error_trace: str,
) -> dict:
record: dict = {
"name": root_dir_name,
"error": str(error),
"traceback": error_trace,
"reserved": reserved,
}
try:
metadata = self._load_plugin_metadata(plugin_path=plugin_dir_path)
if metadata:
record.update(
{
"name": metadata.name,
"author": metadata.author,
"desc": metadata.desc,
"version": metadata.version,
"repo": metadata.repo,
"display_name": metadata.display_name,
"support_platforms": metadata.support_platforms,
"astrbot_version": metadata.astrbot_version,
}
)
except Exception as metadata_error:
logger.debug(
f"读取失败插件 {root_dir_name} 元数据失败: {metadata_error!s}",
)

return record

def _rebuild_failed_plugin_info(self) -> None:
if not self.failed_plugin_dict:
self.failed_plugin_info = ""
return

lines = []
for dir_name, info in self.failed_plugin_dict.items():
if isinstance(info, dict):
error = info.get("error", "未知错误")
else:
error = str(info)
lines.append(f"加载 {dir_name} 插件时出现问题,原因 {error}。")

self.failed_plugin_info = "\n".join(lines) + "\n"
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

async def reload_failed_plugin(self, dir_name):
"""
重新加载未注册(加载失败)的插件
Expand All @@ -435,8 +487,7 @@ async def reload_failed_plugin(self, dir_name):
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 = ""
self._rebuild_failed_plugin_info()
return success, None
else:
return False, error
Expand Down Expand Up @@ -524,7 +575,7 @@ async def load(
if plugin_modules is None:
return False, "未找到任何插件模块"

fail_rec = ""
has_load_error = False

# 导入插件模块,并尝试实例化插件类
for plugin_module in plugin_modules:
Expand Down Expand Up @@ -566,11 +617,16 @@ async def load(
error_trace = traceback.format_exc()
logger.error(error_trace)
logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}")
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}。\n"
self.failed_plugin_dict[root_dir_name] = {
"error": str(e),
"traceback": error_trace,
}
has_load_error = True
self.failed_plugin_dict[root_dir_name] = (
self._build_failed_plugin_record(
root_dir_name=root_dir_name,
plugin_dir_path=plugin_dir_path,
reserved=reserved,
error=e,
error_trace=error_trace,
)
)
if path in star_map:
logger.info("失败插件依旧在插件列表中,正在清理...")
metadata = star_map.pop(path)
Expand Down Expand Up @@ -836,11 +892,16 @@ async def load(
for line in errors.split("\n"):
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,
}
has_load_error = True
self.failed_plugin_dict[root_dir_name] = (
self._build_failed_plugin_record(
root_dir_name=root_dir_name,
plugin_dir_path=plugin_dir_path,
reserved=reserved,
error=e,
error_trace=errors,
)
)
# 记录注册失败的插件名称,以便后续重载插件
if path in star_map:
logger.info("失败插件依旧在插件列表中,正在清理...")
Expand All @@ -857,10 +918,10 @@ async def load(
logger.error(f"同步指令配置失败: {e!s}")
logger.error(traceback.format_exc())

if not fail_rec:
return True, None
self.failed_plugin_info = fail_rec
return False, fail_rec
self._rebuild_failed_plugin_info()
if has_load_error:
return False, self.failed_plugin_info
return True, None

async def _cleanup_failed_plugin_install(
self,
Expand Down Expand Up @@ -934,10 +995,8 @@ async def install_plugin(
async with self._pm_lock:
plugin_path = ""
dir_name = ""
cleanup_required = False
try:
plugin_path = await self.updator.install(repo_url, proxy)
cleanup_required = True

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

return plugin_info
except Exception:
if cleanup_required and dir_name and plugin_path:
await self._cleanup_failed_plugin_install(
dir_name=dir_name,
plugin_path=plugin_path,
if dir_name and plugin_path:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
logger.warning(
f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}",
)
raise

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

async def uninstall_failed_plugin(
self,
dir_name: str,
delete_config: bool = False,
delete_data: bool = False,
) -> None:
"""卸载加载失败的插件(按目录名)。"""
async with self._pm_lock:
failed_info = self.failed_plugin_dict.get(dir_name)
if not failed_info:
raise Exception("插件不存在于失败列表中。")

if isinstance(failed_info, dict) and failed_info.get("reserved"):
raise Exception("该插件是 AstrBot 保留插件,无法卸载。")

plugin_path = os.path.join(self.plugin_store_path, dir_name)
if not os.path.exists(plugin_path):
raise Exception("插件目录不存在。")

self._cleanup_plugin_state(dir_name)

try:
remove_dir(plugin_path)
except Exception as e:
raise Exception(
f"移除失败插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
)

if delete_config:
config_file = os.path.join(
self.plugin_config_path,
f"{dir_name}_config.json",
)
if os.path.exists(config_file):
try:
os.remove(config_file)
logger.info(f"已删除失败插件 {dir_name} 的配置文件")
except Exception as e:
logger.warning(f"删除失败插件配置文件失败: {e!s}")

if delete_data:
data_base_dir = os.path.dirname(self.plugin_store_path)

plugin_data_dir = os.path.join(data_base_dir, "plugin_data", dir_name)
if os.path.exists(plugin_data_dir):
try:
remove_dir(plugin_data_dir)
logger.info(
f"已删除失败插件 {dir_name} 的持久化数据 (plugin_data)",
)
except Exception as e:
logger.warning(
f"删除失败插件持久化数据失败 (plugin_data): {e!s}",
)

plugins_data_dir = os.path.join(
data_base_dir,
"plugins_data",
dir_name,
)
if os.path.exists(plugins_data_dir):
try:
remove_dir(plugins_data_dir)
logger.info(
f"已删除失败插件 {dir_name} 的持久化数据 (plugins_data)",
)
except Exception as e:
logger.warning(
f"删除失败插件持久化数据失败 (plugins_data): {e!s}",
)

self.failed_plugin_dict.pop(dir_name, None)
self._rebuild_failed_plugin_info()

async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str) -> None:
"""解绑并移除一个插件。

Expand Down Expand Up @@ -1267,7 +1399,6 @@ async def install_plugin_from_file(
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
desti_dir = os.path.join(self.plugin_store_path, dir_name)
cleanup_required = False

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

try:
self.updator.unzip_file(zip_file_path, desti_dir)
cleanup_required = True

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

return plugin_info
except Exception:
if cleanup_required:
await self._cleanup_failed_plugin_install(
dir_name=dir_name,
plugin_path=desti_dir,
)
logger.warning(
f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}",
)
raise
29 changes: 29 additions & 0 deletions astrbot/dashboard/routes/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(
"/plugin/update": ("POST", self.update_plugin),
"/plugin/update-all": ("POST", self.update_all_plugins),
"/plugin/uninstall": ("POST", self.uninstall_plugin),
"/plugin/uninstall-failed": ("POST", self.uninstall_failed_plugin),
"/plugin/market_list": ("GET", self.get_online_plugins),
"/plugin/off": ("POST", self.off_plugin),
"/plugin/on": ("POST", self.on_plugin),
Expand Down Expand Up @@ -565,6 +566,34 @@ async def uninstall_plugin(self):
logger.error(traceback.format_exc())
return Response().error(str(e)).__dict__

async def uninstall_failed_plugin(self):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode")
.__dict__
)

post_data = await request.get_json()
dir_name = post_data.get("dir_name", "")
delete_config = post_data.get("delete_config", False)
delete_data = post_data.get("delete_data", False)
if not dir_name:
return Response().error("缺少失败插件目录名").__dict__

try:
logger.info(f"正在卸载失败插件 {dir_name}")
await self.plugin_manager.uninstall_failed_plugin(
dir_name,
delete_config=delete_config,
delete_data=delete_data,
)
logger.info(f"卸载失败插件 {dir_name} 成功")
return Response().ok(None, "卸载成功").__dict__
except Exception as e:
logger.error(traceback.format_exc())
return Response().error(str(e)).__dict__

async def update_plugin(self):
if DEMO_MODE:
return (
Expand Down
19 changes: 4 additions & 15 deletions dashboard/src/components/shared/ExtensionCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -189,28 +189,15 @@ const viewChangelog = () => {
class="ml-2"
icon="mdi-update"
size="small"
style="cursor: pointer"
@click.stop="updateExtension"
></v-icon>
</template>
<span
>{{ tm("card.status.hasUpdate") }}:
{{ extension.online_version }}</span
>
</v-tooltip>
<v-tooltip
location="top"
v-if="!extension.activated && !marketMode"
>
<template v-slot:activator="{ props: tooltipProps }">
<v-icon
v-bind="tooltipProps"
color="error"
class="ml-2"
icon="mdi-cancel"
size="small"
></v-icon>
</template>
<span>{{ tm("card.status.disabled") }}</span>
</v-tooltip>
</p>

<template v-if="!marketMode">
Expand Down Expand Up @@ -299,6 +286,8 @@ const viewChangelog = () => {
color="warning"
label
size="small"
style="cursor: pointer"
@click="updateExtension"
>
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
{{ extension.online_version }}
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
"titles": {
"installedAstrBotPlugins": "Installed AstrBot Plugins"
},
"failedPlugins": {
"title": "Failed to Load Plugins ({count})",
"hint": "These plugins failed to load. You can try reload or uninstall them directly.",
"columns": {
"plugin": "Plugin",
"error": "Error"
}
},
"search": {
"placeholder": "Search extensions...",
"marketPlaceholder": "Search market extensions..."
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
"titles": {
"installedAstrBotPlugins": "已安装的 AstrBot 插件"
},
"failedPlugins": {
"title": "加载失败插件({count})",
"hint": "这些插件加载失败,仍可尝试重载或直接卸载。",
"columns": {
"plugin": "插件",
"error": "错误"
}
},
"search": {
"placeholder": "搜索插件...",
"marketPlaceholder": "搜索市场插件..."
Expand Down
Loading