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:
Soulter
2026-02-20 13:35:45 +08:00
committed by GitHub
parent e469178a6b
commit 9a7a594cb5
13 changed files with 585 additions and 28 deletions

View File

@@ -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,

View File

@@ -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}"

View File

@@ -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

View File

@@ -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__

View File

@@ -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"

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
: [],
})
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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]

View File

@@ -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