refactor(plugin_system): 引入 PluginMetadata 替代 manifest.json

将插件元数据定义从外部 `_manifest.json` 文件迁移到插件 `__init__.py` 文件中的 `__plugin_meta__` 变量。此举简化了插件加载流程,减少了文件I/O,并使元数据与插件代码更紧密地耦合。

主要变更:
- 引入 `PluginMetadata` 数据类来标准化插件元数据。
- 插件基类 `PluginBase` 现在直接接收 `PluginMetadata` 对象,不再负责解析 JSON 文件。
- 插件管理器 `PluginManager` 调整加载逻辑,从插件模块的 `__plugin_meta__` 属性获取元数据。
- 删除了 `manifest_utils.py` 及其相关的验证和版本比较逻辑,简化了依赖关系。
- 更新了所有内置插件,以采用新的元数据定义方式,并删除了它们各自的 `_manifest.json` 文件。

BREAKING CHANGE: 插件加载机制已改变。所有插件必须在其 `__init__.py` 中定义一个 `__plugin_meta__` 变量,该变量是 `PluginMetadata` 类的实例,并移除旧的 `_manifest.json` 文件。
This commit is contained in:
minecraft1024a
2025-10-04 16:17:03 +08:00
parent 46d6acfdcc
commit 3764b3a8a6
28 changed files with 281 additions and 1061 deletions

View File

@@ -1,7 +1,10 @@
#!/usr/bin/env python3
"""
Bilibili 插件包
提供B站视频观看体验功能像真实用户一样浏览和评价视频
"""
from src.plugin_system.base.plugin_metadata import PluginMetadata
# 插件会通过 @register_plugin 装饰器自动注册,这里不需要额外的导入
__plugin_meta__ = PluginMetadata(
name="Bilibili Plugin",
description="A plugin for interacting with Bilibili.",
usage="Usage details for Bilibili plugin.",
version="1.0.0",
author="Your Name",
license="MIT",
)

View File

@@ -0,0 +1,10 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="Echo Example Plugin",
description="An example plugin that echoes messages.",
usage="!echo [message]",
version="1.0.0",
author="Your Name",
license="MIT",
)

View File

@@ -0,0 +1,10 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="Hello World Plugin",
description="A simple hello world plugin.",
usage="!hello",
version="1.0.0",
author="Your Name",
license="MIT",
)

View File

@@ -50,13 +50,6 @@ from .base import (
create_plus_command_adapter,
)
# 导入工具模块
from .utils import (
ManifestValidator,
# ManifestGenerator,
# validate_plugin_manifest,
# generate_plugin_manifest,
)
from .utils.dependency_config import configure_dependency_settings, get_dependency_config
# 导入依赖管理模块

View File

