Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions astrbot/core/star/filter/platform_adapter_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -26,6 +27,7 @@ class PlatformAdapterType(enum.Flag):
| QQOFFICIAL
| TELEGRAM
| WECOM
| WECOM_AI_BOT
| LARK
| DINGTALK
| DISCORD
Expand All @@ -44,6 +46,7 @@ class PlatformAdapterType(enum.Flag):
"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,
Expand Down
6 changes: 6 additions & 0 deletions astrbot/core/star/star.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
108 changes: 103 additions & 5 deletions astrbot/core/star/star_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
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 (
Expand All @@ -40,6 +43,10 @@
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()
Expand Down Expand Up @@ -268,10 +275,58 @@ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | N
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,
Expand Down Expand Up @@ -408,7 +463,12 @@ async def reload(self, specified_plugin_name=None):

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 时,只载入指定的插件。

Expand Down Expand Up @@ -507,10 +567,25 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
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("/", "_")
Expand Down Expand Up @@ -621,6 +696,19 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
)
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
Expand Down Expand Up @@ -754,7 +842,9 @@ async def _cleanup_failed_plugin_install(
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 下载并安装插件,然后加载该插件到系统中
Expand Down Expand Up @@ -788,7 +878,10 @@ async def install_plugin(self, repo_url: str, proxy=""):

# 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
Expand Down Expand Up @@ -1092,7 +1185,9 @@ async def turn_on_plugin(self, plugin_name: str) -> None:

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)
Expand Down Expand Up @@ -1148,7 +1243,10 @@ async def install_plugin_from_file(self, zip_file_path: str):
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
Expand Down
65 changes: 61 additions & 4 deletions astrbot/dashboard/routes/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
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,
Expand Down Expand Up @@ -49,6 +52,7 @@ def __init__(
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),
Expand Down Expand Up @@ -81,6 +85,27 @@ def __init__(

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 (
Expand Down Expand Up @@ -121,7 +146,7 @@ async def reload_plugins(self):
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()}")
Expand Down Expand Up @@ -349,6 +374,8 @@ async def get_plugins(self):
),
"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(
Expand Down Expand Up @@ -443,17 +470,31 @@ async def install_plugin(self):

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:
proxy = proxy.removesuffix("/")

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__
Expand All @@ -469,16 +510,32 @@ async def install_plugin_upload(self):
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__
Expand Down
Loading