diff --git a/scripts/manifest_tool.py b/scripts/manifest_tool.py index 3b31f5736..8312dc3e4 100644 --- a/scripts/manifest_tool.py +++ b/scripts/manifest_tool.py @@ -19,39 +19,36 @@ project_root = Path(__file__).parent.parent.parent.parent sys.path.insert(0, str(project_root)) - logger = get_logger("manifest_tool") def create_minimal_manifest(plugin_dir: str, plugin_name: str, description: str = "", author: str = "") -> bool: """创建最小化的manifest文件 - + Args: plugin_dir: 插件目录 plugin_name: 插件名称 description: 插件描述 author: 插件作者 - + Returns: bool: 是否创建成功 """ manifest_path = os.path.join(plugin_dir, "_manifest.json") - + if os.path.exists(manifest_path): print(f"❌ Manifest文件已存在: {manifest_path}") return False - + # 创建最小化manifest minimal_manifest = { "manifest_version": 1, "name": plugin_name, "version": "1.0.0", "description": description or f"{plugin_name}插件", - "author": { - "name": author or "Unknown" - } + "author": {"name": author or "Unknown"}, } - + try: with open(manifest_path, "w", encoding="utf-8") as f: json.dump(minimal_manifest, f, ensure_ascii=False, indent=2) @@ -64,35 +61,29 @@ def create_minimal_manifest(plugin_dir: str, plugin_name: str, description: str def create_complete_manifest(plugin_dir: str, plugin_name: str) -> bool: """创建完整的manifest模板文件 - + Args: plugin_dir: 插件目录 plugin_name: 插件名称 - + Returns: bool: 是否创建成功 """ manifest_path = os.path.join(plugin_dir, "_manifest.json") - + if os.path.exists(manifest_path): print(f"❌ Manifest文件已存在: {manifest_path}") return False - + # 创建完整模板 complete_manifest = { "manifest_version": 1, "name": plugin_name, "version": "1.0.0", "description": f"{plugin_name}插件描述", - "author": { - "name": "插件作者", - "url": "https://github.com/your-username" - }, + "author": {"name": "插件作者", "url": "https://github.com/your-username"}, "license": "MIT", - "host_application": { - "min_version": "1.0.0", - "max_version": "4.0.0" - }, + "host_application": {"min_version": "1.0.0", "max_version": "4.0.0"}, "homepage_url": "https://github.com/your-repo", "repository_url": "https://github.com/your-repo", "keywords": ["keyword1", "keyword2"], @@ -102,16 +93,10 @@ def create_complete_manifest(plugin_dir: str, plugin_name: str) -> bool: "plugin_info": { "is_built_in": False, "plugin_type": "general", - "components": [ - { - "type": "action", - "name": "sample_action", - "description": "示例动作组件" - } - ] - } + "components": [{"type": "action", "name": "sample_action", "description": "示例动作组件"}], + }, } - + try: with open(manifest_path, "w", encoding="utf-8") as f: json.dump(complete_manifest, f, ensure_ascii=False, indent=2) @@ -125,37 +110,37 @@ def create_complete_manifest(plugin_dir: str, plugin_name: str) -> bool: def validate_manifest_file(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): print(f"❌ 未找到manifest文件: {manifest_path}") return False - + try: with open(manifest_path, "r", encoding="utf-8") as f: manifest_data = json.load(f) - + validator = ManifestValidator() is_valid = validator.validate_manifest(manifest_data) - + # 显示验证结果 print("📋 Manifest验证结果:") print(validator.get_validation_report()) - + if is_valid: print("✅ Manifest文件验证通过") else: print("❌ Manifest文件验证失败") - + return is_valid - + except json.JSONDecodeError as e: print(f"❌ Manifest文件格式错误: {e}") return False @@ -166,24 +151,24 @@ def validate_manifest_file(plugin_dir: str) -> bool: def scan_plugins_without_manifest(root_dir: str) -> None: """扫描缺少manifest文件的插件 - + Args: root_dir: 扫描的根目录 """ print(f"🔍 扫描目录: {root_dir}") - + plugins_without_manifest = [] - + for root, dirs, files in os.walk(root_dir): # 跳过隐藏目录和__pycache__ - dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__'] - + dirs[:] = [d for d in dirs if not d.startswith(".") and d != "__pycache__"] + # 检查是否包含plugin.py文件(标识为插件目录) if "plugin.py" in files: manifest_path = os.path.join(root, "_manifest.json") if not os.path.exists(manifest_path): plugins_without_manifest.append(root) - + if plugins_without_manifest: print(f"❌ 发现 {len(plugins_without_manifest)} 个插件缺少manifest文件:") for plugin_dir in plugins_without_manifest: @@ -198,56 +183,51 @@ def main(): """主函数""" parser = argparse.ArgumentParser(description="插件Manifest管理工具") subparsers = parser.add_subparsers(dest="command", help="可用命令") - + # 创建最小化manifest命令 create_minimal_parser = subparsers.add_parser("create-minimal", help="创建最小化manifest文件") create_minimal_parser.add_argument("plugin_dir", help="插件目录路径") create_minimal_parser.add_argument("--name", help="插件名称") create_minimal_parser.add_argument("--description", help="插件描述") create_minimal_parser.add_argument("--author", help="插件作者") - + # 创建完整manifest命令 create_complete_parser = subparsers.add_parser("create-complete", help="创建完整manifest模板") create_complete_parser.add_argument("plugin_dir", help="插件目录路径") create_complete_parser.add_argument("--name", help="插件名称") - + # 验证manifest命令 validate_parser = subparsers.add_parser("validate", help="验证manifest文件") validate_parser.add_argument("plugin_dir", help="插件目录路径") - + # 扫描插件命令 scan_parser = subparsers.add_parser("scan", help="扫描缺少manifest的插件") scan_parser.add_argument("root_dir", help="扫描的根目录路径") - + args = parser.parse_args() - + if not args.command: parser.print_help() return - + try: if args.command == "create-minimal": plugin_name = args.name or os.path.basename(os.path.abspath(args.plugin_dir)) - success = create_minimal_manifest( - args.plugin_dir, - plugin_name, - args.description or "", - args.author or "" - ) + success = create_minimal_manifest(args.plugin_dir, plugin_name, args.description or "", args.author or "") sys.exit(0 if success else 1) - + elif args.command == "create-complete": plugin_name = args.name or os.path.basename(os.path.abspath(args.plugin_dir)) success = create_complete_manifest(args.plugin_dir, plugin_name) sys.exit(0 if success else 1) - + elif args.command == "validate": success = validate_manifest_file(args.plugin_dir) sys.exit(0 if success else 1) - + elif args.command == "scan": scan_plugins_without_manifest(args.root_dir) - + except Exception as e: print(f"❌ 执行命令时发生错误: {e}") sys.exit(1) diff --git a/scripts/test_version_compatibility.py b/scripts/test_version_compatibility.py index 986b27b53..298267c0a 100644 --- a/scripts/test_version_compatibility.py +++ b/scripts/test_version_compatibility.py @@ -18,7 +18,7 @@ from src.plugin_system.utils.manifest_utils import VersionComparator def test_version_normalization(): """测试版本号标准化功能""" print("🧪 测试版本号标准化...") - + test_cases = [ ("0.8.0-snapshot.1", "0.8.0"), ("0.8.0-snapshot.2", "0.8.0"), @@ -30,7 +30,7 @@ def test_version_normalization(): ("", "0.0.0"), ("invalid", "0.0.0"), ] - + for input_version, expected in test_cases: result = VersionComparator.normalize_version(input_version) status = "✅" if result == expected else "❌" @@ -40,16 +40,16 @@ def test_version_normalization(): def test_version_comparison(): """测试版本号比较功能""" print("\n🧪 测试版本号比较...") - + test_cases = [ ("0.8.0", "0.9.0", -1), # 0.8.0 < 0.9.0 - ("0.9.0", "0.8.0", 1), # 0.9.0 > 0.8.0 - ("1.0.0", "1.0.0", 0), # 1.0.0 == 1.0.0 + ("0.9.0", "0.8.0", 1), # 0.9.0 > 0.8.0 + ("1.0.0", "1.0.0", 0), # 1.0.0 == 1.0.0 ("0.8.0-snapshot.1", "0.8.0", 0), # 标准化后相等 - ("1.2.3", "1.2.4", -1), # 1.2.3 < 1.2.4 - ("2.0.0", "1.9.9", 1), # 2.0.0 > 1.9.9 + ("1.2.3", "1.2.4", -1), # 1.2.3 < 1.2.4 + ("2.0.0", "1.9.9", 1), # 2.0.0 > 1.9.9 ] - + for v1, v2, expected in test_cases: result = VersionComparator.compare_versions(v1, v2) status = "✅" if result == expected else "❌" @@ -60,17 +60,17 @@ def test_version_comparison(): def test_version_range_check(): """测试版本范围检查功能""" print("\n🧪 测试版本范围检查...") - + test_cases = [ - ("0.8.0", "0.7.0", "0.9.0", True), # 在范围内 + ("0.8.0", "0.7.0", "0.9.0", True), # 在范围内 ("0.6.0", "0.7.0", "0.9.0", False), # 低于最小版本 ("1.0.0", "0.7.0", "0.9.0", False), # 高于最大版本 - ("0.8.0", "0.8.0", "0.8.0", True), # 等于边界 - ("0.8.0", "", "0.9.0", True), # 只有最大版本限制 - ("0.8.0", "0.7.0", "", True), # 只有最小版本限制 - ("0.8.0", "", "", True), # 无版本限制 + ("0.8.0", "0.8.0", "0.8.0", True), # 等于边界 + ("0.8.0", "", "0.9.0", True), # 只有最大版本限制 + ("0.8.0", "0.7.0", "", True), # 只有最小版本限制 + ("0.8.0", "", "", True), # 无版本限制 ] - + for version, min_ver, max_ver, expected in test_cases: is_compatible, error_msg = VersionComparator.is_version_in_range(version, min_ver, max_ver) status = "✅" if is_compatible == expected else "❌" @@ -83,18 +83,18 @@ def test_version_range_check(): def test_current_version(): """测试获取当前版本功能""" print("\n🧪 测试获取当前主机版本...") - + try: current_version = VersionComparator.get_current_host_version() print(f" ✅ 当前主机版本: {current_version}") - + # 验证版本号格式 - parts = current_version.split('.') + parts = current_version.split(".") if len(parts) == 3 and all(part.isdigit() for part in parts): - print(f" ✅ 版本号格式正确") + print(" ✅ 版本号格式正确") else: - print(f" ❌ 版本号格式错误") - + print(" ❌ 版本号格式错误") + except Exception as e: print(f" ❌ 获取当前版本失败: {e}") @@ -102,52 +102,32 @@ def test_current_version(): def test_manifest_compatibility(): """测试manifest兼容性检查""" print("\n🧪 测试manifest兼容性检查...") - + # 模拟manifest数据 test_manifests = [ - { - "name": "兼容插件", - "host_application": { - "min_version": "0.1.0", - "max_version": "2.0.0" - } - }, - { - "name": "版本过高插件", - "host_application": { - "min_version": "10.0.0", - "max_version": "20.0.0" - } - }, - { - "name": "版本过低插件", - "host_application": { - "min_version": "0.1.0", - "max_version": "0.2.0" - } - }, + {"name": "兼容插件", "host_application": {"min_version": "0.1.0", "max_version": "2.0.0"}}, + {"name": "版本过高插件", "host_application": {"min_version": "10.0.0", "max_version": "20.0.0"}}, + {"name": "版本过低插件", "host_application": {"min_version": "0.1.0", "max_version": "0.2.0"}}, { "name": "无版本要求插件", # 没有host_application字段 - } + }, ] - + # 这里需要导入PluginManager来测试,但可能会有依赖问题 # 所以我们直接使用VersionComparator进行测试 current_version = VersionComparator.get_current_host_version() - + for manifest in test_manifests: plugin_name = manifest["name"] - + if "host_application" in manifest: host_app = manifest["host_application"] min_version = host_app.get("min_version", "") max_version = host_app.get("max_version", "") - - is_compatible, error_msg = VersionComparator.is_version_in_range( - current_version, min_version, max_version - ) - + + is_compatible, error_msg = VersionComparator.is_version_in_range(current_version, min_version, max_version) + status = "✅" if is_compatible else "❌" print(f" {status} {plugin_name}: {is_compatible}") if error_msg: @@ -159,24 +139,23 @@ def test_manifest_compatibility(): def test_additional_snapshot_formats(): """测试额外的snapshot版本格式""" print("\n🧪 测试额外的snapshot版本格式...") - + test_cases = [ # 用户提到的版本格式 ("0.8.0-snapshot.1", "0.8.0"), ("0.8.0-snapshot.2", "0.8.0"), ("0.8.0", "0.8.0"), ("0.9.0-snapshot.1", "0.9.0"), - # 边界情况 ("1.0.0-snapshot.999", "1.0.0"), ("2.15.3-snapshot.42", "2.15.3"), ("10.5.0-snapshot.1", "10.5.0"), - # 不正确的snapshot格式(应该被忽略或正确处理) + # 不正确的snapshot格式(应该被忽略或正确处理) ("0.8.0-snapshot", "0.0.0"), # 无数字后缀,应该标准化为0.0.0 ("0.8.0-snapshot.abc", "0.0.0"), # 非数字后缀,应该标准化为0.0.0 ("0.8.0-beta.1", "0.0.0"), # 其他预发布版本,应该标准化为0.0.0 ] - + for input_version, expected in test_cases: result = VersionComparator.normalize_version(input_version) status = "✅" if result == expected else "❌" @@ -186,23 +165,21 @@ def test_additional_snapshot_formats(): def test_snapshot_version_comparison(): """测试snapshot版本的比较功能""" print("\n🧪 测试snapshot版本比较...") - + test_cases = [ # snapshot版本与正式版本比较 - ("0.8.0-snapshot.1", "0.8.0", 0), # 应该相等 - ("0.8.0-snapshot.2", "0.8.0", 0), # 应该相等 - ("0.9.0-snapshot.1", "0.8.0", 1), # 应该大于 + ("0.8.0-snapshot.1", "0.8.0", 0), # 应该相等 + ("0.8.0-snapshot.2", "0.8.0", 0), # 应该相等 + ("0.9.0-snapshot.1", "0.8.0", 1), # 应该大于 ("0.7.0-snapshot.1", "0.8.0", -1), # 应该小于 - # snapshot版本之间比较 ("0.8.0-snapshot.1", "0.8.0-snapshot.2", 0), # 都标准化为0.8.0,相等 ("0.9.0-snapshot.1", "0.8.0-snapshot.1", 1), # 0.9.0 > 0.8.0 - # 边界情况 - ("1.0.0-snapshot.1", "0.9.9", 1), # 主版本更高 - ("0.9.0-snapshot.1", "0.8.99", 1), # 次版本更高 + ("1.0.0-snapshot.1", "0.9.9", 1), # 主版本更高 + ("0.9.0-snapshot.1", "0.8.99", 1), # 次版本更高 ] - + for version1, version2, expected in test_cases: result = VersionComparator.compare_versions(version1, version2) status = "✅" if result == expected else "❌" @@ -214,7 +191,7 @@ def main(): """主函数""" print("🔧 MaiBot插件版本兼容性检查测试") print("=" * 50) - + try: test_version_normalization() test_version_comparison() @@ -223,12 +200,13 @@ def main(): test_manifest_compatibility() test_additional_snapshot_formats() test_snapshot_version_comparison() - + print("\n🎉 所有测试完成!") - + except Exception as e: print(f"\n❌ 测试过程中发生错误: {e}") import traceback + traceback.print_exc() sys.exit(1) diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 1da361486..01b9a6125 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -28,7 +28,7 @@ from src.plugin_system.utils import ( ManifestValidator, ManifestGenerator, validate_plugin_manifest, - generate_plugin_manifest + generate_plugin_manifest, ) @@ -37,7 +37,7 @@ __version__ = "1.0.0" __all__ = [ # 基础类 "BasePlugin", - "BaseAction", + "BaseAction", "BaseCommand", # 类型定义 "ComponentType", diff --git a/src/plugin_system/base/base_plugin.py b/src/plugin_system/base/base_plugin.py index 9d1f5f1e3..24aa35353 100644 --- a/src/plugin_system/base/base_plugin.py +++ b/src/plugin_system/base/base_plugin.py @@ -63,7 +63,7 @@ class BasePlugin(ABC): self._validate_plugin_info() # 加载插件配置 - self._load_plugin_config() # 创建插件信息对象 + self._load_plugin_config() # 创建插件信息对象 self.plugin_info = PluginInfo( name=self.plugin_name, description=self.plugin_description, @@ -88,7 +88,7 @@ class BasePlugin(ABC): logger.debug(f"{self.log_prefix} 插件基类初始化完成") def _validate_plugin_info(self): - """验证插件基本信息""" + """验证插件基本信息""" if not self.plugin_name: raise ValueError(f"插件类 {self.__class__.__name__} 必须定义 plugin_name") if not self.plugin_description: @@ -100,7 +100,7 @@ class BasePlugin(ABC): 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) @@ -109,23 +109,23 @@ class BasePlugin(ABC): try: with open(manifest_path, "r", encoding="utf-8") as f: self.manifest_data = json.load(f) - + logger.debug(f"{self.log_prefix} 成功加载manifest文件: {manifest_path}") - + # 验证manifest格式 self._validate_manifest() - + # 从manifest覆盖插件基本信息(如果插件类中未定义) self._apply_manifest_overrides() - + except json.JSONDecodeError as e: error_msg = f"{self.log_prefix} manifest文件格式错误: {e}" logger.error(error_msg) - raise ValueError(error_msg) #noqa + raise ValueError(error_msg) # noqa except IOError as e: error_msg = f"{self.log_prefix} 读取manifest文件失败: {e}" logger.error(error_msg) - raise IOError(error_msg) #noqa + raise IOError(error_msg) # noqa def _apply_manifest_overrides(self): """从manifest文件覆盖插件信息""" @@ -145,7 +145,7 @@ class BasePlugin(ABC): if not self.plugin_author: author_info = self.manifest_data.get("author", {}) if isinstance(author_info, dict): - self.plugin_author = author_info.get("name", "") + self.plugin_author = author_info.get("name", "") else: self.plugin_author = str(author_info) @@ -153,18 +153,18 @@ class BasePlugin(ABC): """验证manifest文件格式(使用强化的验证器)""" if not self.manifest_data: return - + # 导入验证器 from src.plugin_system.utils.manifest_utils import ManifestValidator - + 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文件验证失败" @@ -183,19 +183,13 @@ class BasePlugin(ABC): "name": self.plugin_name, "version": self.plugin_version, "description": self.plugin_description or "插件描述", - "author": { - "name": self.plugin_author or "Unknown", - "url": "" - }, + "author": {"name": self.plugin_author or "Unknown", "url": ""}, "license": "MIT", - "host_application": { - "min_version": "1.0.0", - "max_version": "4.0.0" - }, + "host_application": {"min_version": "1.0.0", "max_version": "4.0.0"}, "keywords": [], "categories": [], "default_locale": "zh-CN", - "locales_path": "_locales" + "locales_path": "_locales", } try: diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index bfcafeb8d..c75403f62 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -131,7 +131,7 @@ class PluginInfo: python_dependencies: List[PythonDependency] = None # Python包依赖 config_file: str = "" # 配置文件路径 metadata: Dict[str, Any] = None # 额外元数据 - # 新增:manifest相关信息 + # 新增:manifest相关信息 manifest_data: Dict[str, Any] = None # manifest文件数据 license: str = "" # 插件许可证 homepage_url: str = "" # 插件主页 diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 5f4705848..1afdda7dd 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -76,7 +76,8 @@ class PluginManager: logger.debug(f"插件模块加载完成 - 成功: {total_loaded_modules}, 失败: {total_failed_modules}") # 第二阶段:实例化所有已注册的插件类 - from src.plugin_system.base.base_plugin import get_registered_plugin_classes + from src.plugin_system.base.base_plugin import get_registered_plugin_classes + plugin_classes = get_registered_plugin_classes() total_registered = 0 total_failed_registration = 0 @@ -90,7 +91,7 @@ class PluginManager: if not plugin_dir: plugin_dir = self._find_plugin_directory(plugin_class) if plugin_dir: - self.plugin_paths[plugin_name] = plugin_dir # 实例化插件(可能因为缺少manifest而失败) + self.plugin_paths[plugin_name] = plugin_dir # 实例化插件(可能因为缺少manifest而失败) plugin_instance = plugin_class(plugin_dir=plugin_dir) # 检查插件是否启用 @@ -121,7 +122,7 @@ class PluginManager: component_types[comp_type] = component_types.get(comp_type, 0) + 1 components_str = ", ".join([f"{count}个{ctype}" for ctype, count in component_types.items()]) - + # 显示manifest信息 manifest_info = "" if plugin_info.license: @@ -130,7 +131,7 @@ class PluginManager: manifest_info += f" 关键词: {', '.join(plugin_info.keywords[:3])}" # 只显示前3个关键词 if len(plugin_info.keywords) > 3: manifest_info += "..." - + logger.info( f"✅ 插件加载成功: {plugin_name} v{plugin_info.version} ({components_str}){manifest_info} - {plugin_info.description}" ) @@ -140,21 +141,21 @@ class PluginManager: total_failed_registration += 1 self.failed_plugins[plugin_name] = "插件注册失败" logger.error(f"❌ 插件注册失败: {plugin_name}") - + except FileNotFoundError as e: # manifest文件缺失 total_failed_registration += 1 error_msg = f"缺少manifest文件: {str(e)}" self.failed_plugins[plugin_name] = error_msg logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") - + except ValueError as e: # manifest文件格式错误或验证失败 total_failed_registration += 1 error_msg = f"manifest验证失败: {str(e)}" self.failed_plugins[plugin_name] = error_msg logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") - + except Exception as e: # 其他错误 total_failed_registration += 1 @@ -517,46 +518,45 @@ class PluginManager: def check_plugin_version_compatibility(self, 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: from src.plugin_system.utils.manifest_utils import VersionComparator - + current_version = VersionComparator.get_current_host_version() - is_compatible, error_msg = VersionComparator.is_version_in_range( - current_version, min_version, max_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}" else: logger.debug(f"插件 {plugin_name} 版本兼容性检查通过") return True, "" - + except Exception as e: logger.warning(f"插件 {plugin_name} 版本兼容性检查失败: {e}") return True, "" # 检查失败时默认允许加载 + # 全局插件管理器实例 plugin_manager = PluginManager() diff --git a/src/plugin_system/utils/__init__.py b/src/plugin_system/utils/__init__.py index 2eed9ccf3..10a4fef34 100644 --- a/src/plugin_system/utils/__init__.py +++ b/src/plugin_system/utils/__init__.py @@ -6,14 +6,9 @@ from src.plugin_system.utils.manifest_utils import ( ManifestValidator, - ManifestGenerator, + ManifestGenerator, validate_plugin_manifest, - generate_plugin_manifest + generate_plugin_manifest, ) -__all__ = [ - "ManifestValidator", - "ManifestGenerator", - "validate_plugin_manifest", - "generate_plugin_manifest" -] +__all__ = ["ManifestValidator", "ManifestGenerator", "validate_plugin_manifest", "generate_plugin_manifest"] diff --git a/src/plugin_system/utils/manifest_utils.py b/src/plugin_system/utils/manifest_utils.py index 3725558c3..49ded87f6 100644 --- a/src/plugin_system/utils/manifest_utils.py +++ b/src/plugin_system/utils/manifest_utils.py @@ -7,7 +7,7 @@ import json import os import re -from typing import Dict, Any, Optional, Tuple, List +from typing import Dict, Any, Optional, Tuple from src.common.logger import get_logger from src.config.config import MMC_VERSION @@ -16,113 +16,113 @@ logger = get_logger("manifest_utils") class VersionComparator: """版本号比较器 - + 支持语义化版本号比较,自动处理snapshot版本 """ - + @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"-snapshot\.\d+", "", version.strip()) + # 确保版本号格式正确 - if not re.match(r'^\d+(\.\d+){0,2}$', normalized): + if not re.match(r"^\d+(\.\d+){0,2}$", normalized): # 如果不是有效的版本号格式,返回默认版本 return "0.0.0" - + # 尝试补全版本号 - parts = normalized.split('.') + parts = normalized.split(".") while len(parts) < 3: - parts.append('0') - normalized = '.'.join(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('.') + 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 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) if VersionComparator.compare_versions(version_normalized, max_normalized) > 0: return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized}" - + return True, "" - + @staticmethod def get_current_host_version() -> str: """获取当前主机应用版本 - + Returns: str: 当前版本号 """ @@ -131,67 +131,59 @@ class VersionComparator: class ManifestValidator: """Manifest文件验证器""" - + # 必需字段(必须存在且不能为空) - REQUIRED_FIELDS = [ - "manifest_version", - "name", - "version", - "description", - "author" - ] - + REQUIRED_FIELDS = ["manifest_version", "name", "version", "description", "author"] + # 可选字段(可以不存在或为空) OPTIONAL_FIELDS = [ "license", "host_application", - "homepage_url", + "homepage_url", "repository_url", "keywords", "categories", "default_locale", "locales_path", - "plugin_info" + "plugin_info", ] - + # 建议填写的字段(会给出警告但不会导致验证失败) - RECOMMENDED_FIELDS = [ - "license", - "keywords", - "categories" - ] - + RECOMMENDED_FIELDS = ["license", "keywords", "categories"] + SUPPORTED_MANIFEST_VERSIONS = [3] - + 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}") - + self.validation_errors.append( + f"不支持的manifest版本: {version},支持的版本: {self.SUPPORTED_MANIFEST_VERSIONS}" + ) + # 检查作者信息格式 if "author" in manifest_data: author = manifest_data["author"] @@ -208,39 +200,41 @@ class ManifestValidator: 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}]") + 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 url_field in manifest_data and manifest_data[url_field]: url = 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: @@ -252,12 +246,12 @@ class ManifestValidator: 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"] @@ -275,121 +269,113 @@ class ManifestValidator: # 检查组件必需字段 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}") + 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("❌ 验证错误:") for error in self.validation_errors: report.append(f" - {error}") - + if self.validation_warnings: report.append("⚠️ 验证警告:") for warning in self.validation_warnings: report.append(f" - {warning}") - + 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", + "version": "1.0.0", "description": "", - "author": { - "name": "", - "url": "" - }, + "author": {"name": "", "url": ""}, "license": "MIT", - "host_application": { - "min_version": "1.0.0", - "max_version": "4.0.0" - }, + "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" + "locales_path": "_locales", } - + def generate_from_plugin(self, plugin_instance) -> 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["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 = { "type": component_info.component_type.value, "name": component_info.name, - "description": component_info.description + "description": component_info.description, } - + # 添加激活模式信息(对于Action组件) - if hasattr(component_class, 'focus_activation_type'): + if hasattr(component_class, "focus_activation_type"): activation_modes = [] - if hasattr(component_class, 'focus_activation_type'): + if hasattr(component_class, "focus_activation_type"): activation_modes.append(component_class.focus_activation_type.value) - if hasattr(component_class, 'normal_activation_type'): + 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 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 - } - + + 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: 是否保存成功 """ @@ -406,30 +392,30 @@ class ManifestGenerator: 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 = json.load(f) - + 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 @@ -437,23 +423,23 @@ def validate_plugin_manifest(plugin_dir: str) -> bool: def generate_plugin_manifest(plugin_instance, 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 diff --git a/src/plugins/built_in/core_actions/plugin.py b/src/plugins/built_in/core_actions/plugin.py index 5df0c07c7..ce5064271 100644 --- a/src/plugins/built_in/core_actions/plugin.py +++ b/src/plugins/built_in/core_actions/plugin.py @@ -340,7 +340,7 @@ class CoreActionsPlugin(BasePlugin): - Reply: 回复动作 - NoReply: 不回复动作 - Emoji: 表情动作 - + 注意:插件基本信息优先从_manifest.json文件中读取 """