@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any
import orjson
import toml
from src.common.logger import get_logger
@@ -15,7 +14,7 @@ from src.plugin_system.base.component_types import (
PythonDependency,
)
from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.utils.manifest_utils import ManifestValidator
from src.plugin_system.base.plugin_metadata import PluginMetadata
logger = get_logger("plugin_base")
@@ -33,39 +32,34 @@ class PluginBase(ABC):
dependencies: list[str] = []
python_dependencies: list[str | PythonDependency] = []
# manifest文件相关
manifest_file_name: str = "_manifest.json" # manifest文件名
manifest_data: dict[str, Any] = {} # manifest数据
config_schema: dict[str, dict[str, ConfigField] | str] = {}
config_section_descriptions: dict[str, str] = {}
def __init__(self, plugin_dir: str):
def __init__(self, plugin_dir: str, metadata: PluginMetadata):
"""初始化插件
Args:
plugin_dir: 插件目录路径,由插件管理器传递
metadata: 插件元数据对象
"""
self.config: dict[str, Any] = {} # 插件配置
self.plugin_dir = plugin_dir # 插件目录路径
self.plugin_meta = metadata # 插件元数据
self.log_prefix = f"[Plugin:{self.plugin_name}]"
self._is_enabled = self.enable_plugin # 从插件定义中获取默认启用状态
# 加载manifest文件
self._load_manifest()
# 验证插件信息
self._validate_plugin_info()
# 加载插件配置
self._load_plugin_config()
# 从manifest获取显示信息
self.display_name = self.get_manifest_info("name", self.plugin_name)
self.plugin_version = self.get_manifest_info("version", "1.0.0")
self.plugin_description = self.get_manifest_info("description", "")
self.plugin_author = self._get_author_name()
# 从元数据获取显示信息
self.display_name = self.plugin_meta.name
self.plugin_version = self.plugin_meta.version
self.plugin_description = self.plugin_meta.description
self.plugin_author = self.plugin_meta.author
# 标准化Python依赖为PythonDependency对象
normalized_python_deps = self._normalize_python_dependencies(self.python_dependencies)
@@ -85,15 +79,6 @@ class PluginBase(ABC):
config_file=self.config_file_name or "",
dependencies=self.dependencies.copy(),
python_dependencies=normalized_python_deps,
# manifest相关信息
manifest_data=self.manifest_data.copy(),
license=self.get_manifest_info("license", ""),
homepage_url=self.get_manifest_info("homepage_url", ""),
repository_url=self.get_manifest_info("repository_url", ""),
keywords=self.get_manifest_info("keywords", []).copy() if self.get_manifest_info("keywords") else [],
categories=self.get_manifest_info("categories", []).copy() if self.get_manifest_info("categories") else [],
min_host_version=self.get_manifest_info("host_application.min_version", ""),
max_host_version=self.get_manifest_info("host_application.max_version", ""),
)
logger.debug(f"{self.log_prefix} 插件基类初始化完成")
@@ -103,93 +88,10 @@ class PluginBase(ABC):
if not self.plugin_name:
raise ValueError(f"插件类 {self.__class__.__name__} 必须定义 plugin_name")
# 验证manifest中的必需信息
if not self.get_manifest_info("name"):
raise ValueError(f"插件 {self.plugin_name} 的manifest中缺少name字段")
if not self.get_manifest_info("description"):
raise ValueError(f"插件 {self.plugin_name} 的manifest中缺少description字段")
def _load_manifest(self): # sourcery skip: raise-from-previous-error
"""加载manifest文件强制要求"""
if not self.plugin_dir:
raise ValueError(f"{self.log_prefix} 没有插件目录路径无法加载manifest")
manifest_path = os.path.join(self.plugin_dir, self.manifest_file_name)
if not os.path.exists(manifest_path):
error_msg = f"{self.log_prefix} 缺少必需的manifest文件: {manifest_path}"
logger.error(error_msg)
raise FileNotFoundError(error_msg)
try:
with open(manifest_path, encoding="utf-8") as f:
self.manifest_data = orjson.loads(f.read())
logger.debug(f"{self.log_prefix} 成功加载manifest文件: {manifest_path}")
# 验证manifest格式
self._validate_manifest()
except orjson.JSONDecodeError as e:
error_msg = f"{self.log_prefix} manifest文件格式错误: {e}"
logger.error(error_msg)
raise ValueError(error_msg)
except OSError as e:
error_msg = f"{self.log_prefix} 读取manifest文件失败: {e}"
logger.error(error_msg)
raise IOError(error_msg) # noqa
def _get_author_name(self) -> str:
"""从manifest获取作者名称"""
author_info = self.get_manifest_info("author", {})
if isinstance(author_info, dict):
return author_info.get("name", "")
else:
return str(author_info) if author_info else ""
def _validate_manifest(self):
"""验证manifest文件格式使用强化的验证器"""
if not self.manifest_data:
raise ValueError(f"{self.log_prefix} manifest数据为空验证失败")
validator = ManifestValidator()
is_valid = validator.validate_manifest(self.manifest_data)
# 记录验证结果
if validator.validation_errors or validator.validation_warnings:
report = validator.get_validation_report()
logger.info(f"{self.log_prefix} Manifest验证结果:\n{report}")
# 如果有验证错误,抛出异常
if not is_valid:
error_msg = f"{self.log_prefix} Manifest文件验证失败"
if validator.validation_errors:
error_msg += f": {'; '.join(validator.validation_errors)}"
raise ValueError(error_msg)
def get_manifest_info(self, key: str, default: Any = None) -> Any:
"""获取manifest信息
Args:
key: 信息键,支持点分割的嵌套键(如 "author.name"
default: 默认值
Returns:
Any: 对应的值
"""
if not self.manifest_data:
return default
keys = key.split(".")
value = self.manifest_data
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
if not self.plugin_meta.name:
raise ValueError(f"插件 {self.plugin_name} 的元数据中缺少 name 字段")
if not self.plugin_meta.description:
raise ValueError(f"插件 {self.plugin_name} 的元数据中缺少 description 字段")
def _generate_and_save_default_config(self, config_file_path: str):
"""根据插件的Schema生成并保存默认配置文件"""
@@ -198,7 +100,7 @@ class PluginBase(ABC):
return
toml_str = f"# {self.plugin_name} - 自动生成的配置文件\n"
plugin_description = self.get_manifest_info("description", "插件配置文件")
plugin_description = self.plugin_meta.description or "插件配置文件"
toml_str += f"# {plugin_description}\n\n"
# 遍历每个配置节
@@ -333,7 +235,7 @@ class PluginBase(ABC):
return
toml_str = f"# {self.plugin_name} - 配置文件\n"
plugin_description = self.get_manifest_info("description", "插件配置文件")
plugin_description = self.plugin_meta.description or "插件配置文件"
toml_str += f"# {plugin_description}\n\n"
# 遍历每个配置节
@@ -564,3 +466,11 @@ class PluginBase(ABC):
bool: 是否成功注册插件
"""
raise NotImplementedError("Subclasses must implement this method")
async def on_plugin_loaded(self):
"""插件加载完成后的钩子函数"""
pass
def on_unload(self):
"""插件卸载时的钩子函数"""
pass

View File

@@ -0,0 +1,25 @@
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Set
@dataclass
class PluginMetadata:
"""
插件元数据,用于存储插件的开发者信息和用户帮助信息。
"""
name: str # 插件名称 (供用户查看)
description: str # 插件功能描述
usage: str # 插件使用方法
# 以下为可选字段,参考自 _manifest.json 和 NoneBot 设计
type: Optional[str] = None # 插件类别: "library", "application"
# 从原 _manifest.json 迁移的字段
version: str = "1.0.0" # 插件版本
author: str = "" # 作者名称
license: Optional[str] = None # 开源协议
repository_url: Optional[str] = None # 仓库地址
keywords: List[str] = field(default_factory=list) # 关键词
categories: List[str] = field(default_factory=list) # 分类
# 扩展字段
extra: Dict[str, Any] = field(default_factory=dict) # 其他任意信息

View File

@@ -9,7 +9,7 @@ from typing import Any, Optional
from src.common.logger import get_logger
from src.plugin_system.base.component_types import ComponentType
from src.plugin_system.base.plugin_base import PluginBase
from src.plugin_system.utils.manifest_utils import VersionComparator
from src.plugin_system.base.plugin_metadata import PluginMetadata
from .component_registry import component_registry
@@ -27,6 +27,7 @@ class PluginManager:
self.plugin_directories: list[str] = [] # 插件根目录列表
self.plugin_classes: dict[str, type[PluginBase]] = {} # 全局插件类注册表,插件名 -> 插件类
self.plugin_paths: dict[str, str] = {} # 记录插件名到目录路径的映射,插件名 -> 目录路径
self.plugin_modules: dict[str, Any] = {} # 记录插件名到模块的映射
self.loaded_plugins: dict[str, PluginBase] = {} # 已加载的插件类实例注册表,插件名 -> 插件类实例
self.failed_plugins: dict[str, str] = {} # 记录加载失败的插件文件及其错误信息,插件名 -> 错误信息
@@ -102,23 +103,25 @@ class PluginManager:
if not plugin_dir:
return False, 1
plugin_instance = plugin_class(plugin_dir=plugin_dir) # 实例化插件可能因为缺少manifest而失败
module = self.plugin_modules.get(plugin_name)
if not module or not hasattr(module, "__plugin_meta__"):
self.failed_plugins[plugin_name] = "插件模块中缺少 __plugin_meta__"
logger.error(f"❌ 插件加载失败: {plugin_name} - 缺少 __plugin_meta__")
return False, 1
metadata: PluginMetadata = getattr(module, "__plugin_meta__")
plugin_instance = plugin_class(plugin_dir=plugin_dir, metadata=metadata)
if not plugin_instance:
logger.error(f"插件 {plugin_name} 实例化失败")
return False, 1
# 检查插件是否启用
if not plugin_instance.enable_plugin:
logger.info(f"插件 {plugin_name} 已禁用,跳过加载")
return False, 0
# 检查版本兼容性
is_compatible, compatibility_error = self._check_plugin_version_compatibility(
plugin_name, plugin_instance.manifest_data
)
if not is_compatible:
self.failed_plugins[plugin_name] = compatibility_error
logger.error(f"❌ 插件加载失败: {plugin_name} - {compatibility_error}")
return False, 1
if plugin_instance.register_plugin():
self.loaded_plugins[plugin_name] = plugin_instance
self._show_plugin_components(plugin_name)
@@ -138,21 +141,6 @@ class PluginManager:
logger.error(f"❌ 插件注册失败: {plugin_name}")
return False, 1
except FileNotFoundError as e:
# manifest文件缺失
error_msg = f"缺少manifest文件: {e!s}"
self.failed_plugins[plugin_name] = error_msg
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
return False, 1
except ValueError as e:
# manifest文件格式错误或验证失败
traceback.print_exc()
error_msg = f"manifest验证失败: {e!s}"
self.failed_plugins[plugin_name] = error_msg
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
return False, 1
except Exception as e:
# 其他错误
error_msg = f"未知错误: {e!s}"
@@ -284,14 +272,23 @@ class PluginManager:
if os.path.isdir(item_path) and not item.startswith(".") and not item.startswith("__"):
plugin_file = os.path.join(item_path, "plugin.py")
if os.path.exists(plugin_file):
if self._load_plugin_module_file(plugin_file):
module = self._load_plugin_module_file(plugin_file)
if module:
# 动态查找插件类并获取真实的 plugin_name
for attr_name in dir(module):
attr = getattr(module, attr_name)
if isinstance(attr, type) and issubclass(attr, PluginBase) and attr is not PluginBase:
plugin_name = getattr(attr, "plugin_name", None)
if plugin_name:
self.plugin_modules[plugin_name] = module
break
loaded_count += 1
else:
failed_count += 1
return loaded_count, failed_count
def _load_plugin_module_file(self, plugin_file: str) -> bool:
def _load_plugin_module_file(self, plugin_file: str) -> Optional[Any]:
# sourcery skip: extract-method
"""加载单个插件模块文件
@@ -305,62 +302,36 @@ class PluginManager:
module_name = ".".join(plugin_path.parent.parts)
try:
# 动态导入插件模块
# 首先加载 __init__.py 来获取元数据
init_file = os.path.join(plugin_dir, "__init__.py")
if os.path.exists(init_file):
init_spec = spec_from_file_location(f"{module_name}.__init__", init_file)
if init_spec and init_spec.loader:
init_module = module_from_spec(init_spec)
init_spec.loader.exec_module(init_module)
# 然后加载 plugin.py
spec = spec_from_file_location(module_name, plugin_file)
if spec is None or spec.loader is None:
logger.error(f"无法创建模块规范: {plugin_file}")
return False
return None
module = module_from_spec(spec)
module.__package__ = module_name # 设置模块包名
module.__package__ = module_name
spec.loader.exec_module(module)
# 将 __plugin_meta__ 从 init_module 附加到主模块
if init_module and hasattr(init_module, "__plugin_meta__"):
setattr(module, "__plugin_meta__", getattr(init_module, "__plugin_meta__"))
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
return True
return module
except Exception as e:
error_msg = f"加载插件模块 {plugin_file} 失败: {e}"
logger.error(error_msg)
self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg
return False
# == 兼容性检查 ==
@staticmethod
def _check_plugin_version_compatibility(plugin_name: str, manifest_data: dict[str, Any]) -> tuple[bool, str]:
"""检查插件版本兼容性
Args:
plugin_name: 插件名称
manifest_data: manifest数据
Returns:
Tuple[bool, str]: (是否兼容, 错误信息)
"""
if "host_application" not in manifest_data:
return True, "" # 没有版本要求,默认兼容
host_app = manifest_data["host_application"]
if not isinstance(host_app, dict):
return True, ""
min_version = host_app.get("min_version", "")
max_version = host_app.get("max_version", "")
if not min_version and not max_version:
return True, "" # 没有版本要求,默认兼容
try:
current_version = VersionComparator.get_current_host_version()
is_compatible, error_msg = VersionComparator.is_version_in_range(current_version, min_version, max_version)
if not is_compatible:
return False, f"版本不兼容: {error_msg}"
logger.debug(f"插件 {plugin_name} 版本兼容性检查通过")
return True, ""
except Exception as e:
logger.warning(f"插件 {plugin_name} 版本兼容性检查失败: {e}")
return False, f"插件 {plugin_name} 版本兼容性检查失败: {e}" # 检查失败时默认不允许加载
return None
# == 显示统计与插件信息 ==
@@ -396,17 +367,6 @@ class PluginManager:
logger.info(f" 📦 {plugin_info.display_name}{extra_info}")
# Manifest信息
if plugin_info.manifest_data:
"""
if plugin_info.keywords:
logger.info(f" 🏷️ 关键词: {', '.join(plugin_info.keywords)}")
if plugin_info.categories:
logger.info(f" 📁 分类: {', '.join(plugin_info.categories)}")
"""
if plugin_info.homepage_url:
logger.info(f" 🌐 主页: {plugin_info.homepage_url}")
# 组件列表
if plugin_info.components:
action_components = [

View File

@@ -3,17 +3,3 @@
提供插件开发和管理的实用工具
"""
from .manifest_utils import (
ManifestValidator,
# ManifestGenerator,
# validate_plugin_manifest,
# generate_plugin_manifest,
)
__all__ = [
"ManifestValidator",
# "ManifestGenerator",
# "validate_plugin_manifest",
# "generate_plugin_manifest",
]

View File

@@ -1,517 +0,0 @@
"""
插件Manifest工具模块
提供manifest文件的验证、生成和管理功能
"""
import re
from typing import Any
from src.common.logger import get_logger
from src.config.config import MMC_VERSION
# if TYPE_CHECKING:
# from src.plugin_system.base.base_plugin import BasePlugin
logger = get_logger("manifest_utils")
class VersionComparator:
"""版本号比较器
支持语义化版本号比较自动处理snapshot版本并支持向前兼容性检查
"""
# 版本兼容性映射表(硬编码)
# 格式: {插件最大支持版本: [实际兼容的版本列表]}
COMPATIBILITY_MAP = {
# 0.8.x 系列向前兼容规则
"0.8.0": ["0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.1": ["0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.2": ["0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.3": ["0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.4": ["0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.5": ["0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.6": ["0.8.7", "0.8.8", "0.8.9", "0.8.10"],
"0.8.7": ["0.8.8", "0.8.9", "0.8.10"],
"0.8.8": ["0.8.9", "0.8.10"],
"0.8.9": ["0.8.10"],
# 可以根据需要添加更多兼容映射
# "0.9.0": ["0.9.1", "0.9.2", "0.9.3"], # 示例0.9.x系列兼容
}
@staticmethod
def normalize_version(version: str) -> str:
"""标准化版本号移除snapshot标识
Args:
version: 原始版本号,如 "0.8.0-snapshot.1"
Returns:
str: 标准化后的版本号,如 "0.8.0"
"""
if not version:
return "0.0.0"
# 移除snapshot部分
normalized = re.sub(r"-snapshot\.\d+", "", version.strip())
normalized = re.sub(r"-alpha\-\d+", "", version.strip())
# 确保版本号格式正确
if not re.match(r"^\d+(\.\d+){0,2}$", normalized):
# 如果不是有效的版本号格式,返回默认版本
return "0.0.0"
# 尝试补全版本号
parts = normalized.split(".")
while len(parts) < 3:
parts.append("0")
normalized = ".".join(parts[:3])
return normalized
@staticmethod
def parse_version(version: str) -> tuple[int, int, int]:
"""解析版本号为元组
Args:
version: 版本号字符串
Returns:
Tuple[int, int, int]: (major, minor, patch)
"""
normalized = VersionComparator.normalize_version(version)
try:
parts = normalized.split(".")
return int(parts[0]), int(parts[1]), int(parts[2])
except (ValueError, IndexError):
logger.warning(f"无法解析版本号: {version},使用默认版本 0.0.0")
return 0, 0, 0
@staticmethod
def compare_versions(version1: str, version2: str) -> int:
"""比较两个版本号
Args:
version1: 第一个版本号
version2: 第二个版本号
Returns:
int: -1 if version1 < version2, 0 if equal, 1 if version1 > version2
"""
v1_tuple = VersionComparator.parse_version(version1)
v2_tuple = VersionComparator.parse_version(version2)
if v1_tuple < v2_tuple:
return -1
elif v1_tuple > v2_tuple:
return 1
else:
return 0
@staticmethod
def check_forward_compatibility(current_version: str, max_version: str) -> tuple[bool, str]:
"""检查向前兼容性(仅使用兼容性映射表)
Args:
current_version: 当前版本
max_version: 插件声明的最大支持版本
Returns:
Tuple[bool, str]: (是否兼容, 兼容信息)
"""
current_normalized = VersionComparator.normalize_version(current_version)
max_normalized = VersionComparator.normalize_version(max_version)
# 检查兼容性映射表
if max_normalized in VersionComparator.COMPATIBILITY_MAP:
compatible_versions = VersionComparator.COMPATIBILITY_MAP[max_normalized]
if current_normalized in compatible_versions:
return True, f"根据兼容性映射表,版本 {current_normalized}{max_normalized} 兼容"
return False, ""
@staticmethod
def is_version_in_range(version: str, min_version: str = "", max_version: str = "") -> tuple[bool, str]:
"""检查版本是否在指定范围内,支持兼容性检查
Args:
version: 要检查的版本号
min_version: 最小版本号(可选)
max_version: 最大版本号(可选)
Returns:
Tuple[bool, str]: (是否兼容, 错误信息或兼容信息)
"""
if not min_version and not max_version:
return True, ""
version_normalized = VersionComparator.normalize_version(version)
# 检查最小版本
if min_version:
min_normalized = VersionComparator.normalize_version(min_version)
if VersionComparator.compare_versions(version_normalized, min_normalized) < 0:
return False, f"版本 {version_normalized} 低于最小要求版本 {min_normalized}"
# 检查最大版本
if max_version:
max_normalized = VersionComparator.normalize_version(max_version)
comparison = VersionComparator.compare_versions(version_normalized, max_normalized)
if comparison > 0:
# 严格版本检查失败,尝试兼容性检查
is_compatible, compat_msg = VersionComparator.check_forward_compatibility(
version_normalized, max_normalized
)
if not is_compatible:
return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射"
logger.info(f"版本兼容性检查:{compat_msg}")
return True, compat_msg
return True, ""
@staticmethod
def get_current_host_version() -> str:
"""获取当前主机应用版本
Returns:
str: 当前版本号
"""
return VersionComparator.normalize_version(MMC_VERSION)
@staticmethod
def add_compatibility_mapping(base_version: str, compatible_versions: list) -> None:
"""动态添加兼容性映射
Args:
base_version: 基础版本(插件声明的最大支持版本)
compatible_versions: 兼容的版本列表
"""
base_normalized = VersionComparator.normalize_version(base_version)
VersionComparator.COMPATIBILITY_MAP[base_normalized] = [
VersionComparator.normalize_version(v) for v in compatible_versions
]
logger.info(f"添加兼容性映射:{base_normalized} -> {compatible_versions}")
@staticmethod
def get_compatibility_info() -> dict[str, list]:
"""获取当前的兼容性映射表
Returns:
Dict[str, list]: 兼容性映射表的副本
"""
return VersionComparator.COMPATIBILITY_MAP.copy()
class ManifestValidator:
"""Manifest文件验证器"""
# 必需字段(必须存在且不能为空)
REQUIRED_FIELDS = ["manifest_version", "name", "version", "description", "author"]
# 可选字段(可以不存在或为空)
OPTIONAL_FIELDS = [
"license",
"host_application",
"homepage_url",
"repository_url",
"keywords",
"categories",
"default_locale",
"locales_path",
"plugin_info",
]
# 建议填写的字段(会给出警告但不会导致验证失败)
RECOMMENDED_FIELDS = ["license", "keywords", "categories"]
SUPPORTED_MANIFEST_VERSIONS = [1]
def __init__(self):
self.validation_errors = []
self.validation_warnings = []
def validate_manifest(self, manifest_data: dict[str, Any]) -> bool:
"""验证manifest数据
Args:
manifest_data: manifest数据字典
Returns:
bool: 是否验证通过(只有错误会导致验证失败,警告不会)
"""
self.validation_errors.clear()
self.validation_warnings.clear()
# 检查必需字段
for field in self.REQUIRED_FIELDS:
if field not in manifest_data:
self.validation_errors.append(f"缺少必需字段: {field}")
elif not manifest_data[field]:
self.validation_errors.append(f"必需字段不能为空: {field}")
# 检查manifest版本
if "manifest_version" in manifest_data:
version = manifest_data["manifest_version"]
if version not in self.SUPPORTED_MANIFEST_VERSIONS:
self.validation_errors.append(
f"不支持的manifest版本: {version},支持的版本: {self.SUPPORTED_MANIFEST_VERSIONS}"
)
# 检查作者信息格式
if "author" in manifest_data:
author = manifest_data["author"]
if isinstance(author, dict):
if "name" not in author or not author["name"]:
self.validation_errors.append("作者信息缺少name字段或为空")
# url字段是可选的
if author.get("url"):
url = author["url"]
if not (url.startswith("http://") or url.startswith("https://")):
self.validation_warnings.append("作者URL建议使用完整的URL格式")
elif isinstance(author, str):
if not author.strip():
self.validation_errors.append("作者信息不能为空")
else:
self.validation_errors.append("作者信息格式错误应为字符串或包含name字段的对象")
# 检查主机应用版本要求(可选)
if "host_application" in manifest_data:
host_app = manifest_data["host_application"]
if isinstance(host_app, dict):
min_version = host_app.get("min_version", "")
max_version = host_app.get("max_version", "")
# 验证版本字段格式
for version_field in ["min_version", "max_version"]:
if version_field in host_app and not host_app[version_field]:
self.validation_warnings.append(f"host_application.{version_field}为空")
# 检查当前主机版本兼容性
if min_version or max_version:
current_version = VersionComparator.get_current_host_version()
is_compatible, error_msg = VersionComparator.is_version_in_range(
current_version, min_version, max_version
)
if not is_compatible:
self.validation_errors.append(f"版本兼容性检查失败: {error_msg} (当前版本: {current_version})")
else:
logger.debug(
f"版本兼容性检查通过: 当前版本 {current_version} 符合要求 [{min_version}, {max_version}]"
)
else:
self.validation_errors.append("host_application格式错误应为对象")
# 检查URL格式可选字段
for url_field in ["homepage_url", "repository_url"]:
if manifest_data.get(url_field):
url: str = manifest_data[url_field]
if not (url.startswith("http://") or url.startswith("https://")):
self.validation_warnings.append(f"{url_field}建议使用完整的URL格式")
# 检查数组字段格式(可选字段)
for list_field in ["keywords", "categories"]:
if list_field in manifest_data:
field_value = manifest_data[list_field]
if field_value is not None and not isinstance(field_value, list):
self.validation_errors.append(f"{list_field}应为数组格式")
elif isinstance(field_value, list):
# 检查数组元素是否为字符串
for i, item in enumerate(field_value):
if not isinstance(item, str):
self.validation_warnings.append(f"{list_field}[{i}]应为字符串")
# 检查建议字段(给出警告)
for field in self.RECOMMENDED_FIELDS:
if field not in manifest_data or not manifest_data[field]:
self.validation_warnings.append(f"建议填写字段: {field}")
# 检查plugin_info结构可选
if "plugin_info" in manifest_data:
plugin_info = manifest_data["plugin_info"]
if isinstance(plugin_info, dict):
# 检查components数组
if "components" in plugin_info:
components = plugin_info["components"]
if not isinstance(components, list):
self.validation_errors.append("plugin_info.components应为数组格式")
else:
for i, component in enumerate(components):
if not isinstance(component, dict):
self.validation_errors.append(f"plugin_info.components[{i}]应为对象")
else:
# 检查组件必需字段
for comp_field in ["type", "name", "description"]:
if comp_field not in component or not component[comp_field]:
self.validation_errors.append(
f"plugin_info.components[{i}]缺少必需字段: {comp_field}"
)
else:
self.validation_errors.append("plugin_info应为对象格式")
return len(self.validation_errors) == 0
def get_validation_report(self) -> str:
"""获取验证报告"""
report = []
if self.validation_errors:
report.append("❌ 验证错误:")
report.extend(f" - {error}" for error in self.validation_errors)
if self.validation_warnings:
report.append("⚠️ 验证警告:")
report.extend(f" - {warning}" for warning in self.validation_warnings)
if not self.validation_errors and not self.validation_warnings:
report.append("✅ Manifest文件验证通过")
return "\n".join(report)
# class ManifestGenerator:
# """Manifest文件生成器"""
# def __init__(self):
# self.template = {
# "manifest_version": 1,
# "name": "",
# "version": "1.0.0",
# "description": "",
# "author": {"name": "", "url": ""},
# "license": "MIT",
# "host_application": {"min_version": "1.0.0", "max_version": "4.0.0"},
# "homepage_url": "",
# "repository_url": "",
# "keywords": [],
# "categories": [],
# "default_locale": "zh-CN",
# "locales_path": "_locales",
# }
# def generate_from_plugin(self, plugin_instance: BasePlugin) -> Dict[str, Any]:
# """从插件实例生成manifest
# Args:
# plugin_instance: BasePlugin实例
# Returns:
# Dict[str, Any]: 生成的manifest数据
# """
# manifest = self.template.copy()
# # 基本信息
# manifest["name"] = plugin_instance.plugin_name
# manifest["version"] = plugin_instance.plugin_version
# manifest["description"] = plugin_instance.plugin_description
# # 作者信息
# if plugin_instance.plugin_author:
# manifest["author"]["name"] = plugin_instance.plugin_author
# # 组件信息
# components = []
# plugin_components = plugin_instance.get_plugin_components()
# for component_info, component_class in plugin_components:
# component_data: Dict[str, Any] = {
# "type": component_info.component_type.value,
# "name": component_info.name,
# "description": component_info.description,
# }
# # 添加激活模式信息对于Action组件
# if hasattr(component_class, "focus_activation_type"):
# activation_modes = []
# if hasattr(component_class, "focus_activation_type"):
# activation_modes.append(component_class.focus_activation_type.value)
# if hasattr(component_class, "normal_activation_type"):
# activation_modes.append(component_class.normal_activation_type.value)
# component_data["activation_modes"] = list(set(activation_modes))
# # 添加关键词信息
# if hasattr(component_class, "activation_keywords"):
# keywords = getattr(component_class, "activation_keywords", [])
# if keywords:
# component_data["keywords"] = keywords
# components.append(component_data)
# manifest["plugin_info"] = {"is_built_in": True, "plugin_type": "general", "components": components}
# return manifest
# def save_manifest(self, manifest_data: Dict[str, Any], plugin_dir: str) -> bool:
# """保存manifest文件
# Args:
# manifest_data: manifest数据
# plugin_dir: 插件目录
# Returns:
# bool: 是否保存成功
# """
# try:
# manifest_path = os.path.join(plugin_dir, "_manifest.json")
# with open(manifest_path, "w", encoding="utf-8") as f:
# orjson.dumps(manifest_data, f, ensure_ascii=False, indent=2)
# logger.info(f"Manifest文件已保存: {manifest_path}")
# return True
# except Exception as e:
# logger.error(f"保存manifest文件失败: {e}")
# return False
# def validate_plugin_manifest(plugin_dir: str) -> bool:
# """验证插件目录中的manifest文件
# Args:
# plugin_dir: 插件目录路径
# Returns:
# bool: 是否验证通过
# """
# manifest_path = os.path.join(plugin_dir, "_manifest.json")
# if not os.path.exists(manifest_path):
# logger.warning(f"未找到manifest文件: {manifest_path}")
# return False
# try:
# with open(manifest_path, "r", encoding="utf-8") as f:
# manifest_data = orjson.loads(f.read())
# validator = ManifestValidator()
# is_valid = validator.validate_manifest(manifest_data)
# logger.info(f"Manifest验证结果:\n{validator.get_validation_report()}")
# return is_valid
# except Exception as e:
# logger.error(f"读取或验证manifest文件失败: {e}")
# return False
# def generate_plugin_manifest(plugin_instance: BasePlugin, save_to_file: bool = True) -> Optional[Dict[str, Any]]:
# """为插件生成manifest文件
# Args:
# plugin_instance: BasePlugin实例
# save_to_file: 是否保存到文件
# Returns:
# Optional[Dict[str, Any]]: 生成的manifest数据
# """
# try:
# generator = ManifestGenerator()
# manifest_data = generator.generate_from_plugin(plugin_instance)
# if save_to_file and plugin_instance.plugin_dir:
# generator.save_manifest(manifest_data, plugin_instance.plugin_dir)
# return manifest_data
# except Exception as e:
# logger.error(f"生成manifest文件失败: {e}")
# return None

View File

@@ -1,7 +1,15 @@
"""
亲和力聊天处理器插件
"""
from src.plugin_system.base.plugin_metadata import PluginMetadata
from .plugin import AffinityChatterPlugin
__plugin_meta__ = PluginMetadata(
name="Affinity Flow Chatter",
description="Built-in chatter plugin for affinity flow with interest scoring and relationship building",
usage="This plugin is automatically triggered by the system.",
version="1.0.0",
author="MoFox",
keywords=["chatter", "affinity", "conversation"],
categories=["Chat", "AI"],
extra={
"is_built_in": True
}
)
__all__ = ["AffinityChatterPlugin"]

View File

@@ -1,23 +0,0 @@
{
"manifest_version": 1,
"name": "affinity_chatter",
"display_name": "Affinity Flow Chatter",
"description": "Built-in chatter plugin for affinity flow with interest scoring and relationship building",
"version": "1.0.0",
"author": "MoFox",
"plugin_class": "AffinityChatterPlugin",
"enabled": true,
"is_built_in": true,
"components": [
{
"name": "affinity_chatter",
"type": "chatter",
"description": "Affinity flow chatter with intelligent interest scoring and relationship building",
"enabled": true,
"chat_type_allow": ["all"]
}
],
"host_application": { "min_version": "0.8.0" },
"keywords": ["chatter", "affinity", "conversation"],
"categories": ["Chat", "AI"]
}

View File

@@ -0,0 +1,17 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="Emoji插件 (Emoji Actions)",
description="可以发送和管理Emoji",
usage="该插件提供 `emoji` action。",
version="1.0.0",
author="SengokuCola",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MaiM-with-u/maibot",
keywords=["emoji", "action", "built-in"],
categories=["Emoji"],
extra={
"is_built_in": True,
"plugin_type": "action_provider",
}
)

