feat: improve plugin failure handling and extension list UX (#5535)
* feat: improve plugin failure handling and extension list UX * fix: address plugin review comments * fix: clear stale reload feedback on failed plugin reload * fix: refine extension i18n and uninstall flow * fix: refresh extension list after install failure * feat: add random plugin visibility controls in market * refactor: extract extension helpers and simplify uninstall flow * refactor: improve failed plugin diagnostics and uninstall flow * refactor: streamline extension uninstall flow * fix: harden failed plugin install tracking and cleanup * refactor: simplify extension flows and remove unused timed message * fix: improve failed uninstall idempotency and extension error handling * refactor: unify extension install-uninstall orchestration
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
"""Shared plugin error message templates for star manager flows."""
|
||||
|
||||
PLUGIN_ERROR_TEMPLATES = {
|
||||
"not_found_in_failed_list": "插件不存在于失败列表中。",
|
||||
"reserved_plugin_cannot_uninstall": "该插件是 AstrBot 保留插件,无法卸载。",
|
||||
"failed_plugin_dir_remove_error": (
|
||||
"移除失败插件成功,但是删除插件文件夹失败: {error}。"
|
||||
"您可以手动删除该文件夹,位于 addons/plugins/ 下。"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def format_plugin_error(key: str, **kwargs) -> str:
|
||||
template = PLUGIN_ERROR_TEMPLATES.get(key, key)
|
||||
try:
|
||||
return template.format(**kwargs)
|
||||
except Exception:
|
||||
return template
|
||||
@@ -31,6 +31,7 @@ from astrbot.core.utils.metrics import Metric
|
||||
from . import StarMetadata
|
||||
from .command_management import sync_command_configs
|
||||
from .context import Context
|
||||
from .error_messages import format_plugin_error
|
||||
from .filter.permission import PermissionType, PermissionTypeFilter
|
||||
from .star import star_map, star_registry
|
||||
from .star_handler import EventType, star_handlers_registry
|
||||
@@ -415,6 +416,68 @@ class PluginManager:
|
||||
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", "未知错误")
|
||||
display_name = info.get("display_name") or info.get("name") or dir_name
|
||||
version = info.get("version") or info.get("astrbot_version")
|
||||
if version:
|
||||
lines.append(
|
||||
f"加载插件「{display_name}」(目录: {dir_name}, 版本: {version}) 时出现问题,原因:{error}。",
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
f"加载插件「{display_name}」(目录: {dir_name}) 时出现问题,原因:{error}。",
|
||||
)
|
||||
else:
|
||||
error = str(info)
|
||||
lines.append(f"加载插件目录 {dir_name} 时出现问题,原因:{error}。")
|
||||
|
||||
self.failed_plugin_info = "\n".join(lines) + "\n"
|
||||
|
||||
async def reload_failed_plugin(self, dir_name):
|
||||
"""
|
||||
重新加载未注册(加载失败)的插件
|
||||
@@ -435,8 +498,7 @@ class PluginManager:
|
||||
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
|
||||
@@ -524,7 +586,7 @@ class PluginManager:
|
||||
if plugin_modules is None:
|
||||
return False, "未找到任何插件模块"
|
||||
|
||||
fail_rec = ""
|
||||
has_load_error = False
|
||||
|
||||
# 导入插件模块,并尝试实例化插件类
|
||||
for plugin_module in plugin_modules:
|
||||
@@ -566,11 +628,16 @@ class PluginManager:
|
||||
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)
|
||||
@@ -836,11 +903,16 @@ class PluginManager:
|
||||
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("失败插件依旧在插件列表中,正在清理...")
|
||||
@@ -857,10 +929,10 @@ class PluginManager:
|
||||
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,
|
||||
@@ -905,6 +977,73 @@ class PluginManager:
|
||||
f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}",
|
||||
)
|
||||
|
||||
def _cleanup_plugin_optional_artifacts(
|
||||
self,
|
||||
*,
|
||||
root_dir_name: str,
|
||||
plugin_label: str,
|
||||
delete_config: bool,
|
||||
delete_data: bool,
|
||||
) -> None:
|
||||
if delete_config:
|
||||
config_file = os.path.join(
|
||||
self.plugin_config_path,
|
||||
f"{root_dir_name}_config.json",
|
||||
)
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
os.remove(config_file)
|
||||
logger.info(f"已删除插件 {plugin_label} 的配置文件")
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件配置文件失败 ({plugin_label}): {e!s}")
|
||||
|
||||
if delete_data:
|
||||
data_base_dir = os.path.dirname(self.plugin_store_path)
|
||||
for data_dir_name in ("plugin_data", "plugins_data"):
|
||||
plugin_data_dir = os.path.join(
|
||||
data_base_dir,
|
||||
data_dir_name,
|
||||
root_dir_name,
|
||||
)
|
||||
if os.path.exists(plugin_data_dir):
|
||||
try:
|
||||
remove_dir(plugin_data_dir)
|
||||
logger.info(
|
||||
f"已删除插件 {plugin_label} 的持久化数据 ({data_dir_name})",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"删除插件持久化数据失败 ({data_dir_name}, {plugin_label}): {e!s}",
|
||||
)
|
||||
|
||||
def _track_failed_install_dir(
|
||||
self,
|
||||
*,
|
||||
dir_name: str,
|
||||
plugin_path: str,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
if (
|
||||
not dir_name
|
||||
or not plugin_path
|
||||
or not os.path.isdir(plugin_path)
|
||||
or dir_name in self.failed_plugin_dict
|
||||
):
|
||||
return
|
||||
|
||||
for star in self.context.get_all_stars():
|
||||
if star.root_dir_name == dir_name:
|
||||
return
|
||||
|
||||
self.failed_plugin_dict[dir_name] = self._build_failed_plugin_record(
|
||||
root_dir_name=dir_name,
|
||||
plugin_dir_path=plugin_path,
|
||||
reserved=False,
|
||||
error=error,
|
||||
error_trace=traceback.format_exc(),
|
||||
)
|
||||
self._rebuild_failed_plugin_info()
|
||||
|
||||
async def install_plugin(
|
||||
self, repo_url: str, proxy: str = "", ignore_version_check: bool = False
|
||||
):
|
||||
@@ -934,10 +1073,8 @@ class PluginManager:
|
||||
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)
|
||||
@@ -984,11 +1121,15 @@ class PluginManager:
|
||||
}
|
||||
|
||||
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,
|
||||
except Exception as e:
|
||||
self._track_failed_install_dir(
|
||||
dir_name=dir_name,
|
||||
plugin_path=plugin_path,
|
||||
error=e,
|
||||
)
|
||||
if dir_name and plugin_path:
|
||||
logger.warning(
|
||||
f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}",
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -1041,50 +1182,68 @@ class PluginManager:
|
||||
f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
|
||||
)
|
||||
|
||||
# 删除插件配置文件
|
||||
if delete_config and root_dir_name:
|
||||
config_file = os.path.join(
|
||||
self.plugin_config_path,
|
||||
f"{root_dir_name}_config.json",
|
||||
)
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
os.remove(config_file)
|
||||
logger.info(f"已删除插件 {plugin_name} 的配置文件")
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件配置文件失败: {e!s}")
|
||||
self._cleanup_plugin_optional_artifacts(
|
||||
root_dir_name=root_dir_name,
|
||||
plugin_label=plugin_name,
|
||||
delete_config=delete_config,
|
||||
delete_data=delete_data,
|
||||
)
|
||||
|
||||
# 删除插件持久化数据
|
||||
# 注意:需要检查两个可能的目录名(plugin_data 和 plugins_data)
|
||||
# data/temp 目录可能被多个插件共享,不自动删除以防误删
|
||||
if delete_data and root_dir_name:
|
||||
data_base_dir = os.path.dirname(ppath) # data/
|
||||
|
||||
# 删除 data/plugin_data 下的插件持久化数据(单数形式,新版本)
|
||||
plugin_data_dir = os.path.join(
|
||||
data_base_dir, "plugin_data", root_dir_name
|
||||
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(
|
||||
format_plugin_error("not_found_in_failed_list"),
|
||||
)
|
||||
if os.path.exists(plugin_data_dir):
|
||||
try:
|
||||
remove_dir(plugin_data_dir)
|
||||
logger.info(
|
||||
f"已删除插件 {plugin_name} 的持久化数据 (plugin_data)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件持久化数据失败 (plugin_data): {e!s}")
|
||||
|
||||
# 删除 data/plugins_data 下的插件持久化数据(复数形式,旧版本兼容)
|
||||
plugins_data_dir = os.path.join(
|
||||
data_base_dir, "plugins_data", root_dir_name
|
||||
if isinstance(failed_info, dict) and failed_info.get("reserved"):
|
||||
raise Exception(
|
||||
format_plugin_error("reserved_plugin_cannot_uninstall"),
|
||||
)
|
||||
if os.path.exists(plugins_data_dir):
|
||||
try:
|
||||
remove_dir(plugins_data_dir)
|
||||
logger.info(
|
||||
f"已删除插件 {plugin_name} 的持久化数据 (plugins_data)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件持久化数据失败 (plugins_data): {e!s}")
|
||||
|
||||
self._cleanup_plugin_state(dir_name)
|
||||
|
||||
plugin_path = os.path.join(self.plugin_store_path, dir_name)
|
||||
if os.path.exists(plugin_path):
|
||||
try:
|
||||
remove_dir(plugin_path)
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
format_plugin_error(
|
||||
"failed_plugin_dir_remove_error",
|
||||
error=f"{e!s}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"插件目录不存在,视为已部分卸载状态,继续清理失败插件记录和可选产物: %s",
|
||||
plugin_path,
|
||||
)
|
||||
|
||||
plugin_label = dir_name
|
||||
if isinstance(failed_info, dict):
|
||||
plugin_label = (
|
||||
failed_info.get("display_name")
|
||||
or failed_info.get("name")
|
||||
or dir_name
|
||||
)
|
||||
|
||||
self._cleanup_plugin_optional_artifacts(
|
||||
root_dir_name=dir_name,
|
||||
plugin_label=plugin_label,
|
||||
delete_config=delete_config,
|
||||
delete_data=delete_data,
|
||||
)
|
||||
|
||||
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:
|
||||
"""解绑并移除一个插件。
|
||||
@@ -1267,7 +1426,6 @@ class PluginManager:
|
||||
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
|
||||
@@ -1289,7 +1447,6 @@ class PluginManager:
|
||||
|
||||
try:
|
||||
self.updator.unzip_file(zip_file_path, desti_dir)
|
||||
cleanup_required = True
|
||||
|
||||
# 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件
|
||||
try:
|
||||
@@ -1368,10 +1525,13 @@ class PluginManager:
|
||||
)
|
||||
|
||||
return plugin_info
|
||||
except Exception:
|
||||
if cleanup_required:
|
||||
await self._cleanup_failed_plugin_install(
|
||||
dir_name=dir_name,
|
||||
plugin_path=desti_dir,
|
||||
)
|
||||
except Exception as e:
|
||||
self._track_failed_install_dir(
|
||||
dir_name=dir_name,
|
||||
plugin_path=desti_dir,
|
||||
error=e,
|
||||
)
|
||||
logger.warning(
|
||||
f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}",
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -58,6 +58,7 @@ class PluginRoute(Route):
|
||||
"/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),
|
||||
@@ -565,6 +566,34 @@ class PluginRoute(Route):
|
||||
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 (
|
||||
|
||||
@@ -189,6 +189,8 @@ const viewChangelog = () => {
|
||||
class="ml-2"
|
||||
icon="mdi-update"
|
||||
size="small"
|
||||
style="cursor: pointer"
|
||||
@click.stop="updateExtension"
|
||||
></v-icon>
|
||||
</template>
|
||||
<span
|
||||
@@ -196,21 +198,6 @@ const viewChangelog = () => {
|
||||
{{ 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">
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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..."
|
||||
@@ -109,6 +117,8 @@
|
||||
"sourceExists": "This source already exists",
|
||||
"installPlugin": "Install Plugin",
|
||||
"randomPlugins": "🎲 Random Plugins",
|
||||
"showRandomPlugins": "Show Random Plugins",
|
||||
"hideRandomPlugins": "Hide Random Plugins",
|
||||
"sourceSafetyWarning": "Even with the default source, plugin stability and security cannot be fully guaranteed. Please verify carefully before use."
|
||||
},
|
||||
"sort": {
|
||||
@@ -177,7 +187,9 @@
|
||||
"refreshing": "Refreshing extension list...",
|
||||
"refreshSuccess": "Extension list refreshed!",
|
||||
"refreshFailed": "Error occurred while refreshing extension list",
|
||||
"operationFailed": "Operation failed",
|
||||
"reloadSuccess": "Reload successful",
|
||||
"reloadFailed": "Reload failed",
|
||||
"updateSuccess": "Update successful!",
|
||||
"addSuccess": "Add successful!",
|
||||
"saveSuccess": "Save successful!",
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
"titles": {
|
||||
"installedAstrBotPlugins": "已安装的 AstrBot 插件"
|
||||
},
|
||||
"failedPlugins": {
|
||||
"title": "加载失败插件({count})",
|
||||
"hint": "这些插件加载失败,仍可尝试重载或直接卸载。",
|
||||
"columns": {
|
||||
"plugin": "插件",
|
||||
"error": "错误"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索插件...",
|
||||
"marketPlaceholder": "搜索市场插件..."
|
||||
@@ -109,6 +117,8 @@
|
||||
"sourceExists": "该插件源已存在",
|
||||
"installPlugin": "安装插件",
|
||||
"randomPlugins": "🎲 随机插件",
|
||||
"showRandomPlugins": "显示随机插件",
|
||||
"hideRandomPlugins": "隐藏随机插件",
|
||||
"sourceSafetyWarning": "即使是默认插件源,我们也不能完全保证插件的稳定性和安全性,使用前请谨慎核查。"
|
||||
},
|
||||
"sort": {
|
||||
@@ -177,7 +187,9 @@
|
||||
"refreshing": "正在刷新插件列表...",
|
||||
"refreshSuccess": "插件列表已刷新!",
|
||||
"refreshFailed": "刷新插件列表时发生错误",
|
||||
"operationFailed": "操作失败",
|
||||
"reloadSuccess": "重载成功",
|
||||
"reloadFailed": "重载失败",
|
||||
"updateSuccess": "更新成功!",
|
||||
"addSuccess": "添加成功!",
|
||||
"saveSuccess": "保存成功!",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
const INVALID_ERROR_STRINGS = new Set(["[object Object]", "undefined", "null", ""]);
|
||||
|
||||
const pickResponseMessage = (responseData) => {
|
||||
if (typeof responseData === "string") {
|
||||
return responseData.trim();
|
||||
}
|
||||
if (!responseData || typeof responseData !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const keys = ["message", "error", "detail", "details", "msg"];
|
||||
for (const key of keys) {
|
||||
const value = responseData[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const resolveErrorMessage = (err, fallbackMessage = "") => {
|
||||
if (typeof err === "string") {
|
||||
return err.trim() || fallbackMessage;
|
||||
}
|
||||
if (typeof err === "number" || typeof err === "boolean") {
|
||||
return String(err);
|
||||
}
|
||||
|
||||
const fromResponse =
|
||||
pickResponseMessage(err?.response?.data) ||
|
||||
(typeof err?.response?.statusText === "string"
|
||||
? err.response.statusText.trim()
|
||||
: "");
|
||||
const fromError =
|
||||
typeof err?.message === "string" ? err.message.trim() : "";
|
||||
|
||||
let fromString = "";
|
||||
if (typeof err?.toString === "function") {
|
||||
const value = err.toString().trim();
|
||||
fromString = INVALID_ERROR_STRINGS.has(value) ? "" : value;
|
||||
}
|
||||
|
||||
return fromResponse || fromError || fromString || fallbackMessage;
|
||||
};
|
||||
@@ -59,7 +59,7 @@ const {
|
||||
installCompat,
|
||||
versionCompatibilityDialog,
|
||||
showUninstallDialog,
|
||||
pluginToUninstall,
|
||||
uninstallTarget,
|
||||
showSourceDialog,
|
||||
showSourceManagerDialog,
|
||||
sourceName,
|
||||
|
||||
@@ -56,7 +56,7 @@ const {
|
||||
installCompat,
|
||||
versionCompatibilityDialog,
|
||||
showUninstallDialog,
|
||||
pluginToUninstall,
|
||||
uninstallTarget,
|
||||
showSourceDialog,
|
||||
showSourceManagerDialog,
|
||||
sourceName,
|
||||
@@ -100,11 +100,12 @@ const {
|
||||
toast,
|
||||
resetLoadingDialog,
|
||||
onLoadingDialogResult,
|
||||
failedPluginsDict,
|
||||
failedPluginItems,
|
||||
getExtensions,
|
||||
handleReloadAllFailed,
|
||||
reloadFailedPlugin,
|
||||
checkUpdate,
|
||||
uninstallExtension,
|
||||
requestUninstallFailedPlugin,
|
||||
handleUninstallConfirm,
|
||||
updateExtension,
|
||||
showUpdateAllConfirm,
|
||||
@@ -209,62 +210,89 @@ const {
|
||||
{{ tm("buttons.updateAll") }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog max-width="500px" v-if="extension_data.message">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
class="ml-auto"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon>mdi-alert-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title class="headline d-flex align-center">
|
||||
<v-icon color="error" class="mr-2"
|
||||
>mdi-alert-circle</v-icon
|
||||
>
|
||||
{{ tm("dialogs.error.title") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-1">
|
||||
{{ extension_data.message }}
|
||||
</p>
|
||||
<p class="text-caption mt-2">
|
||||
{{ tm("dialogs.error.checkConsole") }}
|
||||
</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"
|
||||
@click="isActive.value = false"
|
||||
>{{ tm("buttons.close") }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card
|
||||
v-if="failedPluginItems.length > 0"
|
||||
class="mb-4 rounded-lg"
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="warning" class="mr-2">mdi-alert-circle</v-icon>
|
||||
{{ tm("failedPlugins.title", { count: failedPluginItems.length }) }}
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0">
|
||||
<div class="text-body-2 mb-3">
|
||||
{{ tm("failedPlugins.hint") }}
|
||||
</div>
|
||||
<v-table density="compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm("failedPlugins.columns.plugin") }}</th>
|
||||
<th>{{ tm("failedPlugins.columns.error") }}</th>
|
||||
<th class="text-right">{{ tm("buttons.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="plugin in failedPluginItems" :key="plugin.dir_name">
|
||||
<td>
|
||||
<div class="font-weight-medium">
|
||||
{{ plugin.display_name }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ plugin.dir_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="max-width: 520px">
|
||||
<div
|
||||
class="text-caption text-medium-emphasis"
|
||||
style="
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
{{ plugin.error || tm("status.unknown") }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="reloadFailedPlugin(plugin.dir_name)"
|
||||
>
|
||||
{{ tm("buttons.reload") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
prepend-icon="mdi-delete"
|
||||
:disabled="plugin.reserved"
|
||||
@click="requestUninstallFailedPlugin(plugin.dir_name)"
|
||||
>
|
||||
{{ tm("buttons.uninstall") }}
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-fade-transition hide-on-leave>
|
||||
<!-- 表格视图 -->
|
||||
<div v-if="isListView">
|
||||
<v-card class="rounded-lg overflow-hidden elevation-0">
|
||||
<v-data-table
|
||||
class="plugin-list-table"
|
||||
:headers="pluginHeaders"
|
||||
:items="filteredPlugins"
|
||||
:loading="loading_"
|
||||
@@ -395,19 +423,36 @@ const {
|
||||
<template v-slot:item.version="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-body-2">{{ item.version }}</span>
|
||||
<v-icon
|
||||
v-if="item.has_update"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
>mdi-alert</v-icon
|
||||
>
|
||||
<v-tooltip v-if="item.has_update" activator="parent">
|
||||
<v-tooltip v-if="item.has_update" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
style="cursor: pointer"
|
||||
@click.stop="updateExtension(item.name)"
|
||||
>mdi-alert</v-icon
|
||||
>
|
||||
</template>
|
||||
<span
|
||||
>{{ tm("messages.hasUpdate") }}
|
||||
{{ item.online_version }}</span
|
||||
>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-if="item.has_update" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<span
|
||||
v-bind="tooltipProps"
|
||||
class="ml-1 text-caption text-warning"
|
||||
style="cursor: pointer"
|
||||
@click.stop="updateExtension(item.name)"
|
||||
>
|
||||
{{ item.online_version }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ tm("buttons.update") }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -416,7 +461,7 @@ const {
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="table-action-row d-flex align-center flex-nowrap ga-2 py-1">
|
||||
<div class="table-action-row d-flex align-center flex-nowrap justify-start ga-2 py-1">
|
||||
<v-btn
|
||||
v-if="!item.activated"
|
||||
size="small"
|
||||
@@ -617,14 +662,27 @@ const {
|
||||
}
|
||||
|
||||
.table-action-btn {
|
||||
min-height: 34px;
|
||||
font-size: 0.9rem;
|
||||
min-height: 32px;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-action-row {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.plugin-list-table :deep(td) {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.table-action-btn {
|
||||
min-width: 0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.fab-button {
|
||||
|
||||
@@ -56,7 +56,7 @@ const {
|
||||
installCompat,
|
||||
versionCompatibilityDialog,
|
||||
showUninstallDialog,
|
||||
pluginToUninstall,
|
||||
uninstallTarget,
|
||||
showSourceDialog,
|
||||
showSourceManagerDialog,
|
||||
sourceName,
|
||||
@@ -78,6 +78,7 @@ const {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
@@ -92,6 +93,7 @@ const {
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
@@ -161,29 +163,50 @@ const currentSourceName = computed(() => {
|
||||
<template>
|
||||
<v-tab-item v-show="activeTab === 'market'">
|
||||
<div class="mb-6 pt-4 pb-4">
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 12px">
|
||||
<h2 class="text-h2 mb-0">{{ tm("tabs.market") }}</h2>
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 12px"
|
||||
>
|
||||
<div class="d-flex align-center" style="gap: 12px; min-width: 0">
|
||||
<h2 class="text-h2 mb-0">{{ tm("tabs.market") }}</h2>
|
||||
|
||||
<v-tooltip location="top" :text="tm('market.sourceManagement')">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="tonal"
|
||||
rounded="md"
|
||||
color="primary"
|
||||
class="text-none px-2"
|
||||
@click="openSourceManagerDialog"
|
||||
>
|
||||
<v-icon size="18" class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="text-truncate" style="max-width: 180px">
|
||||
{{ currentSourceName }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip location="top" :text="tm('market.sourceManagement')">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="tonal"
|
||||
rounded="md"
|
||||
color="primary"
|
||||
class="text-none px-2"
|
||||
@click="openSourceManagerDialog"
|
||||
>
|
||||
<v-icon size="18" class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="text-truncate" style="max-width: 180px">
|
||||
{{ currentSourceName }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded="md"
|
||||
class="text-none px-2"
|
||||
:prepend-icon="showRandomPlugins ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click="toggleRandomPluginsVisibility"
|
||||
>
|
||||
{{
|
||||
showRandomPlugins
|
||||
? tm("market.hideRandomPlugins")
|
||||
: tm("market.showRandomPlugins")
|
||||
}}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="marketSearch"
|
||||
class="ml-auto"
|
||||
density="compact"
|
||||
:label="tm('search.marketPlaceholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
@@ -191,7 +214,7 @@ const currentSourceName = computed(() => {
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
style="min-width: 220px; max-width: 340px"
|
||||
style="width: 340px; min-width: 220px; max-width: 340px"
|
||||
>
|
||||
</v-text-field>
|
||||
</div>
|
||||
@@ -237,41 +260,45 @@ const currentSourceName = computed(() => {
|
||||
</v-tooltip>
|
||||
|
||||
<div class="mt-4">
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
style="justify-content: space-between; flex-wrap: wrap; gap: 8px"
|
||||
>
|
||||
<h2>
|
||||
{{ tm("market.randomPlugins") }}
|
||||
</h2>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-shuffle-variant"
|
||||
:disabled="pluginMarketData.length === 0"
|
||||
@click="refreshRandomPlugins"
|
||||
>
|
||||
{{ tm("buttons.reshuffle") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-expand-transition>
|
||||
<div v-if="showRandomPlugins">
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
style="justify-content: space-between; flex-wrap: wrap; gap: 8px"
|
||||
>
|
||||
<h2>
|
||||
{{ tm("market.randomPlugins") }}
|
||||
</h2>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-shuffle-variant"
|
||||
:disabled="pluginMarketData.length === 0"
|
||||
@click="refreshRandomPlugins"
|
||||
>
|
||||
{{ tm("buttons.reshuffle") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-row class="mb-6" dense>
|
||||
<v-col
|
||||
v-for="plugin in randomPlugins"
|
||||
:key="`random-${plugin.name}`"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
class="pb-2"
|
||||
>
|
||||
<MarketPluginCard
|
||||
:plugin="plugin"
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mb-6" dense>
|
||||
<v-col
|
||||
v-for="plugin in randomPlugins"
|
||||
:key="`random-${plugin.name}`"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
class="pb-2"
|
||||
>
|
||||
<MarketPluginCard
|
||||
:plugin="plugin"
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
|
||||
@@ -2,10 +2,56 @@ import axios from "axios";
|
||||
import { pinyin } from "pinyin-pro";
|
||||
import { useCommonStore } from "@/stores/common";
|
||||
import { useI18n, useModuleI18n } from "@/i18n/composables";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { getPlatformDisplayName } from "@/utils/platformUtils";
|
||||
import { resolveErrorMessage } from "@/utils/errorUtils";
|
||||
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
const useRandomPluginsDisplay = ({ activeTab, marketSearch, currentPage }) => {
|
||||
const showRandomPlugins = ref(true);
|
||||
|
||||
const toggleRandomPluginsVisibility = () => {
|
||||
showRandomPlugins.value = !showRandomPlugins.value;
|
||||
};
|
||||
|
||||
const collapseRandomPlugins = () => {
|
||||
showRandomPlugins.value = false;
|
||||
};
|
||||
|
||||
watch(marketSearch, () => {
|
||||
if (activeTab.value === "market") {
|
||||
collapseRandomPlugins();
|
||||
}
|
||||
});
|
||||
|
||||
watch(currentPage, (newPage, oldPage) => {
|
||||
if (newPage === oldPage) return;
|
||||
if (activeTab.value !== "market") return;
|
||||
collapseRandomPlugins();
|
||||
});
|
||||
|
||||
return {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
};
|
||||
};
|
||||
|
||||
const buildFailedPluginItems = (raw) => {
|
||||
return Object.entries(raw || {}).map(([dirName, info]) => {
|
||||
const detail = info && typeof info === "object" ? info : {};
|
||||
return {
|
||||
...detail,
|
||||
dir_name: dirName,
|
||||
name: detail.name || dirName,
|
||||
display_name: detail.display_name || detail.name || dirName,
|
||||
error: detail.error || "",
|
||||
traceback: detail.traceback || "",
|
||||
reserved: !!detail.reserved,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const useExtensionPage = () => {
|
||||
|
||||
@@ -15,6 +61,7 @@ export const useExtensionPage = () => {
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { width } = useDisplay();
|
||||
|
||||
const getSelectedGitHubProxy = () => {
|
||||
if (typeof window === "undefined" || !window.localStorage) return "";
|
||||
@@ -156,7 +203,7 @@ export const useExtensionPage = () => {
|
||||
|
||||
// 卸载插件确认对话框(列表模式用)
|
||||
const showUninstallDialog = ref(false);
|
||||
const pluginToUninstall = ref(null);
|
||||
const uninstallTarget = ref(null);
|
||||
|
||||
// 自定义插件源相关
|
||||
const showSourceDialog = ref(false);
|
||||
@@ -182,6 +229,15 @@ export const useExtensionPage = () => {
|
||||
const sortBy = ref("default"); // default, stars, author, updated
|
||||
const sortOrder = ref("desc"); // desc (降序) or asc (升序)
|
||||
const randomPluginNames = ref([]);
|
||||
const {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
} = useRandomPluginsDisplay({
|
||||
activeTab,
|
||||
marketSearch,
|
||||
currentPage,
|
||||
});
|
||||
|
||||
// 插件市场拼音搜索
|
||||
const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
|
||||
@@ -224,18 +280,43 @@ export const useExtensionPage = () => {
|
||||
]);
|
||||
|
||||
// 插件表格的表头定义
|
||||
const pluginHeaders = computed(() => [
|
||||
{ title: tm("table.headers.name"), key: "name", width: "200px" },
|
||||
{ title: tm("table.headers.description"), key: "desc", width: "180px" },
|
||||
{ title: tm("table.headers.version"), key: "version", width: "100px" },
|
||||
{ title: tm("table.headers.author"), key: "author", width: "100px" },
|
||||
{
|
||||
const showAuthorColumn = computed(() => width.value >= 1280);
|
||||
const pluginHeaders = computed(() => {
|
||||
const headers = [
|
||||
{
|
||||
title: tm("table.headers.name"),
|
||||
key: "name",
|
||||
width: showAuthorColumn.value ? "24%" : "26%",
|
||||
},
|
||||
{
|
||||
title: tm("table.headers.description"),
|
||||
key: "desc",
|
||||
width: showAuthorColumn.value ? "32%" : "36%",
|
||||
},
|
||||
{
|
||||
title: tm("table.headers.version"),
|
||||
key: "version",
|
||||
width: showAuthorColumn.value ? "12%" : "14%",
|
||||
},
|
||||
];
|
||||
|
||||
if (showAuthorColumn.value) {
|
||||
headers.push({
|
||||
title: tm("table.headers.author"),
|
||||
key: "author",
|
||||
width: "10%",
|
||||
});
|
||||
}
|
||||
|
||||
headers.push({
|
||||
title: tm("table.headers.actions"),
|
||||
key: "actions",
|
||||
sortable: false,
|
||||
width: "520px",
|
||||
},
|
||||
]);
|
||||
width: showAuthorColumn.value ? "22%" : "24%",
|
||||
});
|
||||
|
||||
return headers;
|
||||
});
|
||||
|
||||
// 过滤要显示的插件
|
||||
const filteredExtensions = computed(() => {
|
||||
@@ -404,6 +485,9 @@ export const useExtensionPage = () => {
|
||||
};
|
||||
|
||||
const failedPluginsDict = ref({});
|
||||
const failedPluginItems = computed(() =>
|
||||
buildFailedPluginItems(failedPluginsDict.value),
|
||||
);
|
||||
|
||||
const getExtensions = async () => {
|
||||
loading_.value = true;
|
||||
@@ -451,6 +535,75 @@ export const useExtensionPage = () => {
|
||||
loading_.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reloadFailedPlugin = async (dirName) => {
|
||||
if (!dirName) return;
|
||||
|
||||
try {
|
||||
const res = await axios.post("/api/plugin/reload-failed", { dir_name: dirName });
|
||||
if (res.data.status === "error") {
|
||||
toast(res.data.message || tm("messages.reloadFailed"), "error");
|
||||
return;
|
||||
}
|
||||
toast(res.data.message || tm("messages.reloadSuccess"), "success");
|
||||
await getExtensions();
|
||||
} catch (err) {
|
||||
toast(resolveErrorMessage(err, tm("messages.reloadFailed")), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const requestUninstall = (target) => {
|
||||
if (!target?.id || !target?.kind) return;
|
||||
uninstallTarget.value = target;
|
||||
showUninstallDialog.value = true;
|
||||
};
|
||||
|
||||
const uninstall = async (
|
||||
target,
|
||||
{ deleteConfig = false, deleteData = false, skipConfirm = false } = {},
|
||||
) => {
|
||||
if (!target?.id || !target?.kind) return;
|
||||
|
||||
if (!skipConfirm) {
|
||||
requestUninstall(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const isFailed = target.kind === "failed";
|
||||
const endpoint = isFailed
|
||||
? "/api/plugin/uninstall-failed"
|
||||
: "/api/plugin/uninstall";
|
||||
const payload = isFailed
|
||||
? { dir_name: target.id, delete_config: deleteConfig, delete_data: deleteData }
|
||||
: { name: target.id, delete_config: deleteConfig, delete_data: deleteData };
|
||||
|
||||
toast(`${tm("messages.uninstalling")} ${target.id}`, "primary");
|
||||
|
||||
try {
|
||||
const res = await axios.post(endpoint, payload);
|
||||
if (res.data.status === "error") {
|
||||
toast(res.data.message, "error");
|
||||
return;
|
||||
}
|
||||
if (!isFailed) {
|
||||
Object.assign(extension_data, res.data);
|
||||
}
|
||||
toast(res.data.message, "success");
|
||||
await getExtensions();
|
||||
} catch (err) {
|
||||
toast(resolveErrorMessage(err, tm("messages.operationFailed")), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const requestUninstallPlugin = (name) => {
|
||||
if (!name) return;
|
||||
uninstall({ kind: "normal", id: name }, { skipConfirm: false });
|
||||
};
|
||||
|
||||
const requestUninstallFailedPlugin = (dirName) => {
|
||||
if (!dirName) return;
|
||||
uninstall({ kind: "failed", id: dirName }, { skipConfirm: false });
|
||||
};
|
||||
|
||||
const checkUpdate = () => {
|
||||
const onlinePluginsMap = new Map();
|
||||
@@ -482,57 +635,34 @@ export const useExtensionPage = () => {
|
||||
};
|
||||
|
||||
const uninstallExtension = async (
|
||||
extension_name,
|
||||
extensionName,
|
||||
optionsOrSkipConfirm = false,
|
||||
) => {
|
||||
let deleteConfig = false;
|
||||
let deleteData = false;
|
||||
let skipConfirm = false;
|
||||
|
||||
// 处理参数:可能是布尔值(旧的 skipConfirm)或对象(新的选项)
|
||||
if (!extensionName) return;
|
||||
|
||||
if (typeof optionsOrSkipConfirm === "boolean") {
|
||||
skipConfirm = optionsOrSkipConfirm;
|
||||
} else if (
|
||||
typeof optionsOrSkipConfirm === "object" &&
|
||||
optionsOrSkipConfirm !== null
|
||||
) {
|
||||
deleteConfig = optionsOrSkipConfirm.deleteConfig || false;
|
||||
deleteData = optionsOrSkipConfirm.deleteData || false;
|
||||
skipConfirm = true; // 如果传递了选项对象,说明已经确认过了
|
||||
}
|
||||
|
||||
// 如果没有跳过确认且没有传递选项对象,显示自定义卸载对话框
|
||||
if (!skipConfirm) {
|
||||
pluginToUninstall.value = extension_name;
|
||||
showUninstallDialog.value = true;
|
||||
return; // 等待对话框回调
|
||||
}
|
||||
|
||||
// 执行卸载
|
||||
toast(tm("messages.uninstalling") + " " + extension_name, "primary");
|
||||
try {
|
||||
const res = await axios.post("/api/plugin/uninstall", {
|
||||
name: extension_name,
|
||||
delete_config: deleteConfig,
|
||||
delete_data: deleteData,
|
||||
});
|
||||
if (res.data.status === "error") {
|
||||
toast(res.data.message, "error");
|
||||
return;
|
||||
}
|
||||
Object.assign(extension_data, res.data);
|
||||
toast(res.data.message, "success");
|
||||
getExtensions();
|
||||
} catch (err) {
|
||||
toast(err, "error");
|
||||
return uninstall(
|
||||
{ kind: "normal", id: extensionName },
|
||||
{ skipConfirm: optionsOrSkipConfirm },
|
||||
);
|
||||
}
|
||||
|
||||
return uninstall(
|
||||
{ kind: "normal", id: extensionName },
|
||||
{ ...(optionsOrSkipConfirm || {}), skipConfirm: true },
|
||||
);
|
||||
};
|
||||
|
||||
// 处理卸载确认对话框的确认事件
|
||||
const handleUninstallConfirm = (options) => {
|
||||
if (pluginToUninstall.value) {
|
||||
uninstallExtension(pluginToUninstall.value, options);
|
||||
pluginToUninstall.value = null;
|
||||
const handleUninstallConfirm = async (options) => {
|
||||
const target = uninstallTarget.value;
|
||||
if (!target) return;
|
||||
|
||||
try {
|
||||
await uninstall(target, { ...(options || {}), skipConfirm: true });
|
||||
} finally {
|
||||
uninstallTarget.value = null;
|
||||
showUninstallDialog.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -738,15 +868,14 @@ export const useExtensionPage = () => {
|
||||
const reloadPlugin = async (plugin_name) => {
|
||||
try {
|
||||
const res = await axios.post("/api/plugin/reload", { name: plugin_name });
|
||||
await getExtensions();
|
||||
if (res.data.status === "error") {
|
||||
toast(res.data.message, "error");
|
||||
toast(res.data.message || tm("messages.reloadFailed"), "error");
|
||||
return;
|
||||
}
|
||||
toast(tm("messages.reloadSuccess"), "success");
|
||||
//getExtensions();
|
||||
await getExtensions();
|
||||
} catch (err) {
|
||||
toast(err, "error");
|
||||
toast(resolveErrorMessage(err, tm("messages.reloadFailed")), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1027,6 +1156,14 @@ export const useExtensionPage = () => {
|
||||
versionCompatibilityDialog.message = message;
|
||||
versionCompatibilityDialog.show = true;
|
||||
};
|
||||
|
||||
const refreshExtensionsAfterInstallFailure = async () => {
|
||||
try {
|
||||
await getExtensions();
|
||||
} catch (error) {
|
||||
console.debug("Failed to refresh extensions after install failure:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const continueInstallIgnoringVersionWarning = async () => {
|
||||
versionCompatibilityDialog.show = false;
|
||||
@@ -1036,6 +1173,68 @@ export const useExtensionPage = () => {
|
||||
const cancelInstallOnVersionWarning = () => {
|
||||
versionCompatibilityDialog.show = false;
|
||||
};
|
||||
|
||||
const handleInstallResponse = async (resData, { toastStatus = false } = {}) => {
|
||||
if (
|
||||
resData.status === "warning" &&
|
||||
resData.data?.warning_type === "astrbot_version_incompatible"
|
||||
) {
|
||||
onLoadingDialogResult(2, resData.message, -1);
|
||||
showVersionCompatibilityWarning(resData.message);
|
||||
await refreshExtensionsAfterInstallFailure();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toastStatus) {
|
||||
toast(resData.message, resData.status === "ok" ? "success" : "error");
|
||||
}
|
||||
|
||||
if (resData.status === "error") {
|
||||
onLoadingDialogResult(2, resData.message, -1);
|
||||
await refreshExtensionsAfterInstallFailure();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const performInstallRequest = async ({ source, ignoreVersionCheck }) => {
|
||||
if (source === "file") {
|
||||
const formData = new FormData();
|
||||
formData.append("file", upload_file.value);
|
||||
formData.append("ignore_version_check", String(ignoreVersionCheck));
|
||||
return axios.post("/api/plugin/install-upload", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return axios.post("/api/plugin/install", {
|
||||
url: extension_url.value,
|
||||
proxy: getSelectedGitHubProxy(),
|
||||
ignore_version_check: ignoreVersionCheck,
|
||||
});
|
||||
};
|
||||
|
||||
const finalizeSuccessfulInstall = async (resData, source) => {
|
||||
if (source === "file") {
|
||||
upload_file.value = null;
|
||||
} else {
|
||||
extension_url.value = "";
|
||||
}
|
||||
|
||||
onLoadingDialogResult(1, resData.message);
|
||||
dialog.value = false;
|
||||
await getExtensions();
|
||||
|
||||
viewReadme({
|
||||
name: resData.data.name,
|
||||
repo: resData.data.repo || null,
|
||||
});
|
||||
|
||||
await checkAndPromptConflicts();
|
||||
};
|
||||
|
||||
const newExtension = async (ignoreVersionCheck = false) => {
|
||||
if (extension_url.value === "" && upload_file.value === null) {
|
||||
@@ -1050,90 +1249,33 @@ export const useExtensionPage = () => {
|
||||
loading_.value = true;
|
||||
loadingDialog.title = tm("status.loading");
|
||||
loadingDialog.show = true;
|
||||
if (upload_file.value !== null) {
|
||||
toast(tm("messages.installing"), "primary");
|
||||
const formData = new FormData();
|
||||
formData.append("file", upload_file.value);
|
||||
formData.append("ignore_version_check", String(ignoreVersionCheck));
|
||||
axios
|
||||
.post("/api/plugin/install-upload", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
loading_.value = false;
|
||||
if (
|
||||
res.data.status === "warning" &&
|
||||
res.data.data?.warning_type === "astrbot_version_incompatible"
|
||||
) {
|
||||
onLoadingDialogResult(2, res.data.message, -1);
|
||||
showVersionCompatibilityWarning(res.data.message);
|
||||
return;
|
||||
}
|
||||
if (res.data.status === "error") {
|
||||
onLoadingDialogResult(2, res.data.message, -1);
|
||||
return;
|
||||
}
|
||||
upload_file.value = null;
|
||||
onLoadingDialogResult(1, res.data.message);
|
||||
dialog.value = false;
|
||||
await getExtensions();
|
||||
|
||||
viewReadme({
|
||||
name: res.data.data.name,
|
||||
repo: res.data.data.repo || null,
|
||||
});
|
||||
|
||||
await checkAndPromptConflicts();
|
||||
})
|
||||
.catch((err) => {
|
||||
loading_.value = false;
|
||||
onLoadingDialogResult(2, err, -1);
|
||||
});
|
||||
} else {
|
||||
toast(
|
||||
tm("messages.installingFromUrl") + " " + extension_url.value,
|
||||
"primary",
|
||||
);
|
||||
axios
|
||||
.post("/api/plugin/install", {
|
||||
url: extension_url.value,
|
||||
proxy: getSelectedGitHubProxy(),
|
||||
ignore_version_check: ignoreVersionCheck,
|
||||
})
|
||||
.then(async (res) => {
|
||||
loading_.value = false;
|
||||
if (
|
||||
res.data.status === "warning" &&
|
||||
res.data.data?.warning_type === "astrbot_version_incompatible"
|
||||
) {
|
||||
onLoadingDialogResult(2, res.data.message, -1);
|
||||
showVersionCompatibilityWarning(res.data.message);
|
||||
return;
|
||||
}
|
||||
toast(res.data.message, res.data.status === "ok" ? "success" : "error");
|
||||
if (res.data.status === "error") {
|
||||
onLoadingDialogResult(2, res.data.message, -1);
|
||||
return;
|
||||
}
|
||||
extension_url.value = "";
|
||||
onLoadingDialogResult(1, res.data.message);
|
||||
dialog.value = false;
|
||||
await getExtensions();
|
||||
|
||||
viewReadme({
|
||||
name: res.data.data.name,
|
||||
repo: res.data.data.repo || null,
|
||||
});
|
||||
|
||||
await checkAndPromptConflicts();
|
||||
})
|
||||
.catch((err) => {
|
||||
loading_.value = false;
|
||||
toast(tm("messages.installFailed") + " " + err, "error");
|
||||
onLoadingDialogResult(2, err, -1);
|
||||
});
|
||||
|
||||
const source = upload_file.value !== null ? "file" : "url";
|
||||
toast(
|
||||
source === "file"
|
||||
? tm("messages.installing")
|
||||
: tm("messages.installingFromUrl") + " " + extension_url.value,
|
||||
"primary",
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await performInstallRequest({ source, ignoreVersionCheck });
|
||||
loading_.value = false;
|
||||
|
||||
const canContinue = await handleInstallResponse(res.data, {
|
||||
toastStatus: source === "url",
|
||||
});
|
||||
if (!canContinue) return;
|
||||
|
||||
await finalizeSuccessfulInstall(res.data, source);
|
||||
} catch (err) {
|
||||
loading_.value = false;
|
||||
const message = resolveErrorMessage(err, tm("messages.installFailed"));
|
||||
if (source === "url") {
|
||||
toast(message, "error");
|
||||
}
|
||||
onLoadingDialogResult(2, message, -1);
|
||||
await refreshExtensionsAfterInstallFailure();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1371,7 +1513,7 @@ export const useExtensionPage = () => {
|
||||
installCompat,
|
||||
versionCompatibilityDialog,
|
||||
showUninstallDialog,
|
||||
pluginToUninstall,
|
||||
uninstallTarget,
|
||||
showSourceDialog,
|
||||
showSourceManagerDialog,
|
||||
sourceName,
|
||||
@@ -1393,6 +1535,7 @@ export const useExtensionPage = () => {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
@@ -1407,6 +1550,8 @@ export const useExtensionPage = () => {
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
@@ -1416,10 +1561,14 @@ export const useExtensionPage = () => {
|
||||
resetLoadingDialog,
|
||||
onLoadingDialogResult,
|
||||
failedPluginsDict,
|
||||
failedPluginItems,
|
||||
getExtensions,
|
||||
handleReloadAllFailed,
|
||||
reloadFailedPlugin,
|
||||
checkUpdate,
|
||||
uninstallExtension,
|
||||
requestUninstallPlugin,
|
||||
requestUninstallFailedPlugin,
|
||||
handleUninstallConfirm,
|
||||
updateExtension,
|
||||
showUpdateAllConfirm,
|
||||
|
||||
Reference in New Issue
Block a user