516 lines
20 KiB
Python
516 lines
20 KiB
Python
"""
|
||
插件Manifest工具模块
|
||
|
||
提供manifest文件的验证、生成和管理功能
|
||
"""
|
||
|
||
import re
|
||
from typing import Dict, Any, Tuple
|
||
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())
|
||
|
||
# 确保版本号格式正确
|
||
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 "url" in author and author["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 url_field in manifest_data and manifest_data[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:
|
||
# json.dump(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 = 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
|
||
|
||
|
||
# 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
|