View File

@@ -1,34 +0,0 @@
{
"manifest_version": 1,
"name": "Emoji插件 (Emoji Actions)",
"version": "1.0.0",
"description": "可以发送和管理Emoji",
"author": {
"name": "SengokuCola",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": ["emoji", "action", "built-in"],
"categories": ["Emoji"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "action_provider",
"components": [
{
"type": "action",
"name": "emoji",
"description": "作为一条全新的消息,发送一个符合当前情景的表情包来生动地表达情绪。"
}
]
}
}

View File

@@ -1,8 +1,17 @@
"""
让框架能够发现并加载子目录中的组件。
"""
from src.plugin_system.base.plugin_metadata import PluginMetadata
from .actions.read_feed_action import ReadFeedAction as ReadFeedAction
from .actions.send_feed_action import SendFeedAction as SendFeedAction
from .commands.send_feed_command import SendFeedCommand as SendFeedCommand
from .plugin import MaiZoneRefactoredPlugin as MaiZoneRefactoredPlugin
__plugin_meta__ = PluginMetadata(
name="MaiZone麦麦空间- 重构版",
description="重构版让你的麦麦发QQ空间说说、评论、点赞支持AI配图、定时发送和自动监控功能",
usage="该插件提供 `send_feed` 和 `read_feed` action以及 `send_feed` command。",
version="3.0.0",
author="MoFox-Studio",
license="GPL-v3.0",
repository_url="https://github.com/MoFox-Studio",
keywords=["QQ空间", "说说", "动态", "评论", "点赞", "自动化", "AI配图"],
categories=["社交", "自动化", "QQ空间"],
extra={
"is_built_in": False,
"plugin_type": "social",
}
)

View File

@@ -1,47 +0,0 @@
{
"manifest_version": 1,
"name": "MaiZone麦麦空间- 重构版",
"version": "3.0.0",
"description": "重构版让你的麦麦发QQ空间说说、评论、点赞支持AI配图、定时发送和自动监控功能",
"author": {
"name": "MoFox-Studio",
"url": "https://github.com/MoFox-Studio"
},
"license": "GPL-v3.0",
"host_application": {
"min_version": "0.10.0"
},
"keywords": ["QQ空间", "说说", "动态", "评论", "点赞", "自动化", "AI配图"],
"categories": ["社交", "自动化", "QQ空间"],
"plugin_info": {
"is_built_in": false,
"plugin_type": "social",
"components": [
{
"type": "action",
"name": "send_feed",
"description": "根据指定主题发送一条QQ空间说说"
},
{
"type": "action",
"name": "read_feed",
"description": "读取指定好友最近的说说,并评论点赞"
},
{
"type": "command",
"name": "send_feed",
"description": "通过命令发送QQ空间说说"
}
],
"features": [
"智能生成说说内容",
"AI自动配图硅基流动",
"自动点赞评论好友说说",
"定时发送说说",
"权限管理系统",
"历史记录避重"
]
}
}

View File

@@ -0,0 +1,16 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="napcat_plugin",
description="基于OneBot 11协议的NapCat QQ协议插件提供完整的QQ机器人API接口使用现有adapter连接",
usage="该插件提供 `napcat_tool` tool。",
version="1.0.0",
author="Windpicker_owo",
license="GPL-v3.0-or-later",
repository_url="https://github.com/Windpicker-owo/InternetSearchPlugin",
keywords=["qq", "bot", "napcat", "onebot", "api", "websocket"],
categories=["protocol"],
extra={
"is_built_in": False,
}
)

