feat: Enhance plugin runtime with new component registry and workflow executor

- Introduced `ComponentRegistry` for managing plugin components with support for registration, enabling/disabling, and querying by type and plugin.
- Added `EventDispatcher` to handle event distribution to registered event handlers, supporting both blocking and non-blocking execution.
- Implemented `WorkflowExecutor` to manage a linear workflow execution across multiple stages, including command routing and error handling.
- Created `ManifestValidator` for validating plugin manifests against required fields and version compatibility.
- Updated `RPCClient` to use `MsgPackCodec` for message encoding.
- Enhanced `PluginRunner` to support lifecycle hooks for plugins, including `on_load` and `on_unload`.
- Added sys.path isolation to restrict plugin access to only necessary directories.
This commit is contained in:
DrSmoothl
2026-03-06 11:55:59 +08:00
parent 61dc15a513
commit 2f21cd00bc
19 changed files with 1970 additions and 318 deletions

View File

@@ -0,0 +1,137 @@
"""Manifest 校验与版本兼容性
从旧系统的 ManifestValidator / VersionComparator 对齐移植,
适配新 plugin_runtime 的 _manifest.json 格式。
"""
from typing import Any
import logging
import re
logger = logging.getLogger("plugin_runtime.runner.manifest_validator")
class VersionComparator:
"""语义化版本号比较器"""
@staticmethod
def normalize_version(version: str) -> str:
if not version:
return "0.0.0"
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")
return ".".join(parts[:3])
@staticmethod
def parse_version(version: str) -> tuple[int, int, int]:
normalized = VersionComparator.normalize_version(version)
try:
parts = normalized.split(".")
return (int(parts[0]), int(parts[1]), int(parts[2]))
except (ValueError, IndexError):
return (0, 0, 0)
@staticmethod
def compare(v1: str, v2: str) -> int:
t1 = VersionComparator.parse_version(v1)
t2 = VersionComparator.parse_version(v2)
if t1 < t2:
return -1
elif t1 > t2:
return 1
return 0
@staticmethod
def is_in_range(version: str, min_version: str = "", max_version: str = "") -> tuple[bool, str]:
if not min_version and not max_version:
return True, ""
vn = VersionComparator.normalize_version(version)
if min_version:
mn = VersionComparator.normalize_version(min_version)
if VersionComparator.compare(vn, mn) < 0:
return False, f"版本 {vn} 低于最小要求 {mn}"
if max_version:
mx = VersionComparator.normalize_version(max_version)
if VersionComparator.compare(vn, mx) > 0:
return False, f"版本 {vn} 高于最大支持 {mx}"
return True, ""
class ManifestValidator:
"""_manifest.json 校验器"""
REQUIRED_FIELDS = ["name", "version", "description", "author"]
RECOMMENDED_FIELDS = ["license", "keywords", "categories"]
SUPPORTED_MANIFEST_VERSIONS = [1, 2]
def __init__(self, host_version: str = ""):
self._host_version = host_version
self.errors: list[str] = []
self.warnings: list[str] = []
def validate(self, manifest: dict[str, Any]) -> bool:
"""校验 manifest 数据返回是否通过errors 为空即通过)。"""
self.errors.clear()
self.warnings.clear()
self._check_required_fields(manifest)
self._check_manifest_version(manifest)
self._check_author(manifest)
self._check_host_compatibility(manifest)
self._check_recommended(manifest)
if self.errors:
for e in self.errors:
logger.error(f"Manifest 校验失败: {e}")
if self.warnings:
for w in self.warnings:
logger.warning(f"Manifest 警告: {w}")
return len(self.errors) == 0
def _check_required_fields(self, manifest: dict[str, Any]) -> None:
for field in self.REQUIRED_FIELDS:
if field not in manifest:
self.errors.append(f"缺少必需字段: {field}")
elif not manifest[field]:
self.errors.append(f"必需字段不能为空: {field}")
def _check_manifest_version(self, manifest: dict[str, Any]) -> None:
mv = manifest.get("manifest_version")
if mv is not None and mv not in self.SUPPORTED_MANIFEST_VERSIONS:
self.errors.append(
f"不支持的 manifest_version: {mv},支持: {self.SUPPORTED_MANIFEST_VERSIONS}"
)
def _check_author(self, manifest: dict[str, Any]) -> None:
author = manifest.get("author")
if author is None:
return
if isinstance(author, dict):
if "name" not in author or not author["name"]:
self.errors.append("author 对象缺少 name 字段")
elif isinstance(author, str):
if not author.strip():
self.errors.append("author 不能为空")
else:
self.errors.append("author 应为字符串或 {name, url} 对象")
def _check_host_compatibility(self, manifest: dict[str, Any]) -> None:
host_app = manifest.get("host_application")
if not isinstance(host_app, dict) or not self._host_version:
return
min_v = host_app.get("min_version", "")
max_v = host_app.get("max_version", "")
ok, msg = VersionComparator.is_in_range(self._host_version, min_v, max_v)
if not ok:
self.errors.append(f"Host 版本不兼容: {msg} (当前 Host: {self._host_version})")
def _check_recommended(self, manifest: dict[str, Any]) -> None:
for field in self.RECOMMENDED_FIELDS:
if field not in manifest or not manifest[field]:
self.warnings.append(f"建议填写字段: {field}")