重大重构:引擎模块拆分 + P0插件实现 + 55个Bug修复

核心变更:
- engine.py(1781行)拆分为8个独立模块: lifecycle/security/deps/
  datastore/pl_injector/watcher/signature/manager
- 新增plugin-bridge: 事件总线 + 服务注册 + RPC通信
- 新增i18n: 国际化/多语言翻译支持
- 新增plugin-storage: 插件键值/文件存储
- 新增ws-api: WebSocket实时通信(pub/sub + 自定义处理器)
- nodejs-adapter统一为Plugin ABC模式

Bug修复:
- 修复load_all()中store_dir未定义崩溃
- 修复DependencyResolver入度计算(拓扑排序)
- 修复PermissionError隐藏内置异常
- 修复CORS中间件头部未附加到响应
- 修复IntegrityChecker跳过__pycache__目录
- 修复版本号不一致(v2.0.0→v1.2.0)
- 修复测试文件的Logger导入/路径/私有方法调用
- 修复context.py缺少typing导入
- 修复config.py STORE_DIR默认路径(./mods→./store)

测试覆盖: 14→91个测试, 全部通过
This commit is contained in:
Falck
2026-05-12 11:40:06 +08:00
parent 3a096f59a9
commit bce27db4ac
57 changed files with 3669 additions and 2367 deletions

301
oss/core/pl_injector.py Normal file
View File