View File

@@ -1,42 +0,0 @@
{
"manifest_version": 1,
"name": "napcat_plugin",
"version": "1.0.0",
"description": "基于OneBot 11协议的NapCat QQ协议插件提供完整的QQ机器人API接口使用现有adapter连接",
"author": {
"name": "Windpicker_owo",
"url": "https://github.com/Windpicker-owo"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.0",
"max_version": "0.11.0"
},
"homepage_url": "https://github.com/Windpicker-owo/InternetSearchPlugin",
"repository_url": "https://github.com/Windpicker-owo/InternetSearchPlugin",
"keywords": ["qq", "bot", "napcat", "onebot", "api", "websocket"],
"categories": ["protocol"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": false,
"components": [
{
"type": "tool",
"name": "napcat_tool",
"description": "NapCat QQ协议综合工具提供消息发送、群管理、好友管理、文件操作等完整功能"
}
],
"features": [
"消息发送与接收",
"群管理功能",
"好友管理功能",
"文件上传下载",
"AI语音功能",
"群签到与戳一戳",
"现有adapter连接"
]
}
}

View File

@@ -0,0 +1,16 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="权限管理插件Permission Management",
description="通过系统API管理权限",
usage="该插件提供 `permission_management` command。",
version="1.0.0",
author="MoFox-Studio",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MoFox-Studio",
keywords=["plugins", "permission", "management", "built-in"],
extra={
"is_built_in": True,
"plugin_type": "permission",
}
)

