更改项目名为NebulaShell
This commit is contained in:
172
store/@{NebulaShell}/plugin-loader/PL_EXAMPLE.md
Normal file
172
store/@{NebulaShell}/plugin-loader/PL_EXAMPLE.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# PL 注入机制使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
PL 注入机制允许插件通过 `PL/` 文件夹向插件加载器注册自定义功能。插件加载器在启动时会自动扫描所有插件,检查其 `manifest.json` 中是否声明了 `pl_injection` 配置项。
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. 在 manifest.json 中声明 pl_injection
|
||||
|
||||
在插件的 `manifest.json` 的 `config.args` 中添加 `"pl_injection": true`:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "my-plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "MyName",
|
||||
"description": "我的插件",
|
||||
"type": "utility"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"pl_injection": true
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建 PL/ 文件夹和 PL/main.py
|
||||
|
||||
在插件目录下创建 `PL/` 文件夹,并在其中创建 `main.py`:
|
||||
|
||||
```
|
||||
store/@{MyName}/my-plugin/
|
||||
├── manifest.json # 声明 pl_injection: true
|
||||
├── main.py # 插件主逻辑
|
||||
├── PL/ # PL 注入文件夹
|
||||
│ └── main.py # 注入逻辑(必须包含 register() 函数)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 3. 实现 PL/main.py
|
||||
|
||||
`PL/main.py` 必须导出一个 `register(injector)` 函数,接收一个 `PLInjector` 实例:
|
||||
|
||||
```python
|
||||
# PL/main.py
|
||||
"""PL 注入 - 向插件加载器注册功能"""
|
||||
|
||||
def register(injector):
|
||||
"""向插件加载器注册功能
|
||||
|
||||
Args:
|
||||
injector: PLInjector 实例,提供以下注册方法:
|
||||
- register_function(name, func, description="")
|
||||
- register_route(method, path, handler)
|
||||
- register_event_handler(event_name, handler)
|
||||
"""
|
||||
|
||||
# 示例 1: 注册一个普通功能
|
||||
def my_helper():
|
||||
print("这是从 PL 注入的功能")
|
||||
|
||||
injector.register_function("my_helper", my_helper, "一个辅助功能")
|
||||
|
||||
# 示例 2: 注册 HTTP 路由
|
||||
def hello_handler(request):
|
||||
return {"message": "Hello from PL injection!"}
|
||||
|
||||
injector.register_route("GET", "/pl/hello", hello_handler)
|
||||
|
||||
# 示例 3: 注册事件处理器
|
||||
def on_plugin_started(plugin_name):
|
||||
print(f"插件 {plugin_name} 已启动")
|
||||
|
||||
injector.register_event_handler("plugin.started", on_plugin_started)
|
||||
```
|
||||
|
||||
### 4. 引用其他文件
|
||||
|
||||
`PL/main.py` 可以引用 `PL/` 文件夹下的其他 Python 文件:
|
||||
|
||||
```
|
||||
store/@{MyName}/my-plugin/PL/
|
||||
├── main.py # 入口,包含 register() 函数
|
||||
├── helpers.py # 辅助函数(被 main.py 引用)
|
||||
└── routes.py # 路由定义(被 main.py 引用)
|
||||
```
|
||||
|
||||
```python
|
||||
# PL/main.py
|
||||
from .helpers import format_response
|
||||
from .routes import register_routes
|
||||
|
||||
def register(injector):
|
||||
def my_handler():
|
||||
return format_response("Hello")
|
||||
injector.register_function("my_handler", my_handler)
|
||||
register_routes(injector)
|
||||
```
|
||||
|
||||
## 行为说明
|
||||
|
||||
| 场景 | 结果 |
|
||||
|------|------|
|
||||
| manifest.json 中 `pl_injection: true` + 存在 `PL/main.py` | ✅ 正常加载,执行注入 |
|
||||
| manifest.json 中 `pl_injection: true` + 缺少 `PL/` 文件夹 | ❌ 警告并拒绝加载该插件 |
|
||||
| manifest.json 中 `pl_injection: true` + 存在 `PL/` 但缺少 `main.py` | ❌ 警告并拒绝加载该插件 |
|
||||
| manifest.json 中未声明 `pl_injection` | ✅ 正常加载,跳过 PL 检查 |
|
||||
| manifest.json 中 `pl_injection: false` | ✅ 正常加载,跳过 PL 检查 |
|
||||
|
||||
## 安全限制
|
||||
|
||||
PL 注入机制实施了多层安全限制,防止恶意代码注入:
|
||||
|
||||
### 1. 文件类型限制
|
||||
- PL 文件夹中禁止包含 `.sh`、`.bat`、`.exe`、`.dll`、`.so`、`.dylib`、`.bin` 等可执行/二进制文件
|
||||
- 违反则拒绝加载该插件
|
||||
|
||||
### 2. 静态源码安全检查
|
||||
PL/main.py 源码在编译前会进行静态扫描,禁止以下操作:
|
||||
- 导入系统级模块(`os`、`sys`、`subprocess`、`shutil`、`socket`、`ctypes`、`cffi`、`multiprocessing`、`threading`)
|
||||
- 使用 `__import__`、`exec`、`eval`、`compile`
|
||||
- 直接操作文件(`open`)
|
||||
- 访问 `__builtins__`
|
||||
|
||||
### 3. 沙箱执行环境
|
||||
PL/main.py 在受限的沙箱中执行,仅提供安全的 builtins:
|
||||
- 基础类型:`dict`、`list`、`str`、`int`、`float`、`bool`、`tuple`、`set`
|
||||
- 安全函数:`len`、`range`、`enumerate`、`zip`、`map`、`filter`、`sorted` 等
|
||||
- 异常类型:`Exception`、`ValueError`、`TypeError`、`KeyError`、`IndexError`
|
||||
|
||||
### 4. 参数校验
|
||||
| 校验项 | 限制 |
|
||||
|--------|------|
|
||||
| 功能名称 | 仅允许字母、数字、下划线、冒号、斜杠、连字符、点,最长 128 字符 |
|
||||
| 路由路径 | 必须以 `/` 开头,禁止 `..`、`//`、`/\.`、`~`、`%`,最长 256 字符 |
|
||||
| HTTP 方法 | 仅允许 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS |
|
||||
| 事件名称 | 字母开头,仅允许字母、数字、点、下划线,最长 128 字符 |
|
||||
| 功能描述 | 最长 256 字符 |
|
||||
|
||||
### 5. 数量限制
|
||||
| 限制项 | 上限 |
|
||||
|--------|------|
|
||||
| 每个插件最多注册的功能数 | 50 |
|
||||
| 每个功能名称最多被注册次数 | 10 |
|
||||
|
||||
### 6. 异常安全
|
||||
- 所有注册的函数会被自动包装,执行时抛出异常不会影响主流程
|
||||
- 异常会被记录到日志,函数返回 `None`
|
||||
|
||||
### 7. 调用者溯源
|
||||
- 通过栈帧回溯自动识别调用者插件名
|
||||
- 防止其他插件冒充注册
|
||||
|
||||
## 注入器 API
|
||||
|
||||
`PLInjector` 实例提供以下方法供 `PL/main.py` 调用:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `register_function(name, func, description="")` | 注册一个注入功能 |
|
||||
| `register_route(method, path, handler)` | 注册 HTTP 路由 |
|
||||
| `register_event_handler(event_name, handler)` | 注册事件处理器 |
|
||||
| `get_injected_functions(name=None)` | 获取已注册的注入功能 |
|
||||
| `get_injection_info(plugin_name=None)` | 获取注入信息 |
|
||||
| `has_injection(plugin_name)` | 检查插件是否有 PL 注入 |
|
||||
| `get_registry_info()` | 获取注册表完整信息(用于监控) |
|
||||
16
store/@{NebulaShell}/plugin-loader/README.md
Normal file
16
store/@{NebulaShell}/plugin-loader/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# plugin-loader 插件加载器
|
||||
|
||||
核心插件,负责扫描、加载和管理所有其他插件。
|
||||
|
||||
## 功能
|
||||
|
||||
- 自动扫描 `store/` 目录
|
||||
- 动态加载 `main.py` 并调用 `New()` 获取实例
|
||||
- 解析 `manifest.json` 获取插件元数据
|
||||
- 自动扫描插件能力(AST 分析)
|
||||
- 按依赖关系排序加载顺序
|
||||
- 关联能力提供者与消费者
|
||||
|
||||
## 使用
|
||||
|
||||
无需手动使用,框架启动时自动加载。
|
||||
8
store/@{NebulaShell}/plugin-loader/SIGNATURE
Normal file
8
store/@{NebulaShell}/plugin-loader/SIGNATURE
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"signature": "XqvMe6BmA+hE8PqWCdKTDT8B9lKt2Oo9ZzZ0/KNlHEM3mk3cRzs4ZCUR4Qx4veqDAycTixT0cwp0hSh0VGNjBTg/hqVkmsDMb02Ky1TUhM1VGXYoffaKP7F5cqVLRc6Le0/mrL8MtdUUxt5USsdpFCuF+LLs+HQ2w/xPZ50n6GwdFIE2cvQJUGpMjLgI7jebmTFFLeED/DK9v9Pki1n27R3tvV333h9SAMO6L30IwJy6dwpssZb60RxLMkYvwokWYRePHKWzfdS9+huZ0o8fK6bYcs2CtBzZ4RDpwojSBPElIaBdn647+kspVTefEFlXvamdPM42pkojWsMU4Ed2Hgnasrz1aAlL7u94b6zcjOWQguRNgVsWFB8kKFR0nLaKUWQvULtDduEFpegU/dI0u1zZuRVmd58TSaLVXReUuARG0viop04pxiqf3H2IGwEafzlprnwQe9IWINgvABdC34UpCw/enBRUj2gjan2Up7nRhz0CMAUKo1TBhRMErp5f0AthZwbHrrq3g5wwKRoftV6O7GSiirbPSMe/ypb5mkdQmdHqOUvhlCexeeMhKB/9J7e2UhJ8YSlq7uZMrMc8dEWwkqMQeiw1uOCnCujlHYfk2RmPRwEZTUB/VQJmYuJhzSuI1XXA52ZcJaHf7Bh62d/ftMZ9OQimTpJg4y365jk=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775970391.1348627,
|
||||
"plugin_hash": "0052362f57f6c9b50adc7ff19a37fa57344f298eade3dd5152c916054879b846",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
754
store/@{NebulaShell}/plugin-loader/main.py
Normal file
754
store/@{NebulaShell}/plugin-loader/main.py
Normal file
@@ -0,0 +1,754 @@
|
||||
"""插件加载器插件 - 支持能力扫描和扩展 + PL 注入机制"""
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import types
|
||||
import traceback
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Callable
|
||||
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
from oss.plugin.capabilities import scan_capabilities
|
||||
|
||||
|
||||
class Log:
|
||||
"""智能彩色日志"""
|
||||
_TTY = sys.stdout.isatty()
|
||||
_C = {"reset": "\033[0m", "white": "\033[0;37m", "yellow": "\033[1;33m", "blue": "\033[1;34m", "red": "\033[1;31m"}
|
||||
|
||||
@classmethod
|
||||
def c(cls, text: str, color: str) -> str:
|
||||
if not cls._TTY: return text
|
||||
return f"{cls._C.get(color, '')}{text}{cls._C['reset']}"
|
||||
|
||||
@classmethod
|
||||
def info(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'white')} {cls.c(msg, 'white')}")
|
||||
@classmethod
|
||||
def warn(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'yellow')} {cls.c('⚠', 'yellow')} {cls.c(msg, 'yellow')}")
|
||||
@classmethod
|
||||
def tip(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'blue')} {cls.c('ℹ', 'blue')} {cls.c(msg, 'blue')}")
|
||||
@classmethod
|
||||
def error(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'red')} {cls.c('✗', 'red')} {cls.c(msg, 'red')}")
|
||||
@classmethod
|
||||
def ok(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'white')} {cls.c(msg, 'white')}")
|
||||
|
||||
|
||||
class PluginInfo:
|
||||
"""插件信息"""
|
||||
def __init__(self):
|
||||
self.name: str = ""
|
||||
self.version: str = ""
|
||||
self.author: str = ""
|
||||
self.description: str = ""
|
||||
self.readme: str = ""
|
||||
self.config: dict[str, Any] = {}
|
||||
self.extensions: dict[str, Any] = {}
|
||||
self.lifecycle: Any = None
|
||||
self.capabilities: set[str] = set()
|
||||
self.dependencies: list[str] = []
|
||||
self.pl_injected: bool = False
|
||||
|
||||
|
||||
class PermissionError(Exception):
|
||||
"""权限错误"""
|
||||
pass
|
||||
|
||||
|
||||
class PluginProxy:
|
||||
"""插件代理 - 防止越级访问"""
|
||||
def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict):
|
||||
self._plugin_name = plugin_name
|
||||
self._plugin_instance = plugin_instance
|
||||
self._allowed_plugins = set(allowed_plugins)
|
||||
self._all_plugins = all_plugins
|
||||
|
||||
def get_plugin(self, name: str) -> Any:
|
||||
if name not in self._allowed_plugins and "*" not in self._allowed_plugins:
|
||||
raise PermissionError(f"插件 '{self._plugin_name}' 无权访问插件 '{name}'")
|
||||
if name not in self._all_plugins: return None
|
||||
return self._all_plugins[name]["instance"]
|
||||
|
||||
def list_plugins(self) -> list[str]:
|
||||
if "*" in self._allowed_plugins: return list(self._all_plugins.keys())
|
||||
return [n for n in self._allowed_plugins if n in self._all_plugins]
|
||||
|
||||
def get_capability(self, capability: str) -> Any: return None
|
||||
def __getattr__(self, name: str): return getattr(self._plugin_instance, name)
|
||||
|
||||
|
||||
class CapabilityRegistry:
|
||||
"""能力注册表"""
|
||||
def __init__(self, permission_check: bool = True):
|
||||
self.providers: dict = {}
|
||||
self.consumers: dict = {}
|
||||
self.permission_check = permission_check
|
||||
|
||||
def register_provider(self, capability: str, plugin_name: str, instance: Any):
|
||||
self.providers[capability] = {"plugin": plugin_name, "instance": instance}
|
||||
if capability not in self.consumers: self.consumers[capability] = []
|
||||
|
||||
def register_consumer(self, capability: str, plugin_name: str):
|
||||
if capability not in self.consumers: self.consumers[capability] = []
|
||||
if plugin_name not in self.consumers[capability]: self.consumers[capability].append(plugin_name)
|
||||
|
||||
def get_provider(self, capability: str, requester: str = "", allowed_plugins: list = None) -> Optional[Any]:
|
||||
if capability not in self.providers: return None
|
||||
if self.permission_check and allowed_plugins is not None:
|
||||
pn = self.providers[capability]["plugin"]
|
||||
if pn != requester and pn not in allowed_plugins and "*" not in allowed_plugins:
|
||||
raise PermissionError(f"插件 '{requester}' 无权使用能力 '{capability}'")
|
||||
return self.providers[capability]["instance"]
|
||||
|
||||
def has_capability(self, capability: str) -> bool: return capability in self.providers
|
||||
def get_consumers(self, capability: str) -> list: return self.consumers.get(capability, [])
|
||||
|
||||
|
||||
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("plugin-loader", f"插件 '{plugin_name}' 声明了 pl_injection,但缺少 PL/ 文件夹,拒绝加载")
|
||||
return False
|
||||
|
||||
pl_main = pl_dir / "main.py"
|
||||
if not pl_main.exists():
|
||||
Log.warn("plugin-loader", 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("plugin-loader", 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("plugin-loader", f"插件 '{plugin_name}' PL 注入成功")
|
||||
else:
|
||||
Log.warn("plugin-loader", f"插件 '{plugin_name}' 的 PL/main.py 缺少 register() 函数,但仍允许加载")
|
||||
|
||||
self._injections[plugin_name] = {"dir": str(pl_dir)}
|
||||
return True
|
||||
|
||||
except PLValidationError as e:
|
||||
Log.error("plugin-loader", f"插件 '{plugin_name}' PL 安全检查失败: {e}")
|
||||
return False
|
||||
except SyntaxError as e:
|
||||
Log.error("plugin-loader", f"插件 '{plugin_name}' PL/main.py 语法错误: {e}")
|
||||
return False
|
||||
except FileNotFoundError as e:
|
||||
Log.error("plugin-loader", f"插件 '{plugin_name}' PL 文件不存在:{e}")
|
||||
return False
|
||||
except PermissionError as e:
|
||||
Log.error("plugin-loader", f"插件 '{plugin_name}' PL 文件权限错误:{e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
Log.error("plugin-loader", f"加载插件 '{plugin_name}' 的 PL 失败:{type(e).__name__}: {e}")
|
||||
import traceback
|
||||
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:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# 检查字符串拼接绕过 (如 'ex' + 'ec')
|
||||
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("plugin-loader", 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("plugin-loader", 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("plugin-loader", 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("plugin-loader", f"PL 注入功能名称非法: '{name}'")
|
||||
return
|
||||
if not callable(func):
|
||||
Log.error("plugin-loader", 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("plugin-loader", 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("plugin-loader", f"PL 注入路由方法非法: '{method}'")
|
||||
return
|
||||
if not self._validate_route_path(path):
|
||||
Log.error("plugin-loader", 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("plugin-loader", 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
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""插件管理器"""
|
||||
|
||||
def __init__(self, permission_check: bool = True):
|
||||
self.plugins: dict = {}
|
||||
self.lifecycle_plugin = None
|
||||
self._dependency_plugin = None
|
||||
self._signature_verifier = None
|
||||
self.capability_registry = CapabilityRegistry(permission_check=permission_check)
|
||||
self.permission_check = permission_check
|
||||
self.enforce_signature = True
|
||||
self.pl_injector = PLInjector(self)
|
||||
|
||||
def set_signature_verifier(self, verifier): self._signature_verifier = verifier
|
||||
def set_lifecycle(self, lifecycle_plugin): self.lifecycle_plugin = lifecycle_plugin
|
||||
|
||||
def _load_manifest(self, plugin_dir: Path) -> dict:
|
||||
mf = plugin_dir / "manifest.json"
|
||||
if not mf.exists(): return {}
|
||||
with open(mf, "r", encoding="utf-8") as f: return json.load(f)
|
||||
|
||||
def _load_readme(self, plugin_dir: Path) -> str:
|
||||
rf = plugin_dir / "README.md"
|
||||
if not rf.exists(): return ""
|
||||
with open(rf, "r", encoding="utf-8") as f: return f.read()
|
||||
|
||||
def _load_config(self, plugin_dir: Path) -> dict:
|
||||
"""加载插件配置文件 - 使用 ast.literal_eval 安全解析"""
|
||||
import ast
|
||||
cf = plugin_dir / "config.py"
|
||||
if not cf.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(cf, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
Log.warn("plugin-loader", f"配置文件不存在:{cf}")
|
||||
return {}
|
||||
except PermissionError as e:
|
||||
Log.error("plugin-loader", f"配置文件无权限读取:{cf} - {e}")
|
||||
return {}
|
||||
except UnicodeDecodeError as e:
|
||||
Log.error("plugin-loader", f"配置文件编码错误:{cf} - {e}")
|
||||
return {}
|
||||
|
||||
# 严格检查:不允许任何代码执行
|
||||
for p in ['import ', 'from ', 'open(', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess', 'lambda', 'def ', 'class ']:
|
||||
if p in content:
|
||||
Log.warn("plugin-loader", f"{cf} 包含危险代码:{p}")
|
||||
return {}
|
||||
|
||||
# 尝试使用 ast.literal_eval 安全解析
|
||||
try:
|
||||
result = ast.literal_eval(content)
|
||||
if isinstance(result, dict):
|
||||
return {k: v for k, v in result.items() if not k.startswith("_")}
|
||||
except (ValueError, SyntaxError):
|
||||
pass
|
||||
|
||||
# 如果失败,尝试提取简单的键值对
|
||||
config = {}
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line)
|
||||
if match:
|
||||
key, value_str = match.groups()
|
||||
if key.startswith('_'):
|
||||
continue
|
||||
try:
|
||||
value = ast.literal_eval(value_str)
|
||||
config[key] = value
|
||||
except (ValueError, SyntaxError):
|
||||
Log.warn("plugin-loader", f"{cf} 跳过无效的值:{line}")
|
||||
continue
|
||||
return config
|
||||
|
||||
|
||||
def _load_extensions(self, plugin_dir: Path) -> dict:
|
||||
"""加载插件扩展配置 - 使用 ast.literal_eval 安全解析"""
|
||||
import ast
|
||||
ef = plugin_dir / "extensions.py"
|
||||
if not ef.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(ef, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
Log.error("plugin-loader", f"扩展文件读取失败:{e}")
|
||||
return {}
|
||||
|
||||
# 严格检查:不允许任何代码执行
|
||||
for p in ['import ', 'from ', 'open(', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess', 'lambda', 'def ', 'class ']:
|
||||
if p in content:
|
||||
Log.warn("plugin-loader", f"{ef} 包含危险代码:{p}")
|
||||
return {}
|
||||
|
||||
# 尝试使用 ast.literal_eval 安全解析
|
||||
try:
|
||||
result = ast.literal_eval(content)
|
||||
if isinstance(result, dict):
|
||||
return {k: v for k, v in result.items() if not k.startswith("_")}
|
||||
except (ValueError, SyntaxError):
|
||||
pass
|
||||
|
||||
# 如果失败,尝试提取简单的键值对
|
||||
extensions = {}
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line)
|
||||
if match:
|
||||
key, value_str = match.groups()
|
||||
if key.startswith('_'):
|
||||
continue
|
||||
try:
|
||||
value = ast.literal_eval(value_str)
|
||||
extensions[key] = value
|
||||
except (ValueError, SyntaxError):
|
||||
Log.warn("plugin-loader", f"{ef} 跳过无效的值:{line}")
|
||||
continue
|
||||
return extensions
|
||||
|
||||
|
||||
def load(self, plugin_dir: Path, use_sandbox: bool = True) -> Optional[Any]:
|
||||
"""加载单个插件"""
|
||||
main_file = plugin_dir / "main.py"
|
||||
if not main_file.exists(): return None
|
||||
|
||||
manifest = self._load_manifest(plugin_dir)
|
||||
readme = self._load_readme(plugin_dir)
|
||||
config = self._load_config(plugin_dir)
|
||||
extensions = self._load_extensions(plugin_dir)
|
||||
capabilities = scan_capabilities(plugin_dir)
|
||||
plugin_name = plugin_dir.name.rstrip("}")
|
||||
|
||||
# PL 注入检查
|
||||
pl_injection = manifest.get("config", {}).get("args", {}).get("pl_injection", False)
|
||||
if pl_injection:
|
||||
Log.tip("plugin-loader", f"插件 '{plugin_name}' 声明了 pl_injection,正在检查 PL/ 文件夹...")
|
||||
if not self.pl_injector.check_and_load_pl(plugin_dir, plugin_name):
|
||||
Log.error("plugin-loader", f"插件 '{plugin_name}' 因 PL 注入检查失败被拒绝加载")
|
||||
return None
|
||||
Log.ok("plugin-loader", f"插件 '{plugin_name}' PL 注入检查通过")
|
||||
|
||||
permissions = manifest.get("permissions", [])
|
||||
|
||||
# 不再使用沙箱,所有插件都直接加载(核心插件是可信的)
|
||||
# use_sandbox 参数保留但不再实际使用
|
||||
spec = importlib.util.spec_from_file_location(f"plugin.{plugin_name}", str(main_file))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
module.__package__ = f"plugin.{plugin_name}"
|
||||
module.__path__ = [str(plugin_dir)]
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
if not hasattr(module, "New"):
|
||||
return None
|
||||
instance = module.New()
|
||||
|
||||
if self.permission_check and permissions:
|
||||
instance = PluginProxy(plugin_name, instance, permissions, self.plugins)
|
||||
|
||||
info = PluginInfo()
|
||||
meta = manifest.get("metadata", {})
|
||||
info.name = meta.get("name", plugin_name)
|
||||
info.version = meta.get("version", "")
|
||||
info.author = meta.get("author", "")
|
||||
info.description = meta.get("description", "")
|
||||
info.readme = readme
|
||||
info.config = manifest.get("config", {}).get("args", config)
|
||||
info.extensions = extensions
|
||||
info.capabilities = capabilities
|
||||
info.dependencies = manifest.get("dependencies", [])
|
||||
info.pl_injected = pl_injection
|
||||
|
||||
for cap in capabilities:
|
||||
self.capability_registry.register_provider(cap, plugin_name, instance)
|
||||
if self.lifecycle_plugin and plugin_name != "lifecycle":
|
||||
info.lifecycle = self.lifecycle_plugin.create(plugin_name)
|
||||
|
||||
self.plugins[plugin_name] = {"instance": instance, "module": module, "info": info, "permissions": permissions}
|
||||
return instance
|
||||
|
||||
def load_all(self, store_dir: str = "store"):
|
||||
if 'plugin' not in sys.modules:
|
||||
pkg = types.ModuleType('plugin')
|
||||
pkg.__path__ = []; pkg.__package__ = 'plugin'
|
||||
sys.modules['plugin'] = pkg
|
||||
Log.tip("plugin-loader", "已创建 plugin 命名空间包")
|
||||
|
||||
if not self._check_any_plugins(store_dir):
|
||||
Log.warn("plugin-loader", "未检测到任何插件,自动引导安装...")
|
||||
self._bootstrap_installation()
|
||||
|
||||
lifecycle_plugin = None
|
||||
lc_dir = Path(store_dir) / "@{NebulaShell}" / "lifecycle"
|
||||
if lc_dir.exists() and (lc_dir / "main.py").exists():
|
||||
try:
|
||||
inst = self.load(lc_dir)
|
||||
if inst: lifecycle_plugin = inst; self.plugins.pop("lifecycle", None)
|
||||
except Exception as e: Log.warn("plugin-loader", f"lifecycle 插件加载失败:{type(e).__name__}: {e}")
|
||||
|
||||
dep_plugin = None
|
||||
dep_dir = Path(store_dir) / "@{NebulaShell}" / "dependency"
|
||||
if dep_dir.exists() and (dep_dir / "main.py").exists():
|
||||
try:
|
||||
inst = self.load(dep_dir)
|
||||
if inst: dep_plugin = inst; self._dependency_plugin = inst; self.plugins.pop("dependency", None)
|
||||
except Exception as e: Log.warn("plugin-loader", f"dependency 插件加载失败:{type(e).__name__}: {e}")
|
||||
|
||||
sig_dir = Path(store_dir) / "@{NebulaShell}" / "signature-verifier"
|
||||
if sig_dir.exists() and (sig_dir / "main.py").exists():
|
||||
try:
|
||||
inst = self.load(sig_dir)
|
||||
if inst: self.set_signature_verifier(inst.verifier); Log.ok("plugin-loader", "签名验证服务已加载")
|
||||
except Exception as e: Log.warn("plugin-loader", f"signature-verifier 加载失败: {e}")
|
||||
|
||||
if lifecycle_plugin: self.set_lifecycle(lifecycle_plugin)
|
||||
self._load_plugins_from_dir(Path(store_dir))
|
||||
if dep_plugin: self._sort_by_dependencies(dep_plugin)
|
||||
|
||||
def _load_plugins_from_dir(self, store_dir: Path):
|
||||
if not store_dir.exists(): return
|
||||
core_plugins = {"webui", "dashboard", "pkg-manager"}
|
||||
skip = {"plugin-loader", "lifecycle", "dependency", "signature-verifier"}
|
||||
for ad in store_dir.iterdir():
|
||||
if ad.is_dir():
|
||||
for pd in ad.iterdir():
|
||||
if pd.is_dir() and pd.name not in skip and (pd / "main.py").exists():
|
||||
self.load(pd, use_sandbox=pd.name not in core_plugins)
|
||||
self._link_capabilities()
|
||||
|
||||
def _check_any_plugins(self, store_dir: str) -> bool:
|
||||
sp = Path(store_dir)
|
||||
if sp.exists():
|
||||
for ad in sp.iterdir():
|
||||
if ad.is_dir():
|
||||
for pd in ad.iterdir():
|
||||
if pd.is_dir() and (pd / "main.py").exists(): return True
|
||||
return False
|
||||
|
||||
def _bootstrap_installation(self): Log.info("plugin-loader", "跳过引导安装(pkg 插件已移除)")
|
||||
|
||||
def _sort_by_dependencies(self, dep_plugin):
|
||||
if not dep_plugin: return
|
||||
for n, i in self.plugins.items(): dep_plugin.add_plugin(n, i["info"].dependencies)
|
||||
try:
|
||||
order = dep_plugin.resolve()
|
||||
sp = {}
|
||||
for n in order:
|
||||
if n in self.plugins: sp[n] = self.plugins[n]
|
||||
for n in set(self.plugins.keys()) - set(sp.keys()): sp[n] = self.plugins[n]
|
||||
self.plugins = sp
|
||||
except Exception as e: Log.error("plugin-loader", f"依赖解析失败: {e}")
|
||||
|
||||
def _link_capabilities(self):
|
||||
for pn, info in self.plugins.items():
|
||||
for cap in info["info"].capabilities:
|
||||
if self.capability_registry.has_capability(cap):
|
||||
for cn in self.capability_registry.get_consumers(cap):
|
||||
if cn in self.plugins:
|
||||
ci = self.plugins[cn]["info"]
|
||||
ca = self.plugins[cn].get("permissions", [])
|
||||
try:
|
||||
p = self.capability_registry.get_provider(cap, requester=cn, allowed_plugins=ca)
|
||||
if p and hasattr(ci, "extensions"): ci.extensions[f"_{cap}_provider"] = p
|
||||
except PermissionError as e: Log.error("plugin-loader", f"权限拒绝: {e}")
|
||||
|
||||
def start_all(self):
|
||||
self._inject_dependencies()
|
||||
for n, i in self.plugins.items():
|
||||
try: i["instance"].start()
|
||||
except Exception as e: Log.error("plugin-loader", f"启动失败 {n}: {e}")
|
||||
|
||||
def init_and_start_all(self):
|
||||
Log.info("plugin-loader", f"init_and_start_all 被调用,plugins={len(self.plugins)}")
|
||||
self._inject_dependencies()
|
||||
ordered = self._get_ordered_plugins()
|
||||
Log.tip("plugin-loader", f"插件启动顺序: {' -> '.join(ordered)}")
|
||||
for name in ordered:
|
||||
if "plugin-loader" in name: continue
|
||||
try:
|
||||
Log.info("plugin-loader", f"初始化: {name}")
|
||||
self.plugins[name]["instance"].init()
|
||||
except Exception as e: Log.error("plugin-loader", f"初始化失败 {name}: {e}")
|
||||
for name in ordered:
|
||||
if "plugin-loader" in name: continue
|
||||
try:
|
||||
Log.info("plugin-loader", f"启动: {name}")
|
||||
self.plugins[name]["instance"].start()
|
||||
except Exception as e: Log.error("plugin-loader", f"启动失败 {name}: {e}")
|
||||
|
||||
def _get_ordered_plugins(self) -> list[str]:
|
||||
if not self._dependency_plugin: return list(self.plugins.keys())
|
||||
try: return [n for n in self._dependency_plugin.resolve() if n in self.plugins]
|
||||
except Exception as e: Log.warn("plugin-loader", f"依赖解析失败,使用原始顺序: {e}"); return list(self.plugins.keys())
|
||||
|
||||
def _inject_dependencies(self):
|
||||
Log.info("plugin-loader", f"开始注入依赖,共 {len(self.plugins)} 个插件")
|
||||
nm = {}
|
||||
for n in self.plugins:
|
||||
c = n.rstrip("}"); nm[c] = n; nm[c + "}"] = n
|
||||
for n, i in self.plugins.items():
|
||||
inst = i["instance"]; io = i.get("info")
|
||||
if not io or not io.dependencies: continue
|
||||
for dn in io.dependencies:
|
||||
ad = nm.get(dn) or nm.get(dn + "}")
|
||||
if ad and ad in self.plugins:
|
||||
sn = f"set_{dn.replace('-', '_')}"
|
||||
if hasattr(inst, sn):
|
||||
try: getattr(inst, sn)(self.plugins[ad]["instance"]); Log.ok("plugin-loader", f"注入成功: {n} <- {ad}")
|
||||
except Exception as e: Log.error("plugin-loader", f"注入依赖失败 {n}.{sn}: {e}")
|
||||
else: Log.warn("plugin-loader", f"{n} 没有 {sn} 方法")
|
||||
|
||||
def stop_all(self):
|
||||
for n, i in reversed(list(self.plugins.items())):
|
||||
try: i["instance"].stop()
|
||||
except Exception as e: Log.error("plugin-loader", f"插件 {n} 停止失败:{type(e).__name__}: {e}")
|
||||
if self.lifecycle_plugin: self.lifecycle_plugin.stop_all()
|
||||
|
||||
def get_info(self, name: str) -> Optional[PluginInfo]:
|
||||
if name in self.plugins: return self.plugins[name]["info"]
|
||||
return None
|
||||
|
||||
def has_capability(self, capability: str) -> bool: return self.capability_registry.has_capability(capability)
|
||||
def get_capability_provider(self, capability: str) -> Optional[Any]: return self.capability_registry.get_provider(capability)
|
||||
|
||||
|
||||
class PluginLoaderPlugin(Plugin):
|
||||
"""插件加载器插件"""
|
||||
def __init__(self):
|
||||
self.manager = PluginManager()
|
||||
self._loaded = False
|
||||
self._started = False
|
||||
self._ensure_plugin_package()
|
||||
|
||||
def _ensure_plugin_package(self):
|
||||
if 'plugin' not in sys.modules:
|
||||
pkg = types.ModuleType('plugin'); pkg.__path__ = []; sys.modules['plugin'] = pkg
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
if self._loaded: return
|
||||
self._loaded = True
|
||||
self._ensure_plugin_package()
|
||||
Log.info("plugin-loader", "开始加载插件...")
|
||||
self.manager.load_all()
|
||||
|
||||
def start(self):
|
||||
if self._started: return
|
||||
self._started = True
|
||||
Log.info("plugin-loader", "启动插件...")
|
||||
self.manager.init_and_start_all()
|
||||
|
||||
def stop(self):
|
||||
Log.info("plugin-loader", "停止插件...")
|
||||
self.manager.stop_all()
|
||||
|
||||
|
||||
register_plugin_type("PluginManager", PluginManager)
|
||||
register_plugin_type("PluginInfo", PluginInfo)
|
||||
register_plugin_type("CapabilityRegistry", CapabilityRegistry)
|
||||
register_plugin_type("PLInjector", PLInjector)
|
||||
|
||||
|
||||
def New():
|
||||
return PluginLoaderPlugin()
|
||||
19
store/@{NebulaShell}/plugin-loader/manifest.json
Normal file
19
store/@{NebulaShell}/plugin-loader/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "plugin-loader",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "插件加载器 - 负责扫描、加载和管理所有插件",
|
||||
"type": "core"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"scan_dirs": ["store"],
|
||||
"sandbox_enabled": true,
|
||||
"permission_check": true
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": ["*"]
|
||||
}
|
||||
Reference in New Issue
Block a user