⚡ 初始提交 - FutureOSS v1.0 插件化运行时框架
一切皆为插件的开发者工具运行时框架
🧩 核心特性:
- 插件热插拔 (importlib 动态加载)
- 依赖自动解析 (拓扑排序 + 循环检测)
- 企业级稳定 (熔断/降级/重试/隔离)
- 事件驱动 (发布/订阅事件总线)
- 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
BIN
oss/plugin/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/capabilities.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/capabilities.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/event_bus.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/event_bus.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/loader.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/loader.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/manager.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/manager.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/types.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/types.cpython-313.pyc
Normal file
Binary file not shown.
73
oss/plugin/capabilities.py
Normal file
73
oss/plugin/capabilities.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""能力扫描器 - 自动扫描插件支持的能力"""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def scan_capabilities(plugin_dir: Path) -> Any:
|
||||
"""扫描插件目录,自动发现支持的能力"""
|
||||
capabilities: set[str] = set()
|
||||
main_file = plugin_dir / "main.py"
|
||||
|
||||
if not main_file.exists():
|
||||
return capabilities
|
||||
|
||||
with open(main_file, "r", encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
|
||||
tree = ast.parse(source)
|
||||
|
||||
# 扫描规则:
|
||||
# 1. 检查是否导出了特定的类或函数
|
||||
# 2. 检查是否有特定的装饰器或标记
|
||||
# 3. 检查 import 语句(表示依赖了某个能力)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
# 检查类定义
|
||||
if isinstance(node, ast.ClassDef):
|
||||
class_name = node.name
|
||||
# 如果类名包含特定后缀,认为是能力提供者
|
||||
if class_name.endswith("Provider"):
|
||||
cap_name = class_name.replace("Provider", "").lower()
|
||||
capabilities.add(cap_name)
|
||||
elif class_name.endswith("Mixin"):
|
||||
cap_name = class_name.replace("Mixin", "").lower()
|
||||
capabilities.add(cap_name)
|
||||
elif class_name.endswith("Support"):
|
||||
cap_name = class_name.replace("Support", "").lower()
|
||||
capabilities.add(cap_name)
|
||||
|
||||
# 检查函数定义
|
||||
elif isinstance(node, ast.FunctionDef):
|
||||
func_name = node.name
|
||||
# 检查是否有能力相关的装饰器
|
||||
for decorator in node.decorator_list:
|
||||
if isinstance(decorator, ast.Name):
|
||||
if decorator.id.startswith("provides_"):
|
||||
cap_name = decorator.id.replace("provides_", "")
|
||||
capabilities.add(cap_name)
|
||||
elif isinstance(decorator, ast.Attribute):
|
||||
if decorator.attr.startswith("provides_"):
|
||||
cap_name = decorator.attr.replace("provides_", "")
|
||||
capabilities.add(cap_name)
|
||||
|
||||
# 检查 import 语句(表示使用了某个能力)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if "circuit" in alias.name.lower() or "breaker" in alias.name.lower():
|
||||
capabilities.add("circuit_breaker")
|
||||
elif "retry" in alias.name.lower():
|
||||
capabilities.add("retry")
|
||||
elif "cache" in alias.name.lower():
|
||||
capabilities.add("cache")
|
||||
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
if "circuit" in node.module.lower() or "breaker" in node.module.lower():
|
||||
capabilities.add("circuit_breaker")
|
||||
elif "retry" in node.module.lower():
|
||||
capabilities.add("retry")
|
||||
elif "cache" in node.module.lower():
|
||||
capabilities.add("cache")
|
||||
|
||||
return capabilities
|
||||
125
oss/plugin/loader.py
Normal file
125
oss/plugin/loader.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""插件加载器 - 加载基础插件(带沙箱隔离)"""
|
||||
import sys
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from oss.plugin.types import Plugin
|
||||
|
||||
|
||||
class Sandbox:
|
||||
"""沙箱隔离"""
|
||||
|
||||
def __init__(self):
|
||||
self._restricted_builtins = {
|
||||
"__builtins__": {
|
||||
"True": True,
|
||||
"False": False,
|
||||
"None": None,
|
||||
"dict": dict,
|
||||
"list": list,
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"bool": bool,
|
||||
"tuple": tuple,
|
||||
"set": set,
|
||||
"len": len,
|
||||
"range": range,
|
||||
"enumerate": enumerate,
|
||||
"zip": zip,
|
||||
"map": map,
|
||||
"filter": filter,
|
||||
"sorted": sorted,
|
||||
"reversed": reversed,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"sum": sum,
|
||||
"abs": abs,
|
||||
"round": round,
|
||||
"isinstance": isinstance,
|
||||
"issubclass": issubclass,
|
||||
"type": type,
|
||||
"id": id,
|
||||
"hash": hash,
|
||||
"repr": repr,
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"list": list,
|
||||
"dict": dict,
|
||||
"set": set,
|
||||
"tuple": tuple,
|
||||
"print": print,
|
||||
}
|
||||
}
|
||||
|
||||
def get_safe_globals(self) -> dict:
|
||||
"""获取安全的 globals"""
|
||||
return dict(self._restricted_builtins)
|
||||
|
||||
|
||||
class PluginLoader:
|
||||
"""插件加载器(带沙箱隔离)"""
|
||||
|
||||
def __init__(self, enable_sandbox: bool = True):
|
||||
self.loaded: dict[str, Any] = {}
|
||||
self.sandbox = Sandbox() if enable_sandbox else None
|
||||
|
||||
def load_core_plugin(self, plugin_name: str, store_dir: str = "store") -> Optional[dict[str, Any]]:
|
||||
"""加载核心插件(不受沙箱限制)"""
|
||||
plugin_dir = Path(store_dir) / "@{FutureOSS}" / plugin_name
|
||||
return self._load_plugin(plugin_name, plugin_dir, use_sandbox=False, allow_relative=True)
|
||||
|
||||
def load_sandbox_plugin(self, plugin_dir: Path) -> Optional[dict[str, Any]]:
|
||||
"""加载沙箱插件"""
|
||||
plugin_name = plugin_dir.name
|
||||
result = self._load_plugin(plugin_name, plugin_dir, use_sandbox=True, allow_relative=True)
|
||||
return result
|
||||
|
||||
def _load_plugin(self, plugin_name: str, plugin_dir: Path, use_sandbox: bool = True, allow_relative: bool = False) -> Optional[dict[str, Any]]:
|
||||
"""加载插件"""
|
||||
if not (plugin_dir / "main.py").exists():
|
||||
return None
|
||||
|
||||
# 清理插件名(去掉 } 等)
|
||||
clean_name = plugin_name.rstrip("}")
|
||||
module_name = f"plugin.{clean_name}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, str(plugin_dir / "main.py"))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
module.__package__ = module_name
|
||||
module.__path__ = [str(plugin_dir)] # 启用相对导入子模块
|
||||
sys.modules[spec.name] = module
|
||||
|
||||
# 沙箱模式:限制内置函数
|
||||
if use_sandbox and self.sandbox:
|
||||
safe_globals = self.sandbox.get_safe_globals()
|
||||
# 允许导入框架模块
|
||||
safe_globals["__builtins__"]["__import__"] = self._safe_import
|
||||
spec.loader.exec_module(module)
|
||||
else:
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if not hasattr(module, "New"):
|
||||
return None
|
||||
|
||||
instance = module.New()
|
||||
self.loaded[clean_name] = {
|
||||
"instance": instance,
|
||||
"module": module,
|
||||
"path": str(plugin_dir),
|
||||
"name": clean_name, # 存储清理后的名称
|
||||
}
|
||||
return self.loaded[clean_name]
|
||||
|
||||
@staticmethod
|
||||
def _safe_import(name: str, globals: dict = None, locals: dict = None, fromlist: tuple = (), level: int = 0):
|
||||
"""安全导入:只允许导入框架模块、标准库子集和插件自身模块"""
|
||||
allowed_prefixes = ("oss.", "json.", "time.", "datetime.", "enum.", "typing.", "dataclasses.", "pathlib.", "mimetypes.", "http.", "threading.", "socket.", "asyncio.", "websockets.", "re.", "urllib.", "shutil.", "string.", "io.", "base64.", "hashlib.", "hmac.", "secrets.", "math.", "random.", "collections.", "functools.", "itertools.", "operator.", "copy.", "pprint.", "textwrap.", "unicodedata.", "struct.", "codecs.", "locale.", "gettext.", "logging.", "warnings.", "contextlib.", "abc.", "atexit.", "traceback.", "linecache.", "tokenize.", "keyword.", "ast.", "dis.", "inspect.", "types.", "__future__.", "importlib.", "pkgutil.", "sys.", "os.", "stat.", "glob.", "tempfile.", "fnmatch.", "csv.", "configparser.", "argparse.", "html.", "xml.", "email.", "mailbox.", "mimetypes.", "binascii.", "zlib.", "gzip.", "bz2.", "lzma.", "zipfile.", "tarfile.", "sqlite3.", "zlib.")
|
||||
if any(name.startswith(p) for p in allowed_prefixes):
|
||||
return __import__(name, globals, locals, fromlist, level)
|
||||
# 允许相对导入(插件自身模块)
|
||||
if level > 0:
|
||||
return __import__(name, globals, locals, fromlist, level)
|
||||
raise ImportError(f"插件不允许导入模块: {name}")
|
||||
|
||||
33
oss/plugin/manager.py
Normal file
33
oss/plugin/manager.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""插件管理器 - 只加载 plugin-loader"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from oss.plugin.loader import PluginLoader
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""管理基础插件"""
|
||||
|
||||
def __init__(self):
|
||||
self.loader = PluginLoader()
|
||||
self.plugin_loader: Optional[Any] = None
|
||||
|
||||
def load(self):
|
||||
"""加载基础插件"""
|
||||
# 只加载 plugin-loader,其他都是可选的
|
||||
pl_info = self.loader.load_core_plugin("plugin-loader")
|
||||
if pl_info:
|
||||
self.plugin_loader = pl_info["instance"]
|
||||
|
||||
def start(self):
|
||||
"""启动基础插件"""
|
||||
if self.plugin_loader:
|
||||
self.plugin_loader.init()
|
||||
self.plugin_loader.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止基础插件"""
|
||||
if self.plugin_loader:
|
||||
try:
|
||||
self.plugin_loader.stop()
|
||||
except Exception:
|
||||
pass
|
||||
92
oss/plugin/types.py
Normal file
92
oss/plugin/types.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""插件类型定义"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
# ========== 插件自定义类型注册 ==========
|
||||
_plugin_types: dict[str, Any] = {}
|
||||
|
||||
|
||||
def register_plugin_type(type_name: str, type_class: Any):
|
||||
"""注册插件自定义类型"""
|
||||
_plugin_types[type_name] = type_class
|
||||
|
||||
|
||||
def get_plugin_type(type_name: str) -> Optional[Any]:
|
||||
"""获取已注册的插件类型"""
|
||||
return _plugin_types.get(type_name)
|
||||
|
||||
|
||||
def list_plugin_types() -> dict[str, Any]:
|
||||
"""列出所有已注册的插件类型"""
|
||||
return _plugin_types.copy()
|
||||
|
||||
|
||||
# ========== HTTP 响应类型 ==========
|
||||
class Response:
|
||||
"""HTTP 响应对象"""
|
||||
def __init__(self, status: int = 200, headers: Optional[dict[str, str]] = None, body: str = ""):
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self.body = body
|
||||
|
||||
|
||||
# ========== 插件数据类型 ==========
|
||||
class Metadata:
|
||||
"""插件元数据"""
|
||||
def __init__(self, name: str = "", version: str = "", author: str = "", description: str = ""):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.author = author
|
||||
self.description = description
|
||||
|
||||
|
||||
class PluginConfig:
|
||||
"""插件配置"""
|
||||
def __init__(self, enabled: bool = True, args: Optional[dict[str, Any]] = None):
|
||||
self.enabled = enabled
|
||||
self.args = args or {}
|
||||
|
||||
|
||||
class Manifest:
|
||||
"""插件清单"""
|
||||
def __init__(self, metadata: Optional[Metadata] = None, config: Optional[PluginConfig] = None, dependencies: Optional[list[str]] = None):
|
||||
self.metadata = metadata or Metadata()
|
||||
self.config = config or PluginConfig()
|
||||
self.dependencies = dependencies or []
|
||||
|
||||
|
||||
# ========== 插件基类 ==========
|
||||
class Plugin(ABC):
|
||||
"""插件基类"""
|
||||
|
||||
@abstractmethod
|
||||
def init(self, deps: Optional[dict[str, Any]] = None):
|
||||
"""初始化插件"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def start(self):
|
||||
"""启动插件"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def stop(self):
|
||||
"""停止插件"""
|
||||
...
|
||||
|
||||
def meta(self) -> Manifest:
|
||||
"""获取插件元数据"""
|
||||
return Manifest()
|
||||
|
||||
def reload(self, config: Optional[dict[str, Any]] = None):
|
||||
"""热重载插件配置"""
|
||||
pass
|
||||
|
||||
def health(self) -> bool:
|
||||
"""健康检查"""
|
||||
return True
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
"""获取插件统计信息"""
|
||||
return {}
|
||||
Reference in New Issue
Block a user