View File

@@ -1,33 +0,0 @@
{
"manifest_version": 1,
"name": "权限管理插件Permission Management",
"version": "1.0.0",
"description": "通过系统API管理权限",
"author": {
"name": "MoFox-Studio",
"url": "https://github.com/MoFox-Studio"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.0"
},
"keywords": [
"plugins",
"permission",
"management",
"built-in"
],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "permission",
"components": [
{
"type": "command",
"name": "permission_management",
"description": "管理用户权限,包括添加、删除和修改权限等操作。"
}
]
}
}

View File

@@ -0,0 +1,17 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="插件和组件管理 (Plugin and Component Management)",
description="通过系统API管理插件和组件的生命周期包括加载、卸载、启用和禁用等操作。",
usage="该插件提供 `plugin_management` command。",
version="1.0.0",
author="MaiBot团队",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MaiM-with-u/maibot",
keywords=["plugins", "components", "management", "built-in"],
categories=["Core System", "Plugin Management"],
extra={
"is_built_in": True,
"plugin_type": "plugin_management",
}
)

View File

@@ -1,39 +0,0 @@
{
"manifest_version": 1,
"name": "插件和组件管理 (Plugin and Component Management)",
"version": "1.0.0",
"description": "通过系统API管理插件和组件的生命周期包括加载、卸载、启用和禁用等操作。",
"author": {
"name": "MaiBot团队",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.9.1"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": [
"plugins",
"components",
"management",
"built-in"
],
"categories": [
"Core System",
"Plugin Management"
],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "plugin_management",
"components": [
{
"type": "command",
"name": "plugin_management",
"description": "管理插件和组件的生命周期,包括加载、卸载、启用和禁用等操作。"
}
]
}
}