@@ -0,0 +1,301 @@
from __future__ import annotations
import re
import traceback
from pathlib import Path
from typing import Any, Optional, Callable, TYPE_CHECKING
from oss.logger.logger import Log
if TYPE_CHECKING:
from oss.core.manager import PluginManager
class PLValidationError(Exception):
"""PL 校验错误"""
pass
class PLInjector:
"""PL 注入管理器 - 带完整安全限制"""
MAX_FUNCTIONS_PER_PLUGIN = 50
MAX_REGISTRATIONS_PER_NAME = 10
MAX_NAME_LENGTH = 128
MAX_DESCRIPTION_LENGTH = 256
_FUNCTION_NAME_RE = re.compile(r'^[a-zA-Z0-9_:/\-.]+$')
_EVENT_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]+$')
_ROUTE_PATH_RE = re.compile(r'^/[a-zA-Z0-9_\-/.]+$')
_FORBIDDEN_ROUTE_PATTERNS = [r'\.\.', r'//', r'/\.', r'~', r'\%']
def __init__(self, plugin_manager: PluginManager):
self._plugin_manager = plugin_manager
self._injections: dict = {}
self._injection_registry: dict = {}
self._plugin_function_count: dict = {}
def check_and_load_pl(self, plugin_dir: Path, plugin_name: str) -> bool:
"""检查并加载 PL 文件夹,返回 True 表示成功"""
pl_dir = plugin_dir / "PL"
if not pl_dir.exists() or not pl_dir.is_dir():
Log.warn("Core", f"插件 '{plugin_name}' 声明了 pl_injection但缺少 PL/ 文件夹,拒绝加载")
return False
pl_main = pl_dir / "main.py"
if not pl_main.exists():
Log.warn("Core", f"插件 '{plugin_name}' 的 PL/ 文件夹中缺少 main.py拒绝加载")
return False
# 禁止危险文件类型
forbidden_ext = {'.sh', '.bat', '.exe', '.dll', '.so', '.dylib', '.bin'}
for f in pl_dir.rglob('*'):
if f.suffix.lower() in forbidden_ext:
Log.error("Core", f"插件 '{plugin_name}' 的 PL/ 文件夹包含危险文件: {f.name},拒绝加载")
return False
try:
# 受限沙箱
safe_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,
'print': print, 'object': object, 'property': property,
'staticmethod': staticmethod, 'classmethod': classmethod,
'super': super, 'iter': iter, 'next': next,
'any': any, 'all': all, 'callable': callable,
'hasattr': hasattr, 'getattr': getattr, 'setattr': setattr,
'ValueError': ValueError, 'TypeError': TypeError,
'KeyError': KeyError, 'IndexError': IndexError,
'Exception': Exception, 'BaseException': BaseException,
}
safe_globals = {
'__builtins__': safe_builtins,
'__name__': f'plugin.{plugin_name}.PL',
'__package__': f'plugin.{plugin_name}.PL',
'__file__': str(pl_main),
}
with open(pl_main, 'r', encoding='utf-8') as f:
source = f.read()
# 静态源码安全检查
self._static_source_check(source, str(pl_main))
code = compile(source, str(pl_main), 'exec')
exec(code, safe_globals)
register_func = safe_globals.get('register')
if register_func and callable(register_func):
register_func(self)
Log.ok("Core", f"插件 '{plugin_name}' PL 注入成功")
else:
Log.warn("Core", f"插件 '{plugin_name}' 的 PL/main.py 缺少 register() 函数,但仍允许加载")
self._injections[plugin_name] = {"dir": str(pl_dir)}
return True
except PLValidationError as e:
Log.error("Core", f"插件 '{plugin_name}' PL 安全检查失败: {e}")
return False
except SyntaxError as e:
Log.error("Core", f"插件 '{plugin_name}' PL/main.py 语法错误: {e}")
return False
except FileNotFoundError as e:
Log.error("Core", f"插件 '{plugin_name}' PL 文件不存在:{e}")
return False
except PermissionError as e:
Log.error("Core", f"插件 '{plugin_name}' PL 文件权限错误:{e}")
return False
except Exception as e:
Log.error("Core", f"加载插件 '{plugin_name}' 的 PL 失败:{type(e).__name__}: {e}")
traceback.print_exc()
return False
def _static_source_check(self, source: str, file_path: str):
"""静态源码安全检查 - 增强版,防止字符串拼接/编码绕过"""
import base64
# 首先检查是否有 base64 编码的恶意代码
try:
string_pattern = r'([A-Za-z0-9+/=]{20,})'
for match in re.finditer(string_pattern, source):
try:
decoded = base64.b64decode(match.group(1)).decode('utf-8', errors='ignore')
for dangerous in ['import ', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess']:
if dangerous in decoded:
raise PLValidationError(f"{file_path} - 检测到 base64 编码的恶意代码")
except Exception:
pass
except Exception:
pass
# 检查字符串拼接绕过
concat_patterns = [
r"""['"]ex['"]\s*\+\s*['"]ec['"]""",
r"""['"]impor['"]\s*\+\s*['"]t['"]""",
r"""['"]eva['"]\s*\+\s*['"]l['"]""",
r"""['"]compil['"]\s*\+\s*['"]e['"]""",
]
for pattern in concat_patterns:
if re.search(pattern, source):
raise PLValidationError(f"{file_path} - 检测到字符串拼接绕过尝试")
forbidden = [
(r'^\s*import\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)', '禁止导入系统级模块'),
(r'^\s*from\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)\s+import', '禁止导入系统级模块'),
(r'__import__\s*\(', '禁止使用 __import__'),
(r'(?<![a-zA-Z_])exec\s*\(', '禁止使用 exec'),
(r'(?<![a-zA-Z_])eval\s*\(', '禁止使用 eval'),
(r'(?<![a-zA-Z_])compile\s*\(', '禁止使用 compile'),
(r'(?<![a-zA-Z_])open\s*\(', '禁止直接操作文件'),
(r'__builtins__', '禁止访问 __builtins__'),
(r'getattr\s*\(\s*__builtins__', '禁止通过 getattr 访问 __builtins__'),
(r'setattr\s*\(', '禁止使用 setattr'),
(r'type\s*\(\s*\(\s*[\'"]', '禁止使用 type 动态创建类'),
]
for line_num, line in enumerate(source.split('\n'), 1):
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
for pattern, msg in forbidden:
if re.search(pattern, stripped):
raise PLValidationError(f"{file_path}:{line_num} - {msg}: '{stripped}'")
def _validate_function_name(self, name: str) -> bool:
if not name or not isinstance(name, str):
return False
if len(name) > self.MAX_NAME_LENGTH:
return False
return bool(self._FUNCTION_NAME_RE.match(name))
def _validate_route_path(self, path: str) -> bool:
if not path or not isinstance(path, str):
return False
if len(path) > 256:
return False
if not self._ROUTE_PATH_RE.match(path):
return False
for p in self._FORBIDDEN_ROUTE_PATTERNS:
if re.search(p, path):
return False
return True
def _validate_event_name(self, event_name: str) -> bool:
if not event_name or not isinstance(event_name, str):
return False
if len(event_name) > self.MAX_NAME_LENGTH:
return False
return bool(self._EVENT_NAME_RE.match(event_name))
def _check_plugin_limit(self, plugin_name: str) -> bool:
count = self._plugin_function_count.get(plugin_name, 0)
if count >= self.MAX_FUNCTIONS_PER_PLUGIN:
Log.warn("Core", f"插件 '{plugin_name}' 注册功能数已达上限 ({self.MAX_FUNCTIONS_PER_PLUGIN})")
return False
return True
def _check_name_limit(self, name: str) -> bool:
registrations = self._injection_registry.get(name, [])
if len(registrations) >= self.MAX_REGISTRATIONS_PER_NAME:
Log.warn("Core", f"功能名称 '{name}' 注册次数已达上限 ({self.MAX_REGISTRATIONS_PER_NAME})")
return False
return True
def _wrap_function(self, func: Callable, plugin_name: str, name: str) -> Callable:
"""包装函数,异常安全"""
def _safe_wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
Log.error("Core", f"PL 注入功能 '{name}' (来自 {plugin_name}) 执行异常: {e}")
return None
return _safe_wrapper
def _get_caller_plugin_name(self) -> Optional[str]:
"""通过栈帧回溯获取调用者插件名"""
stack = traceback.extract_stack()
for frame in stack:
filename = frame.filename
if '/PL/' in filename and 'main.py' in filename:
parts = Path(filename).parts
for i, part in enumerate(parts):
if part == 'PL':
return parts[i - 1] if i > 0 else None
return None
def register_function(self, name: str, func: Callable, description: str = ""):
"""注册注入功能 - 带参数校验和权限限制"""
if not self._validate_function_name(name):
Log.error("Core", f"PL 注入功能名称非法: '{name}'")
return
if not callable(func):
Log.error("Core", f"PL 注入功能 '{name}' 不是可调用对象")
return
if description and len(description) > self.MAX_DESCRIPTION_LENGTH:
description = description[:self.MAX_DESCRIPTION_LENGTH]
plugin_name = self._get_caller_plugin_name() or "unknown"
if not self._check_plugin_limit(plugin_name):
return
if not self._check_name_limit(name):
return
wrapped_func = self._wrap_function(func, plugin_name, name)
if name not in self._injection_registry:
self._injection_registry[name] = []
self._injection_registry[name].append({
"func": wrapped_func, "plugin": plugin_name, "description": description,
})
self._plugin_function_count[plugin_name] = self._plugin_function_count.get(plugin_name, 0) + 1
Log.tip("Core", f"PL 注入功能已注册: '{name}' (来自 {plugin_name})")
def register_route(self, method: str, path: str, handler: Callable):
"""注册 HTTP 路由 - 带路径安全校验"""
valid_methods = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'}
method_upper = method.upper()
if method_upper not in valid_methods:
Log.error("Core", f"PL 注入路由方法非法: '{method}'")
return
if not self._validate_route_path(path):
Log.error("Core", f"PL 注入路由路径非法: '{path}'")
return
self.register_function(f"{method_upper}:{path}", handler, f"路由 {method_upper} {path}")
def register_event_handler(self, event_name: str, handler: Callable):
"""注册事件处理器 - 带名称校验"""
if not self._validate_event_name(event_name):
Log.error("Core", f"PL 注入事件名称非法: '{event_name}'")
return
self.register_function(f"event:{event_name}", handler, f"事件 {event_name}")
def get_injected_functions(self, name: str = None) -> list[Callable]:
if name:
return [e["func"] for e in self._injection_registry.get(name, [])]
return [f for es in self._injection_registry.values() for f in [e["func"] for e in es]]
def get_injection_info(self, plugin_name: str = None) -> dict:
if plugin_name:
return self._injections.get(plugin_name, {})
return dict(self._injections)
def has_injection(self, plugin_name: str) -> bool:
return plugin_name in self._injections
def get_registry_info(self) -> dict:
info = {}
for name, entries in self._injection_registry.items():
info[name] = {
"count": len(entries),
"plugins": [e["plugin"] for e in entries],
"descriptions": [e["description"] for e in entries],
}
return info