mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: add support for plugin astrbot-version and platform requirement checks (#5235)
* feat: add support for plugin astrbot-version and platform requirement checks * fix: remove unsupported platform and version constraints from metadata.yaml * fix: remove restriction on 'v' in astrbot_version specification format * ruff format
This commit is contained in:
@@ -11,6 +11,7 @@ class PlatformAdapterType(enum.Flag):
|
||||
QQOFFICIAL = enum.auto()
|
||||
TELEGRAM = enum.auto()
|
||||
WECOM = enum.auto()
|
||||
WECOM_AI_BOT = enum.auto()
|
||||
LARK = enum.auto()
|
||||
DINGTALK = enum.auto()
|
||||
DISCORD = enum.auto()
|
||||
@@ -26,6 +27,7 @@ class PlatformAdapterType(enum.Flag):
|
||||
| QQOFFICIAL
|
||||
| TELEGRAM
|
||||
| WECOM
|
||||
| WECOM_AI_BOT
|
||||
| LARK
|
||||
| DINGTALK
|
||||
| DISCORD
|
||||
@@ -44,6 +46,7 @@ ADAPTER_NAME_2_TYPE = {
|
||||
"qq_official": PlatformAdapterType.QQOFFICIAL,
|
||||
"telegram": PlatformAdapterType.TELEGRAM,
|
||||
"wecom": PlatformAdapterType.WECOM,
|
||||
"wecom_ai_bot": PlatformAdapterType.WECOM_AI_BOT,
|
||||
"lark": PlatformAdapterType.LARK,
|
||||
"dingtalk": PlatformAdapterType.DINGTALK,
|
||||
"discord": PlatformAdapterType.DISCORD,
|
||||
|
||||
@@ -61,6 +61,12 @@ class StarMetadata:
|
||||
logo_path: str | None = None
|
||||
"""插件 Logo 的路径"""
|
||||
|
||||
support_platforms: list[str] = field(default_factory=list)
|
||||
"""插件声明支持的平台适配器 ID 列表(对应 ADAPTER_NAME_2_TYPE 的 key)"""
|
||||
|
||||
astrbot_version: str | None = None
|
||||
"""插件要求的 AstrBot 版本范围(PEP 440 specifier,如 >=4.13.0,<4.17.0)"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@ import traceback
|
||||
from types import ModuleType
|
||||
|
||||
import yaml
|
||||
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
from astrbot.core import logger, pip_installer, sp
|
||||
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.platform.register import unregister_platform_adapters_by_module
|
||||
from astrbot.core.provider.register import llm_tools
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
@@ -40,6 +43,10 @@ except ImportError:
|
||||
logger.warning("未安装 watchfiles,无法实现插件的热重载。")
|
||||
|
||||
|
||||
class PluginVersionIncompatibleError(Exception):
|
||||
"""Raised when plugin astrbot_version is incompatible with current AstrBot."""
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, context: Context, config: AstrBotConfig) -> None:
|
||||
self.updator = PluginUpdator()
|
||||
@@ -268,10 +275,58 @@ class PluginManager:
|
||||
version=metadata["version"],
|
||||
repo=metadata["repo"] if "repo" in metadata else None,
|
||||
display_name=metadata.get("display_name", None),
|
||||
support_platforms=(
|
||||
[
|
||||
platform_id
|
||||
for platform_id in metadata["support_platforms"]
|
||||
if isinstance(platform_id, str)
|
||||
]
|
||||
if isinstance(metadata.get("support_platforms"), list)
|
||||
else []
|
||||
),
|
||||
astrbot_version=(
|
||||
metadata["astrbot_version"]
|
||||
if isinstance(metadata.get("astrbot_version"), str)
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
return metadata
|
||||
|
||||
@staticmethod
|
||||
def _validate_astrbot_version_specifier(
|
||||
version_spec: str | None,
|
||||
) -> tuple[bool, str | None]:
|
||||
if not version_spec:
|
||||
return True, None
|
||||
|
||||
normalized_spec = version_spec.strip()
|
||||
if not normalized_spec:
|
||||
return True, None
|
||||
|
||||
try:
|
||||
specifier = SpecifierSet(normalized_spec)
|
||||
except InvalidSpecifier:
|
||||
return (
|
||||
False,
|
||||
"astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5。",
|
||||
)
|
||||
|
||||
try:
|
||||
current_version = Version(VERSION)
|
||||
except InvalidVersion:
|
||||
return (
|
||||
False,
|
||||
f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。",
|
||||
)
|
||||
|
||||
if current_version not in specifier:
|
||||
return (
|
||||
False,
|
||||
f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}",
|
||||
)
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def _get_plugin_related_modules(
|
||||
plugin_root_dir: str,
|
||||
@@ -408,7 +463,12 @@ class PluginManager:
|
||||
|
||||
return result
|
||||
|
||||
async def load(self, specified_module_path=None, specified_dir_name=None):
|
||||
async def load(
|
||||
self,
|
||||
specified_module_path=None,
|
||||
specified_dir_name=None,
|
||||
ignore_version_check: bool = False,
|
||||
):
|
||||
"""载入插件。
|
||||
当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。
|
||||
|
||||
@@ -507,10 +567,25 @@ class PluginManager:
|
||||
metadata.version = metadata_yaml.version
|
||||
metadata.repo = metadata_yaml.repo
|
||||
metadata.display_name = metadata_yaml.display_name
|
||||
metadata.support_platforms = metadata_yaml.support_platforms
|
||||
metadata.astrbot_version = metadata_yaml.astrbot_version
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。",
|
||||
)
|
||||
|
||||
if not ignore_version_check:
|
||||
is_valid, error_message = (
|
||||
self._validate_astrbot_version_specifier(
|
||||
metadata.astrbot_version,
|
||||
)
|
||||
)
|
||||
if not is_valid:
|
||||
raise PluginVersionIncompatibleError(
|
||||
error_message
|
||||
or "The plugin is not compatible with the current AstrBot version."
|
||||
)
|
||||
|
||||
logger.info(metadata)
|
||||
metadata.config = plugin_config
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
@@ -621,6 +696,19 @@ class PluginManager:
|
||||
)
|
||||
if not metadata:
|
||||
raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。")
|
||||
|
||||
if not ignore_version_check:
|
||||
is_valid, error_message = (
|
||||
self._validate_astrbot_version_specifier(
|
||||
metadata.astrbot_version,
|
||||
)
|
||||
)
|
||||
if not is_valid:
|
||||
raise PluginVersionIncompatibleError(
|
||||
error_message
|
||||
or "The plugin is not compatible with the current AstrBot version."
|
||||
)
|
||||
|
||||
metadata.star_cls = obj
|
||||
metadata.config = plugin_config
|
||||
metadata.module = module
|
||||
@@ -754,7 +842,9 @@ class PluginManager:
|
||||
f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}",
|
||||
)
|
||||
|
||||
async def install_plugin(self, repo_url: str, proxy=""):
|
||||
async def install_plugin(
|
||||
self, repo_url: str, proxy: str = "", ignore_version_check: bool = False
|
||||
):
|
||||
"""从仓库 URL 安装插件
|
||||
|
||||
从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中
|
||||
@@ -788,7 +878,10 @@ class PluginManager:
|
||||
|
||||
# reload the plugin
|
||||
dir_name = os.path.basename(plugin_path)
|
||||
success, error_message = await self.load(specified_dir_name=dir_name)
|
||||
success, error_message = await self.load(
|
||||
specified_dir_name=dir_name,
|
||||
ignore_version_check=ignore_version_check,
|
||||
)
|
||||
if not success:
|
||||
raise Exception(
|
||||
error_message
|
||||
@@ -1092,7 +1185,9 @@ class PluginManager:
|
||||
|
||||
await self.reload(plugin_name)
|
||||
|
||||
async def install_plugin_from_file(self, zip_file_path: str):
|
||||
async def install_plugin_from_file(
|
||||
self, zip_file_path: str, ignore_version_check: bool = False
|
||||
):
|
||||
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)
|
||||
@@ -1148,7 +1243,10 @@ class PluginManager:
|
||||
except BaseException as e:
|
||||
logger.warning(f"删除插件压缩包失败: {e!s}")
|
||||
# await self.reload()
|
||||
success, error_message = await self.load(specified_dir_name=dir_name)
|
||||
success, error_message = await self.load(
|
||||
specified_dir_name=dir_name,
|
||||
ignore_version_check=ignore_version_check,
|
||||
)
|
||||
if not success:
|
||||
raise Exception(
|
||||
error_message
|
||||
|
||||
@@ -19,7 +19,10 @@ from astrbot.core.star.filter.command_group import CommandGroupFilter
|
||||
from astrbot.core.star.filter.permission import PermissionTypeFilter
|
||||
from astrbot.core.star.filter.regex import RegexFilter
|
||||
from astrbot.core.star.star_handler import EventType, star_handlers_registry
|
||||
from astrbot.core.star.star_manager import PluginManager
|
||||
from astrbot.core.star.star_manager import (
|
||||
PluginManager,
|
||||
PluginVersionIncompatibleError,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_data_path,
|
||||
get_astrbot_temp_path,
|
||||
@@ -49,6 +52,7 @@ class PluginRoute(Route):
|
||||
super().__init__(context)
|
||||
self.routes = {
|
||||
"/plugin/get": ("GET", self.get_plugins),
|
||||
"/plugin/check-compat": ("POST", self.check_plugin_compatibility),
|
||||
"/plugin/install": ("POST", self.install_plugin),
|
||||
"/plugin/install-upload": ("POST", self.install_plugin_upload),
|
||||
"/plugin/update": ("POST", self.update_plugin),
|
||||
@@ -81,6 +85,27 @@ class PluginRoute(Route):
|
||||
|
||||
self._logo_cache = {}
|
||||
|
||||
async def check_plugin_compatibility(self):
|
||||
try:
|
||||
data = await request.get_json()
|
||||
version_spec = data.get("astrbot_version", "")
|
||||
is_valid, message = self.plugin_manager._validate_astrbot_version_specifier(
|
||||
version_spec
|
||||
)
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"compatible": is_valid,
|
||||
"message": message,
|
||||
"astrbot_version": version_spec,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def reload_failed_plugins(self):
|
||||
if DEMO_MODE:
|
||||
return (
|
||||
@@ -121,7 +146,7 @@ class PluginRoute(Route):
|
||||
try:
|
||||
success, message = await self.plugin_manager.reload(plugin_name)
|
||||
if not success:
|
||||
return Response().error(message).__dict__
|
||||
return Response().error(message or "插件重载失败").__dict__
|
||||
return Response().ok(None, "重载成功。").__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"/api/plugin/reload: {traceback.format_exc()}")
|
||||
@@ -349,6 +374,8 @@ class PluginRoute(Route):
|
||||
),
|
||||
"display_name": plugin.display_name,
|
||||
"logo": f"/api/file/{logo_url}" if logo_url else None,
|
||||
"support_platforms": plugin.support_platforms,
|
||||
"astrbot_version": plugin.astrbot_version,
|
||||
}
|
||||
# 检查是否为全空的幽灵插件
|
||||
if not any(
|
||||
@@ -443,6 +470,7 @@ class PluginRoute(Route):
|
||||
|
||||
post_data = await request.get_json()
|
||||
repo_url = post_data["url"]
|
||||
ignore_version_check = bool(post_data.get("ignore_version_check", False))
|
||||
|
||||
proxy: str = post_data.get("proxy", None)
|
||||
if proxy:
|
||||
@@ -450,10 +478,23 @@ class PluginRoute(Route):
|
||||
|
||||
try:
|
||||
logger.info(f"正在安装插件 {repo_url}")
|
||||
plugin_info = await self.plugin_manager.install_plugin(repo_url, proxy)
|
||||
plugin_info = await self.plugin_manager.install_plugin(
|
||||
repo_url,
|
||||
proxy,
|
||||
ignore_version_check=ignore_version_check,
|
||||
)
|
||||
# self.core_lifecycle.restart()
|
||||
logger.info(f"安装插件 {repo_url} 成功。")
|
||||
return Response().ok(plugin_info, "安装成功。").__dict__
|
||||
except PluginVersionIncompatibleError as e:
|
||||
return {
|
||||
"status": "warning",
|
||||
"message": str(e),
|
||||
"data": {
|
||||
"warning_type": "astrbot_version_incompatible",
|
||||
"can_ignore": True,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
@@ -469,16 +510,32 @@ class PluginRoute(Route):
|
||||
try:
|
||||
file = await request.files
|
||||
file = file["file"]
|
||||
form_data = await request.form
|
||||
ignore_version_check = (
|
||||
str(form_data.get("ignore_version_check", "false")).lower() == "true"
|
||||
)
|
||||
logger.info(f"正在安装用户上传的插件 {file.filename}")
|
||||
file_path = os.path.join(
|
||||
get_astrbot_temp_path(),
|
||||
f"plugin_upload_{file.filename}",
|
||||
)
|
||||
await file.save(file_path)
|
||||
plugin_info = await self.plugin_manager.install_plugin_from_file(file_path)
|
||||
plugin_info = await self.plugin_manager.install_plugin_from_file(
|
||||
file_path,
|
||||
ignore_version_check=ignore_version_check,
|
||||
)
|
||||
# self.core_lifecycle.restart()
|
||||
logger.info(f"安装插件 {file.filename} 成功")
|
||||
return Response().ok(plugin_info, "安装成功。").__dict__
|
||||
except PluginVersionIncompatibleError as e:
|
||||
return {
|
||||
"status": "warning",
|
||||
"message": str(e),
|
||||
"data": {
|
||||
"warning_type": "astrbot_version_incompatible",
|
||||
"can_ignore": True,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { getPlatformDisplayName } from "@/utils/platformUtils";
|
||||
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
|
||||
@@ -20,6 +21,17 @@ defineProps({
|
||||
|
||||
const emit = defineEmits(["install"]);
|
||||
|
||||
const normalizePlatformList = (platforms) => {
|
||||
if (!Array.isArray(platforms)) return [];
|
||||
return platforms.filter((item) => typeof item === "string");
|
||||
};
|
||||
|
||||
const getPlatformDisplayList = (platforms) => {
|
||||
return normalizePlatformList(platforms).map((platformId) =>
|
||||
getPlatformDisplayName(platformId),
|
||||
);
|
||||
};
|
||||
|
||||
const handleInstall = (plugin) => {
|
||||
emit("install", plugin);
|
||||
};
|
||||
@@ -29,7 +41,7 @@ const handleInstall = (plugin) => {
|
||||
<v-card
|
||||
class="rounded-lg d-flex flex-column plugin-card"
|
||||
elevation="0"
|
||||
style="height: 12rem; position: relative"
|
||||
style="height: 13rem; position: relative"
|
||||
>
|
||||
<v-chip
|
||||
v-if="plugin?.pinned"
|
||||
@@ -152,6 +164,42 @@ const handleInstall = (plugin) => {
|
||||
{{ plugin.desc }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="plugin.astrbot_version || normalizePlatformList(plugin.support_platforms).length"
|
||||
class="d-flex align-center flex-wrap"
|
||||
style="gap: 4px; margin-top: 4px; margin-bottom: 4px;"
|
||||
>
|
||||
<v-chip
|
||||
v-if="plugin.astrbot_version"
|
||||
size="x-small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style="height: 20px"
|
||||
>
|
||||
AstrBot: {{ plugin.astrbot_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="normalizePlatformList(plugin.support_platforms).length"
|
||||
size="x-small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
style="height: 20px"
|
||||
>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<span v-bind="tooltipProps">
|
||||
{{
|
||||
tm("card.status.supportPlatformsCount", {
|
||||
count: getPlatformDisplayList(plugin.support_platforms).length,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ getPlatformDisplayList(plugin.support_platforms).join(", ") }}</span>
|
||||
</v-tooltip>
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="gap: 8px; margin-top: auto">
|
||||
<div
|
||||
v-if="plugin.stars !== undefined"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { getPlatformDisplayName } from "@/utils/platformUtils";
|
||||
import UninstallConfirmDialog from "./UninstallConfirmDialog.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -38,6 +39,25 @@ const showUninstallDialog = ref(false);
|
||||
// 国际化
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
|
||||
const supportPlatforms = computed(() => {
|
||||
const platforms = props.extension?.support_platforms;
|
||||
if (!Array.isArray(platforms)) {
|
||||
return [];
|
||||
}
|
||||
return platforms.filter((item) => typeof item === "string");
|
||||
});
|
||||
|
||||
const supportPlatformDisplayNames = computed(() =>
|
||||
supportPlatforms.value.map((platformId) => getPlatformDisplayName(platformId)),
|
||||
);
|
||||
|
||||
const astrbotVersionRequirement = computed(() => {
|
||||
const versionSpec = props.extension?.astrbot_version;
|
||||
return typeof versionSpec === "string" && versionSpec.trim().length
|
||||
? versionSpec.trim()
|
||||
: "";
|
||||
});
|
||||
|
||||
// 操作函数
|
||||
const configure = () => {
|
||||
emit("configure", props.extension);
|
||||
@@ -316,6 +336,37 @@ const viewChangelog = () => {
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="supportPlatforms.length"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
label
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<span v-bind="tooltipProps">
|
||||
{{
|
||||
tm("card.status.supportPlatformsCount", {
|
||||
count: supportPlatformDisplayNames.length,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ supportPlatformDisplayNames.join(", ") }}</span>
|
||||
</v-tooltip>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="astrbotVersionRequirement"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
label
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
AstrBot: {{ astrbotVersionRequirement }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -143,7 +143,8 @@
|
||||
"install": {
|
||||
"title": "Install Extension",
|
||||
"fromFile": "Install from File",
|
||||
"fromUrl": "Install from URL"
|
||||
"fromUrl": "Install from URL",
|
||||
"supportPlatformsCount": "Supports {count} Platforms"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "Dangerous Plugin Warning",
|
||||
@@ -151,6 +152,12 @@
|
||||
"confirm": "Continue",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"versionCompatibility": {
|
||||
"title": "Version Compatibility Warning",
|
||||
"message": "This plugin declares an AstrBot version range that does not match your current version. You can ignore this warning and continue installation, but it may not work correctly.",
|
||||
"confirm": "Ignore Warning and Install",
|
||||
"cancel": "Cancel Installation"
|
||||
},
|
||||
"forceUpdate": {
|
||||
"title": "No New Version Detected",
|
||||
"message": "No new version detected for this plugin. Do you want to force reinstall? This will pull the latest code from the remote repository.",
|
||||
@@ -230,7 +237,10 @@
|
||||
"status": {
|
||||
"hasUpdate": "New version available",
|
||||
"disabled": "This extension is disabled",
|
||||
"handlersCount": " handlers"
|
||||
"handlersCount": " handlers",
|
||||
"supportPlatform": "Supported Platform",
|
||||
"supportPlatformsCount": "Supports {count} Platforms",
|
||||
"astrbotVersion": "AstrBot Version Requirement"
|
||||
},
|
||||
"alt": {
|
||||
"logo": "logo",
|
||||
|
||||
@@ -143,7 +143,8 @@
|
||||
"install": {
|
||||
"title": "安装插件",
|
||||
"fromFile": "从文件安装",
|
||||
"fromUrl": "从链接安装"
|
||||
"fromUrl": "从链接安装",
|
||||
"supportPlatformsCount": "支持 {count} 个平台"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "警告",
|
||||
@@ -151,6 +152,12 @@
|
||||
"confirm": "继续",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"versionCompatibility": {
|
||||
"title": "版本兼容性警告",
|
||||
"message": "该插件声明的 AstrBot 版本范围与当前版本不匹配。你可以无视警告继续安装,但可能无法正常运行。",
|
||||
"confirm": "无视警告,继续安装",
|
||||
"cancel": "取消安装"
|
||||
},
|
||||
"forceUpdate": {
|
||||
"title": "未检测到新版本",
|
||||
"message": "当前插件未检测到新版本,是否强制重新安装?这将从远程仓库拉取最新代码。",
|
||||
@@ -230,7 +237,10 @@
|
||||
"status": {
|
||||
"hasUpdate": "有新版本可用",
|
||||
"disabled": "该插件已经被禁用",
|
||||
"handlersCount": "个行为"
|
||||
"handlersCount": "个行为",
|
||||
"supportPlatform": "支持平台",
|
||||
"supportPlatformsCount": "支持 {count} 个平台",
|
||||
"astrbotVersion": "AstrBot 版本要求"
|
||||
},
|
||||
"alt": {
|
||||
"logo": "logo",
|
||||
|
||||
@@ -177,6 +177,14 @@ export const useCommonStore = defineStore({
|
||||
"stars": pluginData?.stars ? pluginData.stars : 0,
|
||||
"updated_at": pluginData?.updated_at ? pluginData.updated_at : "",
|
||||
"display_name": pluginData?.display_name ? pluginData.display_name : "",
|
||||
"astrbot_version": pluginData?.astrbot_version ? pluginData.astrbot_version : "",
|
||||
"support_platforms": Array.isArray(pluginData?.support_platforms)
|
||||
? pluginData.support_platforms
|
||||
: Array.isArray(pluginData?.support_platform)
|
||||
? pluginData.support_platform
|
||||
: Array.isArray(pluginData?.platform)
|
||||
? pluginData.platform
|
||||
: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,3 +80,29 @@ export function getPlatformDescription(template, name) {
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台展示名(用于插件支持平台显示)
|
||||
* @param {string} platformId - 平台适配器 ID
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getPlatformDisplayName(platformId) {
|
||||
const displayNameMap = {
|
||||
aiocqhttp: 'aiocqhttp (OneBot v11)',
|
||||
qq_official: 'qq_official (QQ 官方机器人平台)',
|
||||
weixin_official_account: 'weixin_official_account (微信公众号)',
|
||||
wecom: 'wecom (企业微信应用)',
|
||||
wecom_ai_bot: 'wecom_ai_bot (企业微信智能机器人)',
|
||||
lark: 'lark (飞书)',
|
||||
dingtalk: 'dingtalk (钉钉)',
|
||||
telegram: 'telegram (Telegram)',
|
||||
discord: 'discord (Discord)',
|
||||
misskey: 'misskey (Misskey)',
|
||||
slack: 'slack (Slack)',
|
||||
kook: 'kook (KOOK)',
|
||||
vocechat: 'vocechat (VoceChat)',
|
||||
satori: 'satori (Satori)',
|
||||
line: 'line (LINE)',
|
||||
};
|
||||
return displayNameMap[platformId] || platformId;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
@@ -149,6 +150,18 @@ const currentPage = ref(1);
|
||||
// 危险插件确认对话框
|
||||
const dangerConfirmDialog = ref(false);
|
||||
const selectedDangerPlugin = ref(null);
|
||||
const selectedMarketInstallPlugin = ref(null);
|
||||
const installCompat = reactive({
|
||||
checked: false,
|
||||
compatible: true,
|
||||
message: "",
|
||||
});
|
||||
|
||||
// AstrBot 版本范围不兼容警告对话框
|
||||
const versionCompatibilityDialog = reactive({
|
||||
show: false,
|
||||
message: "",
|
||||
});
|
||||
|
||||
// 卸载插件确认对话框(列表模式用)
|
||||
const showUninstallDialog = ref(false);
|
||||
@@ -250,10 +263,16 @@ const filteredPlugins = computed(() => {
|
||||
|
||||
const search = pluginSearch.value.toLowerCase();
|
||||
return filteredExtensions.value.filter((plugin) => {
|
||||
const supportPlatforms = Array.isArray(plugin.support_platforms)
|
||||
? plugin.support_platforms.join(" ").toLowerCase()
|
||||
: "";
|
||||
const astrbotVersion = (plugin.astrbot_version ?? "").toLowerCase();
|
||||
return (
|
||||
plugin.name?.toLowerCase().includes(search) ||
|
||||
plugin.desc?.toLowerCase().includes(search) ||
|
||||
plugin.author?.toLowerCase().includes(search)
|
||||
plugin.author?.toLowerCase().includes(search) ||
|
||||
supportPlatforms.includes(search) ||
|
||||
astrbotVersion.includes(search)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -758,6 +777,7 @@ const handleInstallPlugin = async (plugin) => {
|
||||
selectedDangerPlugin.value = plugin;
|
||||
dangerConfirmDialog.value = true;
|
||||
} else {
|
||||
selectedMarketInstallPlugin.value = plugin;
|
||||
extension_url.value = plugin.repo;
|
||||
dialog.value = true;
|
||||
uploadTab.value = "url";
|
||||
@@ -767,6 +787,7 @@ const handleInstallPlugin = async (plugin) => {
|
||||
// 确认安装危险插件
|
||||
const confirmDangerInstall = () => {
|
||||
if (selectedDangerPlugin.value) {
|
||||
selectedMarketInstallPlugin.value = selectedDangerPlugin.value;
|
||||
extension_url.value = selectedDangerPlugin.value.repo;
|
||||
dialog.value = true;
|
||||
uploadTab.value = "url";
|
||||
@@ -958,9 +979,33 @@ const checkAlreadyInstalled = () => {
|
||||
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
|
||||
const installedRepos = new Set(data.map((ext) => ext.repo?.toLowerCase()));
|
||||
const installedNames = new Set(data.map((ext) => ext.name));
|
||||
const installedByRepo = new Map(
|
||||
data
|
||||
.filter((ext) => ext.repo)
|
||||
.map((ext) => [ext.repo.toLowerCase(), ext]),
|
||||
);
|
||||
const installedByName = new Map(data.map((ext) => [ext.name, ext]));
|
||||
|
||||
for (let i = 0; i < pluginMarketData.value.length; i++) {
|
||||
const plugin = pluginMarketData.value[i];
|
||||
const matchedInstalled =
|
||||
(plugin.repo && installedByRepo.get(plugin.repo.toLowerCase())) ||
|
||||
installedByName.get(plugin.name);
|
||||
|
||||
// 兜底:市场源未提供字段时,回填本地已安装插件中的元数据,便于在市场页直接展示
|
||||
if (matchedInstalled) {
|
||||
if (
|
||||
(!Array.isArray(plugin.support_platforms) ||
|
||||
plugin.support_platforms.length === 0) &&
|
||||
Array.isArray(matchedInstalled.support_platforms)
|
||||
) {
|
||||
plugin.support_platforms = matchedInstalled.support_platforms;
|
||||
}
|
||||
if (!plugin.astrbot_version && matchedInstalled.astrbot_version) {
|
||||
plugin.astrbot_version = matchedInstalled.astrbot_version;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.installed =
|
||||
installedRepos.has(plugin.repo?.toLowerCase()) ||
|
||||
installedNames.has(plugin.name);
|
||||
@@ -978,7 +1023,21 @@ const checkAlreadyInstalled = () => {
|
||||
pluginMarketData.value = notInstalled.concat(installed);
|
||||
};
|
||||
|
||||
const newExtension = async () => {
|
||||
const showVersionCompatibilityWarning = (message) => {
|
||||
versionCompatibilityDialog.message = message;
|
||||
versionCompatibilityDialog.show = true;
|
||||
};
|
||||
|
||||
const continueInstallIgnoringVersionWarning = async () => {
|
||||
versionCompatibilityDialog.show = false;
|
||||
await newExtension(true);
|
||||
};
|
||||
|
||||
const cancelInstallOnVersionWarning = () => {
|
||||
versionCompatibilityDialog.show = false;
|
||||
};
|
||||
|
||||
const newExtension = async (ignoreVersionCheck = false) => {
|
||||
if (extension_url.value === "" && upload_file.value === null) {
|
||||
toast(tm("messages.fillUrlOrFile"), "error");
|
||||
return;
|
||||
@@ -995,6 +1054,7 @@ const newExtension = async () => {
|
||||
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: {
|
||||
@@ -1003,6 +1063,14 @@ const newExtension = async () => {
|
||||
})
|
||||
.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;
|
||||
@@ -1032,9 +1100,18 @@ const newExtension = async () => {
|
||||
.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);
|
||||
@@ -1060,6 +1137,53 @@ const newExtension = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const normalizePlatformList = (platforms) => {
|
||||
if (!Array.isArray(platforms)) return [];
|
||||
return platforms.filter((item) => typeof item === "string");
|
||||
};
|
||||
|
||||
const getPlatformDisplayList = (platforms) => {
|
||||
return normalizePlatformList(platforms).map((platformId) =>
|
||||
getPlatformDisplayName(platformId),
|
||||
);
|
||||
};
|
||||
|
||||
const resolveSelectedInstallPlugin = () => {
|
||||
if (
|
||||
selectedMarketInstallPlugin.value &&
|
||||
selectedMarketInstallPlugin.value.repo === extension_url.value
|
||||
) {
|
||||
return selectedMarketInstallPlugin.value;
|
||||
}
|
||||
return pluginMarketData.value.find((plugin) => plugin.repo === extension_url.value) || null;
|
||||
};
|
||||
|
||||
const selectedInstallPlugin = computed(() => resolveSelectedInstallPlugin());
|
||||
|
||||
const checkInstallCompatibility = async () => {
|
||||
installCompat.checked = false;
|
||||
installCompat.compatible = true;
|
||||
installCompat.message = "";
|
||||
|
||||
const plugin = selectedInstallPlugin.value;
|
||||
if (!plugin?.astrbot_version || uploadTab.value !== "url") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post("/api/plugin/check-compat", {
|
||||
astrbot_version: plugin.astrbot_version,
|
||||
});
|
||||
if (res.data.status === "ok") {
|
||||
installCompat.checked = true;
|
||||
installCompat.compatible = !!res.data.data?.compatible;
|
||||
installCompat.message = res.data.data?.message || "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug("Failed to check plugin compatibility:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新插件市场数据
|
||||
const refreshPluginMarket = async () => {
|
||||
refreshingMarket.value = true;
|
||||
@@ -1165,6 +1289,19 @@ watch(isListView, (newVal) => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => dialog.value, () => extension_url.value, () => uploadTab.value],
|
||||
async ([dialogOpen, _, currentUploadTab]) => {
|
||||
if (!dialogOpen || currentUploadTab !== "url") {
|
||||
installCompat.checked = false;
|
||||
installCompat.compatible = true;
|
||||
installCompat.message = "";
|
||||
return;
|
||||
}
|
||||
await checkInstallCompatibility();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
@@ -1451,18 +1588,54 @@ watch(activeTab, (newTab) => {
|
||||
</template>
|
||||
|
||||
<template v-slot:item.desc="{ item }">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis mt-2 mb-2"
|
||||
style="
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
>
|
||||
{{ item.desc }}
|
||||
<div class="py-2">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
style="
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
>
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.support_platforms?.length"
|
||||
class="d-flex align-center flex-wrap mt-2"
|
||||
>
|
||||
<span class="text-caption text-medium-emphasis mr-2">
|
||||
{{ tm("card.status.supportPlatform") }}:
|
||||
</span>
|
||||
<v-chip
|
||||
v-for="platformId in item.support_platforms"
|
||||
:key="platformId"
|
||||
size="x-small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ platformId }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.astrbot_version"
|
||||
class="d-flex align-center flex-wrap mt-1"
|
||||
>
|
||||
<span class="text-caption text-medium-emphasis mr-2">
|
||||
{{ tm("card.status.astrbotVersion") }}:
|
||||
</span>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ item.astrbot_version }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2357,6 +2530,31 @@ watch(activeTab, (newTab) => {
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 版本不兼容警告对话框 -->
|
||||
<v-dialog v-model="versionCompatibilityDialog.show" width="520" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 d-flex align-center">
|
||||
<v-icon color="warning" class="mr-2">mdi-alert</v-icon>
|
||||
{{ tm("dialogs.versionCompatibility.title") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="mb-2">{{ tm("dialogs.versionCompatibility.message") }}</div>
|
||||
<div class="text-medium-emphasis">
|
||||
{{ versionCompatibilityDialog.message }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" @click="cancelInstallOnVersionWarning">
|
||||
{{ tm("dialogs.versionCompatibility.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="warning" @click="continueInstallIgnoringVersionWarning">
|
||||
{{ tm("dialogs.versionCompatibility.confirm") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 上传插件对话框 -->
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<div
|
||||
@@ -2438,6 +2636,46 @@ watch(activeTab, (newTab) => {
|
||||
placeholder="https://github.com/username/repo"
|
||||
></v-text-field>
|
||||
|
||||
<div v-if="selectedInstallPlugin" class="mb-3">
|
||||
<v-chip
|
||||
v-if="selectedInstallPlugin.astrbot_version"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
{{ tm("card.status.astrbotVersion") }}:
|
||||
{{ selectedInstallPlugin.astrbot_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="normalizePlatformList(selectedInstallPlugin.support_platforms).length"
|
||||
size="small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ tm("card.status.supportPlatform") }}:
|
||||
{{
|
||||
getPlatformDisplayList(selectedInstallPlugin.support_platforms).join(
|
||||
", ",
|
||||
)
|
||||
}}
|
||||
</v-chip>
|
||||
<v-alert
|
||||
v-if="
|
||||
selectedInstallPlugin.astrbot_version &&
|
||||
installCompat.checked &&
|
||||
!installCompat.compatible
|
||||
"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ installCompat.message }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<ProxySelector></ProxySelector>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
@@ -62,6 +62,7 @@ dependencies = [
|
||||
"tenacity>=9.1.2",
|
||||
"shipyard-python-sdk>=0.2.4",
|
||||
"python-socks>=2.8.0",
|
||||
"packaging>=24.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -54,3 +54,4 @@ markitdown-no-magika[docx,xls,xlsx]>=0.1.2
|
||||
xinference-client
|
||||
tenacity>=9.1.2
|
||||
shipyard-python-sdk>=0.2.4
|
||||
packaging>=24.2
|
||||
|
||||
Reference in New Issue
Block a user