View File

@@ -0,0 +1,17 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="MoFox-Bot主动思考",
description="主动思考插件",
usage="该插件由系统自动触发。",
version="1.0.0",
author="MoFox-Studio",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MoFox-Studio",
keywords=["主动思考","自己发消息"],
categories=["Chat", "Integration"],
extra={
"is_built_in": True,
"plugin_type": "functional"
}
)

View File

@@ -1,25 +0,0 @@
{
"manifest_version": 1,
"name": "MoFox-Bot主动思考",
"version": "1.0.0",
"description": "主动思考插件",
"author": {
"name": "MoFox-Studio",
"url": "https://github.com/MoFox-Studio"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.0"
},
"keywords": ["主动思考","自己发消息"],
"categories": ["Chat", "Integration"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": "true",
"plugin_type": "functional"
}
}

View File

@@ -0,0 +1,17 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="MoFox-Bot工具箱",
description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
usage="该插件提供多种命令,详情请查阅文档。",
version="1.0.0",
author="MoFox-Studio",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MoFox-Studio",
keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
categories=["Chat", "Integration"],
extra={
"is_built_in": "true",
"plugin_type": "functional"
}
)

View File

@@ -1,25 +0,0 @@
{
"manifest_version": 1,
"name": "MoFox-Bot工具箱",
"version": "1.0.0",
"description": "一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
"author": {
"name": "MoFox-Studio",
"url": "https://github.com/MoFox-Studio"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.0"
},
"keywords": ["emoji", "reaction", "like", "表情", "回应", "点赞"],
"categories": ["Chat", "Integration"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": "true",
"plugin_type": "functional"
}
}

View File

@@ -0,0 +1,17 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="文本转语音插件 (Text-to-Speech)",
description="将文本转换为语音进行播放的插件,支持多种语音模式和智能语音输出场景判断。",
usage="该插件提供 `tts_action` action。",
version="0.1.0",
author="MaiBot团队",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MaiM-with-u/maibot",
keywords=["tts", "voice", "audio", "speech", "accessibility"],
categories=["Audio Tools", "Accessibility", "Voice Assistant"],
extra={
"is_built_in": True,
"plugin_type": "audio_processor",
}
)

View File

@@ -1,42 +0,0 @@
{
"manifest_version": 1,
"name": "文本转语音插件 (Text-to-Speech)",
"version": "0.1.0",
"description": "将文本转换为语音进行播放的插件,支持多种语音模式和智能语音输出场景判断。",
"author": {
"name": "MaiBot团队",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.8.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": ["tts", "voice", "audio", "speech", "accessibility"],
"categories": ["Audio Tools", "Accessibility", "Voice Assistant"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "audio_processor",
"components": [
{
"type": "action",
"name": "tts_action",
"description": "将文本转换为语音进行播放",
"activation_modes": ["llm_judge", "keyword"],
"keywords": ["语音", "tts", "播报", "读出来", "语音播放", "听", "朗读"]
}
],
"features": [
"文本转语音播放",
"智能场景判断",
"关键词触发",
"支持多种语音模式"
]
}
}

View File

@@ -0,0 +1,16 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="Web Search Tool",
description="A tool for searching the web.",
usage="This plugin provides a `web_search` tool.",
version="1.0.0",
author="MoFox-Studio",
license="GPL-v3.0-or-later",
repository_url="https://github.com/MoFox-Studio",
keywords=["web", "search", "tool"],
categories=["Tools"],
extra={
"is_built_in": True,
}
)