插件化架构
-所有功能皆可通过插件扩展,灵活定制您的系统
-安全隔离
-进程级沙箱保护,确保插件运行安全
-多语言支持
-内置国际化框架,支持全球多种语言
-轻松部署
-Docker 容器化部署,一键启动服务
-diff --git a/LICENSE b/LICENSE index 1361477..ba89e79 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 Falck, yongwanxing + Copyright 2026 Falck, yongwanxing, NebulaShell Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 1f7e6b4..1210c41 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,421 @@ -# NebulaShell +
+
+
+ 插件化运行时框架 · 多重签名加密分发 · NIR 一次编译到处运行 · 企业级安全体系 +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
日志视图渲染出错:{e}
" - - def _render_terminal(self) -> str: - html = """ - - - -插件管理页面渲染出错: {e}
" - - def _store_content(self) -> str: - try: - html = "" - for pkg in self._fetch_remote_plugins(): - safe_name = html.escape(pkg.get('name', '')) - safe_desc = html.escape(pkg.get('description', '')) - safe_version = html.escape(pkg.get('version', '未知')) - safe_author = html.escape(pkg.get('author', '未知')) - action_btn = '' - html += f"""{safe_desc}
- -插件商店页面渲染出错: {e}
" - - - - def _handle_list_plugins(self, request): - plugin_name = request.path_params.get('name', '') - schema = self._load_config_schema(plugin_name) - current = self._load_plugin_config(plugin_name) - return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps({ - "schema": schema, - "current": current - }, ensure_ascii=False)) - - def _handle_save_config(self, request): - plugin_name = request.path_params.get('name', '') - info = self._get_plugin_detailed_info(plugin_name) - return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(info, ensure_ascii=False)) - - def _handle_uninstall(self, request): - try: - plugins = self._fetch_remote_plugins() - return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(plugins, ensure_ascii=False)) - except Exception as e: - return Response(status=500, body=json.dumps({"error": str(e)})) - - def _handle_store_install(self, request): - import time - now = time.time() - if self._remote_cache and (now - self._cache_time) < self._cache_ttl: - return self._remote_cache - - plugins = [] - try: - store_url = f"{GITEE_API_BASE}/store" - for attempt in range(3): - try: - with _gitee_request(store_url, timeout=15) as resp: - dirs = json.loads(resp.read().decode("utf-8")) - break - except Exception as e: - if attempt < 2: - time.sleep(1 + attempt) - continue - raise - - time.sleep(0.5) - - for dir_info in dirs: - if dir_info.get("type") != "dir": - continue - author = dir_info.get("name", "") - if not author.startswith("@{"): - continue - - author_url = f"{GITEE_API_BASE}/store/{urllib.parse.quote(author, safe='')}" - for attempt in range(3): - try: - with _gitee_request(author_url, timeout=15) as resp: - plugin_dirs = json.loads(resp.read().decode("utf-8")) - break - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - if attempt < 2: - time.sleep(1 + attempt) - continue - raise - - time.sleep(0.5) - - for plugin_dir in plugin_dirs: - if plugin_dir.get("type") != "dir": - continue - plugin_name = plugin_dir.get("name", "") - - manifest_url = f"{GITEE_API_BASE}/store/{urllib.parse.quote(author, safe='')}/{plugin_name}/manifest.json" - manifest = {} - for attempt in range(3): - try: - with _gitee_request(manifest_url, timeout=15) as resp: - manifest = json.loads(resp.read().decode("utf-8")) - break - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - if attempt < 2: - time.sleep(1 + attempt) - continue - - plugins.append({ - "name": plugin_name, - "author": author, - "full_name": f"{author}/{plugin_name}", - "metadata": manifest.get("metadata", {}), - "dependencies": manifest.get("dependencies", []), - "has_config": False, - "is_installed": self._is_plugin_installed(plugin_name, author) - }) - - time.sleep(0.5) - - self._remote_cache = plugins - self._cache_time = now - except Exception as e: - Log.error("pkg-manager", f"获取远程插件列表失败: {type(e).__name__}: {e}") - - return plugins - - def _install_from_gitee(self, plugin_name: str, author: str) -> bool: - import time - try: - api_url = f"{GITEE_API_BASE}/store/{author}/{plugin}/{sub_dir}" - with _gitee_request(api_url, timeout=15) as resp: - items = json.loads(resp.read().decode("utf-8")) - - local_dir.mkdir(parents=True, exist_ok=True) - for item in items: - if item.get("type") == "file": - filename = item.get("name") - raw_url = f"{GITEE_RAW_BASE}/store/{author}/{plugin}/{sub_dir}/{filename}" - try: - with _gitee_request(raw_url, timeout=15) as resp: - content = resp.read() - with open(local_dir / filename, 'wb') as f: - f.write(content) - except: - pass - elif item.get("type") == "dir": - self._download_dir_raw(author, plugin, f"{sub_dir}/{item.get('name')}", local_dir / item.get("name")) - except: - pass - - - def _scan_all_plugins(self) -> list: - plugin_dir = self.store_dir / author / plugin_name - return (plugin_dir / "main.py").exists() - - def _find_plugin_dir(self, plugin_name: str) -> Path | None: - plugin_dir = self._find_plugin_dir(plugin_name) - if not plugin_dir: - return {} - schema_path = plugin_dir / "config.json" - if not schema_path.exists(): - return {} - with open(schema_path, 'r', encoding='utf-8') as f: - return json.load(f) - - def _load_plugin_config(self, plugin_name: str) -> dict: - if self.storage: - storage_instance = self.storage.get_storage("pkg-manager") - return storage_instance.get(f"plugin_config.{plugin_name}", {}) - return {} - - def _get_plugin_detailed_info(self, plugin_name: str) -> dict: - return {} diff --git a/store/NebulaShell/pkg-manager/manifest.json b/store/NebulaShell/pkg-manager/manifest.json deleted file mode 100644 index 1a86890..0000000 --- a/store/NebulaShell/pkg-manager/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "metadata": { - "name": "pkg-manager", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件包管理器 - 配置管理/商店/多语言项目部署支持", - "type": "webui-extension" - }, - "config": { - "enabled": true, - "args": { - "store_url": "https://store.nebulashell.org", - "auto_update": false, - "verify_signatures": true, - "cache_enabled": true, - "max_cache_size": 524288000 - } - }, - "dependencies": ["http-api", "webui", "plugin-storage", "i18n"], - "permissions": ["lifecycle", "plugin-storage"] -} diff --git a/store/NebulaShell/plugin-bridge/README.md b/store/NebulaShell/plugin-bridge/README.md deleted file mode 100644 index ef274aa..0000000 --- a/store/NebulaShell/plugin-bridge/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# plugin-bridge 插件桥接器 - -提供插件间的事件共享、广播、桥接和 RPC 服务调用。 - -## 功能 - -- **事件总线**: 插件间共享事件(发布/订阅) -- **广播**: 向多个插件发送消息 -- **桥接**: 将不同插件的事件互相映射 -- **RPC 服务调用**: 插件 A 调用插件 B 的方法并获取返回值 - -## 事件总线(发布/订阅 + 解耦) - -```python -bridge = plugin_mgr.get("plugin-bridge") -bus = bridge.event_bus - -# 订阅事件(发布者和订阅者解耦) -bus.on("http.request", lambda event: print(f"收到请求: {event.payload}")) - -# 发布事件 -bus.emit(BridgeEvent( - type="http.request", - source_plugin="http-api", - payload={"path": "/api/users"} -)) -``` - -## RPC 服务调用 - -```python -# 插件 B 注册服务 -bridge.services.register("plugin-b", "get_user", lambda user_id: {"id": user_id, "name": "test"}) - -# 插件 A 调用插件 B 的服务 -result = bridge.services.call("plugin-b", "get_user", 123) -print(result) # {"id": 123, "name": "test"} -``` - -## 广播 - -```python -broadcast = bridge.broadcast - -# 创建频道 -broadcast.create_channel("system", ["lifecycle", "metrics"]) - -# 广播消息 -broadcast.broadcast("system", {"action": "shutdown"}, "plugin-loader") -``` - -## 桥接 - -```python -bridge_mgr = bridge.bridge - -# 创建桥接:将 http-api 的事件映射到 metrics -bridge_mgr.create_bridge( - name="http-to-metrics", - from_plugin="http-api", - to_plugin="metrics", - event_mapping={ - "http.request": "metrics.http_request", - "http.error": "metrics.http_error", - } -) -``` - -## 事件历史 - -```python -# 查询历史 -history = bus.get_history("http.request") - -# 清空历史 -bus.clear_history() -``` diff --git a/store/NebulaShell/plugin-bridge/SIGNATURE b/store/NebulaShell/plugin-bridge/SIGNATURE deleted file mode 100644 index d834740..0000000 --- a/store/NebulaShell/plugin-bridge/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "yHmcdnBP6fx7TYFqHyiQeVYiP+S/9o7gx+fCC7nELQ2ZM55yXn9e4qpYWgPGAEw4zmuZbnKwLj0JQ1sE8BW28059+HWCj34ytUY/gckNvEkN+cGrqefwxWPGU19tysDC9Iy+HgBc+t34/igLZvRbcqpCpE0KH9SGfe34de6C60fL/HYZ1v3A29R05VmoPUBIOUY3X/9R5q4fYkjQqzvJ9LXujRR7Uyg8vP4dQo3k/MdxALg0xemXrMNRvX9F2g7i7DLCG8ABNxLHl7u5BymNXqBBClSu+/Fuf0HeyzLyYoOUP0Jhbxf56ep8jFLZRTU1qbt6itmaZgF8YSUh4oq1rWNYHZLZYH9sO6H32XsqXSq/509DkKXWJDZtIvJB/yrmVpt1Anj8YfMyA4pZ/R+htMa+coOlCAw20lnN0IMJW8oduKoYHFKMKkE7b++TzUv+7jon7WRWW8/2BXUFGV62jUSkPzI5o4TOgflHcCbLJ6SuOutxTpGiereVdDxlLRUVwBcRxY89DM9LKzqBPCbfG4Q6bVTtIvnyHn/ARQuYYXw41QzJGUYss/pS0YIH0YgYUHR88RCFqlZI53JXv1Y7kzieEprEWBBWEr6YxmYhx010W36hI0mM7YpBK3XWVkN7oJFBDt7DzFSQEYeeKDV/U0ZZgA5ufSiB8LYLYVjpz9Y=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.957446, - "plugin_hash": "97113f6d132bf58ea11688416b0fa3dda3a3642f3b82fd1e0b65ad06f8aad39c", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-bridge/main.py b/store/NebulaShell/plugin-bridge/main.py deleted file mode 100644 index cddeaa6..0000000 --- a/store/NebulaShell/plugin-bridge/main.py +++ /dev/null @@ -1,217 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Callable -from pathlib import Path -import importlib.util - -from oss.plugin.types import Plugin - - -@dataclass -class BridgeEvent: - type: str - source_plugin: str - payload: Any = None - context: dict[str, Any] = field(default_factory=dict) - - -class EventBus: - def __init__(self): - self._handlers: dict[str, list[Callable]] = {} - self._history: list[BridgeEvent] = [] - - def emit(self, event: BridgeEvent): - self._history.append(event) - handlers = self._handlers.get(event.type, []) - wildcard_handlers = self._handlers.get("*", []) - for handler in handlers + wildcard_handlers: - try: - handler(event) - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - - def on(self, event_type: str, handler: Callable): - if event_type not in self._handlers: - self._handlers[event_type] = [] - self._handlers[event_type].append(handler) - - def off(self, event_type: str, handler: Callable): - if event_type in self._handlers: - try: - self._handlers[event_type].remove(handler) - except ValueError: - pass - - def once(self, event_type: str, handler: Callable): - def wrapper(event): - self.off(event_type, wrapper) - handler(event) - self.on(event_type, wrapper) - - def get_history(self, event_type: str = None) -> list[BridgeEvent]: - if event_type: - return [e for e in self._history if e.type == event_type] - return self._history.copy() - - def clear_history(self): - self._history.clear() - - -class BroadcastManager: - def __init__(self, event_bus: EventBus): - self.event_bus = event_bus - self._channels: dict[str, list[str]] = {} - - def create_channel(self, name: str, plugins: list[str]): - self._channels[name] = plugins - - def broadcast(self, channel: str, payload: Any, source_plugin: str = ""): - if channel not in self._channels: - return - event = BridgeEvent( - type=f"broadcast.{channel}", - source_plugin=source_plugin, - payload=payload - ) - self.event_bus.emit(event) - - def get_channels(self) -> dict[str, list[str]]: - return dict(self._channels) - - -class ServiceRegistry: - def __init__(self): - self._services: dict[str, dict[str, Callable]] = {} - - def register(self, plugin_name: str, service_name: str, handler: Callable): - if plugin_name not in self._services: - self._services[plugin_name] = {} - self._services[plugin_name][service_name] = handler - - def unregister(self, plugin_name: str, service_name: str = None): - if plugin_name in self._services: - if service_name: - self._services[plugin_name].pop(service_name, None) - else: - del self._services[plugin_name] - - def call(self, plugin_name: str, service_name: str, *args, **kwargs) -> Any: - plugin = self._services.get(plugin_name) - if plugin and service_name in plugin: - return plugin[service_name](*args, **kwargs) - return None - - def list_services(self, plugin_name: str = None) -> dict: - if plugin_name: - return self._services.get(plugin_name, {}).copy() - return {k: v.copy() for k, v in self._services.items()} - - -class BridgeManager: - def __init__(self, event_bus: EventBus): - self.event_bus = event_bus - self._bridges: dict = {} - - def create_bridge(self, name: str, from_plugin: str, to_plugin: str, event_mapping: dict): - self._bridges[name] = { - "from": from_plugin, - "to": to_plugin, - "mapping": event_mapping, - } - for src_event, dst_event in event_mapping.items(): - def handler(event, dst_event=dst_event): - bridged = BridgeEvent( - type=dst_event, - source_plugin=event.source_plugin, - payload=event.payload, - context={**event.context, "_bridged_from": event.type} - ) - self.event_bus.emit(bridged) - self.event_bus.on(src_event, handler) - - def remove_bridge(self, name: str): - self._bridges.pop(name, None) - - def get_bridges(self) -> dict: - return self._bridges.copy() - - -_use_cache: dict[str, Any] = {} - -def use(plugin_name: str): - if plugin_name in _use_cache: - return _use_cache[plugin_name] - - from oss.plugin.manager import get_plugin_manager - manager = get_plugin_manager() - if manager and plugin_name in manager.plugins: - _use_cache[plugin_name] = manager.plugins[plugin_name] - return _use_cache[plugin_name] - - # 插件未通过 plugin-loader 加载,记录警告 - from oss.logger.logger import Log - Log.warn("plugin-bridge", f"use('{plugin_name}') 绕过 plugin-loader 直接加载,建议通过 plugin-loader 管理插件生命周期") - - from oss.config import get_config - config = get_config() - store_dir = Path(config.get("store_dir", "store")) - - if not store_dir.exists(): - return None - - for ns_dir in store_dir.iterdir(): - if not ns_dir.is_dir(): - continue - for pdir in ns_dir.iterdir(): - if not pdir.is_dir(): - continue - manifest = pdir / "manifest.json" - if not manifest.exists(): - continue - try: - meta = json.loads(manifest.read_text()) - name = meta.get("name", pdir.name) - if name == plugin_name: - main_file = pdir / "main.py" - if not main_file.exists(): - continue - PluginClass = None - if manager and plugin_name in manager._plugin_types: - PluginClass = manager._plugin_types[plugin_name] - if PluginClass is None: - spec = importlib.util.spec_from_file_location(f"use_{plugin_name}", str(main_file)) - if spec and spec.loader: - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - for attr in dir(mod): - cls = getattr(mod, attr) - if isinstance(cls, type) and issubclass(cls, Plugin) and cls is not Plugin: - PluginClass = cls - break - if PluginClass: - instance = PluginClass() if isinstance(PluginClass, type) else PluginClass - _use_cache[plugin_name] = instance - if manager: - manager.plugins[plugin_name] = instance - if hasattr(instance, "start"): - instance.start() - return instance - except (json.JSONDecodeError, OSError): - continue - return None - - -class PluginBridgePlugin(Plugin): - def __init__(self): - self.event_bus = EventBus() - self.services = ServiceRegistry() - self.broadcast = BroadcastManager(self.event_bus) - self.bridge = BridgeManager(self.event_bus) - - def start(self): - self.event_bus.clear_history() - - def set_plugin_storage(self, storage_plugin): - pass - - def stop(self): - self.event_bus.clear_history() diff --git a/store/NebulaShell/plugin-bridge/manifest.json b/store/NebulaShell/plugin-bridge/manifest.json deleted file mode 100644 index 34b1bf0..0000000 --- a/store/NebulaShell/plugin-bridge/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "metadata": { - "name": "plugin-bridge", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件桥接器 - 共享事件/广播/桥接/多语言支持", - "type": "core", - "load_priority": "first" - }, - "config": { - "enabled": true, - "args": { - "max_events": 1000, - "event_ttl": 3600, - "broadcast_enabled": true, - "queue_size": 5000 - } - }, - "dependencies": ["plugin-storage", "i18n"], - "permissions": ["plugin-storage", "lifecycle"] -} diff --git a/store/NebulaShell/plugin-loader-pro/SIGNATURE b/store/NebulaShell/plugin-loader-pro/SIGNATURE deleted file mode 100644 index b94b203..0000000 --- a/store/NebulaShell/plugin-loader-pro/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "j3U1ZFmpc+pOBC8auYyj84O9DMaAmhOx7F0yGIdrpnclTvteuuXDa7qdBduF+cTu7JStUxN9Yx4oA8dZkorvCZgShQ26jWgLxTAUpa74Pqv6b1q1KQVGcgmiIcF5spIu3zNH4R2tfAWidm7Jncmd2BDDrjVMg16d6Bk73fvMN8GajAaNt3PELIr55LFEER3mOMB9ooeuvUmr7EIoDvZap5bLO4iP88kZaKd6xArNhYi5sCgm4HOxKxUFBOLRAnmJFcOKTqGLL0kYwsoqiN1UPLEawndQKNyX47ZQRfKCut8qQZEPpXl4rYpI6j++Lw7NNrj/jX+IEWFpqMaXiumJAG3tDWKWd5I/7/CAOpttERooJEjG2tVyM2ka9HjIyrc4TrWD9DZTamwkRlrbWm0Q7soTn3O6ZkolQ2n/WUxWKu1o84OHkeeoXDg9AS/uiKsOf7ufTpL7doXUm4bj4xTNkPk63D5PlAoF/kLBgcLHo2UkdxYhv9Y/moig2ogqr//nU5ucIZLmGIIX2Bag8RKgwnhRnKZ+KIGJntIuOoAuoH1H3G/EV42/siqU/AsRSOBtCxhAoqBxaHzZMnyios8kguE/6BfIEs7yS4DzN2ANNcA6tXfbvWGq7oeEB2DBAdamPbyVB76rSsdi0/4zGugvXmBJO4yZuxcuu/HeBH7ES+0=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964226.5213168, - "plugin_hash": "bed620b64c10798828613a45e3227a7849a9a450e471dfd009135354fb650a1e", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader-pro/circuit/__init__.py b/store/NebulaShell/plugin-loader-pro/circuit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/circuit/breaker.py b/store/NebulaShell/plugin-loader-pro/circuit/breaker.py deleted file mode 100644 index 712b67e..0000000 --- a/store/NebulaShell/plugin-loader-pro/circuit/breaker.py +++ /dev/null @@ -1,28 +0,0 @@ - -class CircuitBreaker: - def __init__(self, failure_threshold: int = 3, recovery_timeout: int = 60, half_open_requests: int = 1): - self.failure_threshold = failure_threshold - self.recovery_timeout = recovery_timeout - self.half_open_requests = half_open_requests - - self.state = CircuitState.CLOSED - self.failure_count = 0 - self.success_count = 0 - self.last_failure_time = 0 - self.half_open_calls = 0 - - def call(self, func: Callable, *args, **kwargs) -> Any: - self.failure_count = 0 - if self.state == CircuitState.HALF_OPEN: - self.half_open_calls += 1 - if self.half_open_calls >= self.half_open_requests: - self.state = CircuitState.CLOSED - self.half_open_calls = 0 - - def _on_failure(self): - self.state = CircuitState.CLOSED - self.failure_count = 0 - self.half_open_calls = 0 - - def get_state(self) -> str: - return self.state diff --git a/store/NebulaShell/plugin-loader-pro/circuit/state.py b/store/NebulaShell/plugin-loader-pro/circuit/state.py deleted file mode 100644 index 6a173dd..0000000 --- a/store/NebulaShell/plugin-loader-pro/circuit/state.py +++ /dev/null @@ -1,4 +0,0 @@ -class CircuitState: - CLOSED = "closed" - OPEN = "open" - HALF_OPEN = "half_open" \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader-pro/core/__init__.py b/store/NebulaShell/plugin-loader-pro/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/core/config.py b/store/NebulaShell/plugin-loader-pro/core/config.py deleted file mode 100644 index ed950c1..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/config.py +++ /dev/null @@ -1,23 +0,0 @@ -class ProConfig: - def __init__(self, config: dict = None): - config = config or {} - self.failure_threshold = config.get("failure_threshold", 3) - self.recovery_timeout = config.get("recovery_timeout", 60) - self.half_open_requests = config.get("half_open_requests", 1) - - -class RetryConfig: - def __init__(self, config: dict = None): - config = config or {} - self.interval = config.get("interval", 30) - self.timeout = config.get("timeout", 5) - self.max_failures = config.get("max_failures", 5) - - -class AutoRecoveryConfig: - def __init__(self, config: dict = None): - config = config or {} - self.enabled = config.get("enabled", True) - self.timeout_per_plugin = config.get("timeout_per_plugin", 30) - - diff --git a/store/NebulaShell/plugin-loader-pro/core/enhancer.py b/store/NebulaShell/plugin-loader-pro/core/enhancer.py deleted file mode 100644 index 34dec8a..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/enhancer.py +++ /dev/null @@ -1,102 +0,0 @@ - -class PluginLoaderEnhancer: - def __init__(self, plugin_manager, config: ProConfig): - self.pm = plugin_manager - self.config = config - self._breakers = {} - self._health_checker = None - self._auto_recovery = AutoRecovery( - config.auto_recovery.max_attempts, - config.auto_recovery.delay - ) - self._enhanced = False - - def enhance(self): - for name, info in self.pm.plugins.items(): - self._breakers[name] = CircuitBreaker( - self.config.circuit_breaker.failure_threshold, - self.config.circuit_breaker.recovery_timeout, - self.config.circuit_breaker.half_open_requests - ) - ProLogger.debug("enhancer", f"为 {name} 创建熔断器") - - def _wrap_start_methods(self): - ordered = self._get_ordered_plugins() - - for name in ordered: - self._safe_call(name, 'init', '初始化') - - for name in ordered: - self._safe_call(name, 'start', '启动') - - def _safe_start_all(self): - info = self.pm.plugins.get(name) - if not info: - return - - instance = info.get("instance") - if not instance or not hasattr(instance, method): - return - - breaker = self._breakers.get(name) - if not breaker: - try: - getattr(instance, method)() - except Exception as e: - ProLogger.error("safe", f"{name} {action}失败: {type(e).__name__}: {e}") - self._on_plugin_error(name, info, str(e)) - return - - def do_call(): - return getattr(instance, method)() - - try: - breaker.call(do_call) - info["info"].error_count = 0 - ProLogger.info("safe", f"{name} {action}成功") - except Exception as e: - ProLogger.error("safe", f"{name} {action}失败: {type(e).__name__}: {e}") - self._on_plugin_error(name, info, str(e)) - - def _on_plugin_error(self, name: str, info: dict, error: str): - self._health_checker = HealthChecker( - self.config.health_check.interval, - self.config.health_check.timeout, - self.config.health_check.max_failures - ) - - for name, info in self.pm.plugins.items(): - self._health_checker.add_plugin(name, info["instance"]) - - self._health_checker.start( - on_failure_callback=self._on_health_check_failure - ) - ProLogger.info("enhancer", "健康检查已启动") - - def _on_health_check_failure(self, name: str): - ordered = [] - visited = set() - - def visit(name): - if name in visited: - return - visited.add(name) - - info = self.pm.plugins.get(name) - if not info: - return - - for dep in info["info"].dependencies: - clean_dep = dep.rstrip("}") - if clean_dep in self.pm.plugins: - visit(clean_dep) - - ordered.append(name) - - for name in self.pm.plugins: - visit(name) - - return ordered - - def disable(self): - pass diff --git a/store/NebulaShell/plugin-loader-pro/core/manager.py b/store/NebulaShell/plugin-loader-pro/core/manager.py deleted file mode 100644 index 8d2589f..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/manager.py +++ /dev/null @@ -1,106 +0,0 @@ - -class ProPluginManager: - def __init__(self, config: ProConfig): - self.config = config - self.plugins: dict[str, dict[str, Any]] = {} - self.capability_registry = CapabilityRegistry() - self._breakers: dict[str, CircuitBreaker] = {} - self._health_checker = HealthChecker( - config.health_check.interval, - config.health_check.timeout, - config.health_check.max_failures - ) - self._auto_recovery = AutoRecovery( - config.auto_recovery.max_attempts, - config.auto_recovery.delay - ) - - def load_all(self, store_dir: str = "store"): - if not store_dir.exists(): - return - - for author_dir in store_dir.iterdir(): - if not author_dir.is_dir(): - continue - - for plugin_dir in author_dir.iterdir(): - if not plugin_dir.is_dir(): - continue - - main_file = plugin_dir / "main.py" - if not main_file.exists(): - continue - - self._load_single_plugin(plugin_dir) - - def _load_single_plugin(self, plugin_dir: Path) -> Optional[Any]: - ProLogger.info("manager", "开始初始化所有插件...") - - self._inject_dependencies() - ordered = self._get_ordered_plugins() - - for name in ordered: - self._safe_init(name) - - ProLogger.info("manager", "开始启动所有插件...") - for name in ordered: - self._safe_start(name) - - self._health_checker.start( - on_failure_callback=self._on_plugin_failure - ) - - def _safe_init(self, name: str): - info = self.plugins[name] - instance = info["instance"] - breaker = self._breakers[name] - - try: - breaker.call(instance.start) - info["info"].status = "running" - self._health_checker.add_plugin(name, instance) - ProLogger.info("manager", f"已启动: {name}") - except Exception as e: - ProLogger.error("manager", f"启动失败 {name}: {type(e).__name__}: {e}") - info["info"].status = "error" - info["info"].error_count += 1 - info["info"].last_error = str(e) - - def stop_all(self): - info = self.plugins[name] - instance = info["instance"] - - try: - instance.stop() - info["info"].status = "stopped" - ProLogger.info("manager", f"已停止: {name}") - except Exception as e: - ProLogger.warn("manager", f"停止异常 {name}: {type(e).__name__}: {e}") - - def _on_plugin_failure(self, name: str): - name_map = {} - for name in self.plugins: - clean = name.rstrip("}") - name_map[clean] = name - name_map[clean + "}"] = name - - for name, info in self.plugins.items(): - deps = info["info"].dependencies - if not deps: - continue - - for dep_name in deps: - actual_dep = name_map.get(dep_name) or name_map.get(dep_name + "}") - if actual_dep and actual_dep in self.plugins: - dep_instance = self.plugins[actual_dep]["instance"] - setter = f"set_{dep_name.replace('-', '_')}" - - if hasattr(info["instance"], setter): - try: - getattr(info["instance"], setter)(dep_instance) - ProLogger.info("inject", f"{name} <- {actual_dep}") - except Exception as e: - ProLogger.error("inject", f"注入失败 {name}.{setter}: {type(e).__name__}: {e}") - - def _get_ordered_plugins(self) -> list[str]: - return [] diff --git a/store/NebulaShell/plugin-loader-pro/core/proxy.py b/store/NebulaShell/plugin-loader-pro/core/proxy.py deleted file mode 100644 index 4121287..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/proxy.py +++ /dev/null @@ -1,21 +0,0 @@ -class ProPluginProxy: - pass - - -class PluginProxy: - def __init__(self, plugin_name: str, allowed_plugins: list[str], all_plugins: dict): - self._plugin_name = plugin_name - self._allowed_plugins = allowed_plugins - self._all_plugins = all_plugins - - def get_plugin(self, name: str): - 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]: - return list(self._all_plugins.keys()) diff --git a/store/NebulaShell/plugin-loader-pro/core/registry.py b/store/NebulaShell/plugin-loader-pro/core/registry.py deleted file mode 100644 index 3206544..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/registry.py +++ /dev/null @@ -1,16 +0,0 @@ - -class ProCapabilityRegistry: - def __init__(self, permission_check: bool = True): - self.providers: dict[str, dict[str, Any]] = {} - self.consumers: dict[str, list[str]] = {} - self.permission_check = permission_check - - def register_provider(self, capability: str, plugin_name: str, instance: Any): - 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[str] = None) -> Optional[Any]: - return None diff --git a/store/NebulaShell/plugin-loader-pro/fallback/__init__.py b/store/NebulaShell/plugin-loader-pro/fallback/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/fallback/handler.py b/store/NebulaShell/plugin-loader-pro/fallback/handler.py deleted file mode 100644 index 8b1ac82..0000000 --- a/store/NebulaShell/plugin-loader-pro/fallback/handler.py +++ /dev/null @@ -1,20 +0,0 @@ -class FallbackHandler: - RETURN_DEFAULT = "return_default" - RETURN_CACHE = "return_cache" - RETURN_NULL = "return_null" - CALL_ALTERNATIVE = "call_alternative" - - def __init__(self): - self._cache = {} - - def execute(self, plugin_name: str, func: Callable, *args, **kwargs): - try: - result = func(*args, **kwargs) - self._cache[plugin_name] = result - return result - except Exception as e: - ProLogger.warn("fallback", f"插件 {plugin_name} 执行失败,触发降级: {type(e).__name__}: {e}") - return self._apply_fallback(plugin_name) - - def _apply_fallback(self, plugin_name: str) -> Any: - return None diff --git a/store/NebulaShell/plugin-loader-pro/isolation/__init__.py b/store/NebulaShell/plugin-loader-pro/isolation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/isolation/timeout.py b/store/NebulaShell/plugin-loader-pro/isolation/timeout.py deleted file mode 100644 index bab75ea..0000000 --- a/store/NebulaShell/plugin-loader-pro/isolation/timeout.py +++ /dev/null @@ -1,21 +0,0 @@ -class TimeoutIsolation: - pass - - -class TimeoutController: - def __init__(self, timeout: int = 30): - self.timeout = timeout - - def execute(self, func: Callable, *args, **kwargs): - def handler(signum, frame): - raise TimeoutError(f"执行超时 (>{self.timeout}s)") - - old_handler = signal.signal(signal.SIGALRM, handler) - signal.alarm(self.timeout) - - try: - result = func(*args, **kwargs) - signal.alarm(0) - return result - finally: - signal.signal(signal.SIGALRM, old_handler) diff --git a/store/NebulaShell/plugin-loader-pro/main.py b/store/NebulaShell/plugin-loader-pro/main.py deleted file mode 100644 index 5dca411..0000000 --- a/store/NebulaShell/plugin-loader-pro/main.py +++ /dev/null @@ -1,73 +0,0 @@ -class PluginLoaderProPlugin: - def __init__(self): - self.plugin_loader = None - self.enhancer = None - self.config = None - self._started = False - - def meta(self): - from oss.plugin.types import Metadata, PluginConfig, Manifest - return Manifest( - metadata=Metadata( - name="plugin-loader-pro", - version="1.0.0", - author="NebulaShell", - description="为 plugin-loader 提供熔断、降级、容错、自动修复等高级机制" - ), - config=PluginConfig( - enabled=True, - args={} - ), - dependencies=["plugin-loader"] - ) - - def set_plugin_loader(self, plugin_loader): - self.plugin_loader = plugin_loader - ProLogger.info("main", "已注入 plugin-loader") - - def init(self, deps: dict = None): - if not self.plugin_loader: - try: - from store.NebulaShell.plugin_bridge.main import use - self.plugin_loader = use("plugin-loader") - except Exception: - pass - if not self.plugin_loader: - ProLogger.warn("main", "未找到 plugin-loader 依赖") - return - - config = {} - if deps: - config = deps.get("config", {}) - - self.config = ProConfig(config) - self.enhancer = PluginLoaderEnhancer( - self.plugin_loader.manager, - self.config - ) - - ProLogger.info("main", "增强器已初始化") - - def start(self): - if self._started: - return - self._started = True - - if not self.enhancer: - ProLogger.warn("main", "增强器未初始化,跳过启动") - return - - ProLogger.info("main", "开始增强 plugin-loader...") - self.enhancer.enhance() - - def stop(self): - ProLogger.info("main", "停止增强器...") - if self.enhancer: - self.enhancer.disable() - - -register_plugin_type("PluginLoaderPro", PluginLoaderPro) - - -def New(): - return PluginLoaderPro() diff --git a/store/NebulaShell/plugin-loader-pro/manifest.json b/store/NebulaShell/plugin-loader-pro/manifest.json deleted file mode 100644 index adbde77..0000000 --- a/store/NebulaShell/plugin-loader-pro/manifest.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "metadata": { - "name": "plugin-loader-pro", - "version": "1.0.0", - "author": "NebulaShell", - "description": "插件加载 Pro - 为 plugin-loader 提供熔断、降级、容错、自动修复等高级机制", - "type": "enhancer" - }, - "config": { - "enabled": true, - "args": { - "circuit_breaker": { - "failure_threshold": 3, - "recovery_timeout": 60, - "half_open_requests": 1 - }, - "retry": { - "max_retries": 3, - "backoff_factor": 2, - "initial_delay": 1 - }, - "health_check": { - "interval": 30, - "timeout": 5, - "max_failures": 5 - }, - "auto_recovery": { - "enabled": true, - "max_attempts": 3, - "delay": 10 - }, - "isolation": { - "enabled": true, - "timeout_per_plugin": 30 - } - } - }, - "dependencies": ["plugin-loader"], - "permissions": ["*"] -} diff --git a/store/NebulaShell/plugin-loader-pro/models/__init__.py b/store/NebulaShell/plugin-loader-pro/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/models/plugin_info.py b/store/NebulaShell/plugin-loader-pro/models/plugin_info.py deleted file mode 100644 index 1e72db1..0000000 --- a/store/NebulaShell/plugin-loader-pro/models/plugin_info.py +++ /dev/null @@ -1,25 +0,0 @@ -class ProPluginInfo: - 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.status: str = "idle" - self.error_count: int = 0 - self.last_error: str = "" - - def to_dict(self) -> dict: - return { - "name": self.name, - "version": self.version, - "author": self.author, - "description": self.description, - "status": self.status, - "error_count": self.error_count - } diff --git a/store/NebulaShell/plugin-loader-pro/recovery/__init__.py b/store/NebulaShell/plugin-loader-pro/recovery/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py b/store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py deleted file mode 100644 index 998cdc7..0000000 --- a/store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py +++ /dev/null @@ -1,13 +0,0 @@ - -class AutoFixRecovery: - def __init__(self, max_attempts: int = 3, delay: int = 10): - self.max_attempts = max_attempts - self.delay = delay - self._recovery_attempts: dict[str, int] = {} - - def attempt_recovery(self, name: str, plugin_dir: Path, - module: any, instance: any) -> bool: - self._recovery_attempts[name] = 0 - - def get_attempts(self, name: str) -> int: - return self._recovery_attempts.get(name, 0) diff --git a/store/NebulaShell/plugin-loader-pro/recovery/health.py b/store/NebulaShell/plugin-loader-pro/recovery/health.py deleted file mode 100644 index 663722e..0000000 --- a/store/NebulaShell/plugin-loader-pro/recovery/health.py +++ /dev/null @@ -1,36 +0,0 @@ - -class HealthChecker: - def __init__(self, interval: int = 30, timeout: int = 5, max_failures: int = 5): - self.interval = interval - self.timeout = timeout - self.max_failures = max_failures - - self._running = False - self._thread = None - self._plugins: dict[str, Any] = {} - self._failure_counts: dict[str, int] = {} - self._on_failure_callback = None - - def add_plugin(self, name: str, instance: Any): - self._on_failure_callback = on_failure_callback - self._running = True - self._thread = threading.Thread(target=self._check_loop, daemon=True) - self._thread.start() - ProLogger.info("health", "健康检查已启动") - - def stop(self): - while self._running: - for name, instance in self._plugins.items(): - self._check_plugin(name, instance) - time.sleep(self.interval) - - def _check_plugin(self, name: str, instance: Any): - self._failure_counts[name] = self._failure_counts.get(name, 0) + 1 - - if self._failure_counts[name] >= self.max_failures: - ProLogger.warn("health", f"插件 {name} 连续失败 {self._failure_counts[name]} 次") - if self._on_failure_callback: - self._on_failure_callback(name) - - def reset_failure_count(self, name: str): - return self._failure_counts.get(name, 0) diff --git a/store/NebulaShell/plugin-loader-pro/retry/__init__.py b/store/NebulaShell/plugin-loader-pro/retry/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/retry/handler.py b/store/NebulaShell/plugin-loader-pro/retry/handler.py deleted file mode 100644 index 3ae9c45..0000000 --- a/store/NebulaShell/plugin-loader-pro/retry/handler.py +++ /dev/null @@ -1,12 +0,0 @@ - -class RetryHandler: - def __init__(self, config: RetryConfig = None): - config = config or RetryConfig() - self.max_retries = config.max_retries - self.backoff_factor = config.backoff_factor - self.initial_delay = config.initial_delay - - def execute(self, func: Callable, *args, **kwargs) -> Any: - delay = self.initial_delay * (self.backoff_factor ** attempt) - jitter = random.uniform(0, delay * 0.1) - return delay + jitter diff --git a/store/NebulaShell/plugin-loader-pro/utils/__init__.py b/store/NebulaShell/plugin-loader-pro/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/utils/logger.py b/store/NebulaShell/plugin-loader-pro/utils/logger.py deleted file mode 100644 index 6c828c1..0000000 --- a/store/NebulaShell/plugin-loader-pro/utils/logger.py +++ /dev/null @@ -1,29 +0,0 @@ - -class ProLogger: - _COLORS = { - "reset": "\033[0m", - "white": "\033[0;37m", - "yellow": "\033[1;33m", - "blue": "\033[1;34m", - "red": "\033[1;31m", - } - - @staticmethod - def _colorize(text: str, color: str) -> str: - tag = ProLogger._colorize(f"[pro:{component}]", "white") - msg = ProLogger._colorize(message, "white") - print(f"{tag} {msg}") - - @staticmethod - def warn(component: str, message: str): - tag = ProLogger._colorize(f"[pro:{component}]", "red") - icon = ProLogger._colorize("✗", "red") - msg = ProLogger._colorize(message, "red") - print(f"{tag} {icon} {msg}") - - @staticmethod - def debug(component: str, message: str): - tag = ProLogger._colorize(f"[pro:{component}]", "blue") - icon = ProLogger._colorize("→", "blue") - msg = ProLogger._colorize(message, "blue") - print(f"{tag} {icon} {msg}") diff --git a/store/NebulaShell/plugin-loader/PL_EXAMPLE.md b/store/NebulaShell/plugin-loader/PL_EXAMPLE.md deleted file mode 100644 index ffeb653..0000000 --- a/store/NebulaShell/plugin-loader/PL_EXAMPLE.md +++ /dev/null @@ -1,172 +0,0 @@ -# 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()` | 获取注册表完整信息(用于监控) | \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader/README.md b/store/NebulaShell/plugin-loader/README.md deleted file mode 100644 index d4c368f..0000000 --- a/store/NebulaShell/plugin-loader/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# plugin-loader 插件加载器 - -核心插件,负责扫描、加载和管理所有其他插件。 - -## 功能 - -- 自动扫描 `store/` 目录 -- 动态加载 `main.py` 并调用 `New()` 获取实例 -- 解析 `manifest.json` 获取插件元数据 -- 自动扫描插件能力(AST 分析) -- 按依赖关系排序加载顺序 -- 关联能力提供者与消费者 - -## 使用 - -无需手动使用,框架启动时自动加载。 diff --git a/store/NebulaShell/plugin-loader/SIGNATURE b/store/NebulaShell/plugin-loader/SIGNATURE deleted file mode 100644 index ea9b389..0000000 --- a/store/NebulaShell/plugin-loader/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader/main.py b/store/NebulaShell/plugin-loader/main.py deleted file mode 100644 index bf9a7d6..0000000 --- a/store/NebulaShell/plugin-loader/main.py +++ /dev/null @@ -1,758 +0,0 @@ -"""插件加载器插件 - 支持能力扫描和扩展 + 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'(? 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 {} - - # 使用 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 {} - - # 使用 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"} - plugin_dirs = [] - for ad in store_dir.iterdir(): - if ad.is_dir(): - for pd in ad.iterdir(): - if not pd.is_dir() or pd.name in skip or not (pd / "main.py").exists(): - continue - # 读取 load_priority,默认为 100 - priority = 100 - manifest_file = pd / "manifest.json" - if manifest_file.exists(): - try: - meta = json.loads(manifest_file.read_text()).get("metadata", {}) - raw = meta.get("load_priority", 100) - priority = 0 if raw == "first" else (int(raw) if isinstance(raw, (int, float)) else 100) - except (json.JSONDecodeError, OSError, (ValueError, TypeError)): - pass - plugin_dirs.append((priority, pd)) - # 按优先级升序排序(数值越小越先加载) - plugin_dirs.sort(key=lambda x: x[0]) - for _, pd in plugin_dirs: - 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() diff --git a/store/NebulaShell/plugin-loader/manifest.json b/store/NebulaShell/plugin-loader/manifest.json deleted file mode 100644 index 9690e4f..0000000 --- a/store/NebulaShell/plugin-loader/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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": ["*"] -} diff --git a/store/NebulaShell/plugin-storage/README.md b/store/NebulaShell/plugin-storage/README.md deleted file mode 100644 index 2543ab9..0000000 --- a/store/NebulaShell/plugin-storage/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# plugin-storage 插件存储 - -为所有插件提供隔离的键值存储服务。 - -## 功能 - -- **隔离存储**:每个插件有独立的命名空间 -- **持久化**:数据自动保存到 JSON 文件 -- **线程安全**:支持并发访问 -- **共享访问**:通过 plugin-bridge 可跨插件访问 - -## 基本使用 - -```python -storage_plugin = plugin_mgr.get("plugin-storage") - -# 获取插件的隔离存储 -storage = storage_plugin.get_storage("my-plugin") - -# 设置值 -storage.set("key", "value") -storage.set("config", {"theme": "dark", "lang": "zh"}) - -# 获取值 -value = storage.get("key") -config = storage.get("config", default={}) - -# 检查键 -if storage.has("key"): - print("存在") - -# 删除 -storage.delete("key") - -# 批量设置 -storage.set_many({"a": 1, "b": 2, "c": 3}) - -# 获取所有数据 -all_data = storage.get_all() - -# 清空 -storage.clear() -``` - -## 通过 plugin-bridge 访问 - -```python -bridge = plugin_mgr.get("plugin-bridge") -shared_storage = bridge.storage # 假设 bridge 集成了 storage - -# 获取其他插件的存储(需要权限) -other_storage = shared_storage.get_plugin_storage("other-plugin") -data = other_storage.get("some_key") -``` - -## 存储位置 - -``` -./data/storage/ -├── plugin-a/ -│ └── data.json -├── plugin-b/ -│ └── data.json -└── ... -``` - -## 元信息 - -```python -meta = storage.get_meta() -# {"plugin": "my-plugin", "keys": 5, "path": "./data/storage/my-plugin"} -``` diff --git a/store/NebulaShell/plugin-storage/SIGNATURE b/store/NebulaShell/plugin-storage/SIGNATURE deleted file mode 100644 index 8937148..0000000 --- a/store/NebulaShell/plugin-storage/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "TA08EBmVwhP0tyOhplpioxsGD4T8fRhj2ekEvQFGEaK2L1/USEGTcGXt/tciHNMU0AWVJ1bD9MY6aBj2+ljlSNCEyNlMMOeZot1/blcQG9wPsHbVXKm8VuyK0KBHwrM39DppbKIn4dlGL6A2Eua0bp20oCnmd2VF3IuTHGnGKmoXmehffXiVIlCgqIX2+wEqlD2TqYfP6LU+XWDYMtQ/ShS3ImbcIoChOzhj3H0LZKg+jd4d4N97B2z9uUinojZ4jJxix1qe6hednBiZNEGUub6/bn8DKtdRidPjwtwObKjL8etBFlca0mHEYvHe/T33uoqi1URGnbqXiiYneSKj4J1zJRMfDxfZhZ6ubeCcSiMufIgzNbMii1lR0mq/pCcUUM0X0I4ean2xk7ygW7xrN8ra3/73gOHvVBnVElwyPIpZaiPzvVnQ14nyv2zWFFezJNdkB0MOaoy0RRzk9Jp7DjGxn9B4f3sGAZX1gTUSzu91BvdhZOfy6VhS+t8Wf3PfY2t0OYyLPg28S0bQhfZ64HnjR2LR098jut40ckbzDRif0ZFtyj0OQWzbC0dTKlSEW6p2ozuWSROX2OMxY4F/srzAfh7SJL42FR9Lm/tlCVSRUVQ2NRMA8UFdtorsp09GDQmwy7xJxn9ghRWoXmrsaDSwvrZUlVo6LMa6ocvhvSo=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.8741558, - "plugin_hash": "e317a24422dcd005fc3f2db0fa34848bd81a45a3eb40cda0b8d20aadf2391cd1", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-storage/main.py b/store/NebulaShell/plugin-storage/main.py deleted file mode 100644 index 43a4b6f..0000000 --- a/store/NebulaShell/plugin-storage/main.py +++ /dev/null @@ -1,262 +0,0 @@ -from typing import Optional -from pathlib import Path - - -class PluginStorage: - def __init__(self, plugin_name: str, data_dir: str = None): - config = get_config() - self.plugin_name = plugin_name - self.data_dir = Path(data_dir or str(config.data_dir)) / plugin_name - self.data_dir.mkdir(parents=True, exist_ok=True) - self._data: dict[str, Any] = {} - self._lock = threading.Lock() - self._load() - - - def _load(self): - """从 data.json 加载持久化数据""" - data_file = self.data_dir / "data.json" - if data_file.exists(): - try: - with open(data_file, "r", encoding="utf-8") as f: - self._data = json.load(f) - except (json.JSONDecodeError, OSError) as e: - Log.error("plugin-storage", f"加载数据失败 {self.plugin_name}: {e}") - self._data = {} - else: - self._data = {} - - def _save(self): - """将数据持久化到 data.json""" - data_file = self.data_dir / "data.json" - try: - with open(data_file, "w", encoding="utf-8") as f: - json.dump(self._data, f, ensure_ascii=False, indent=2) - except OSError as e: - Log.error("plugin-storage", f"保存数据失败 {self.plugin_name}: {e}") - - def get(self, key: str, default: Any = None) -> Any: - with self._lock: - return self._data.get(key, default) - - def set(self, key: str, value: Any): - with self._lock: - self._data[key] = value - self._save() - - def delete(self, key: str) -> bool: - with self._lock: - if key in self._data: - del self._data[key] - self._save() - return True - return False - - def keys(self) -> list[str]: - with self._lock: - return list(self._data.keys()) - - def size(self) -> int: - with self._lock: - return len(self._data) - - def set_many(self, data: dict[str, Any]): - with self._lock: - self._data.update(data) - self._save() - - - def read_file(self, path: str, mode: str = "r") -> Optional[str | bytes]: - try: - file_path = self._resolve_path(path) - if file_path is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{path}") - return None - if not file_path.exists() or not file_path.is_file(): - return None - with open(file_path, mode, encoding="utf-8" if mode == "r" else None) as f: - return f.read() - except Exception as e: - Log.error("plugin-storage", f"读取文件失败 {self.plugin_name}/{path}: {type(e).__name__}: {e}") - return None - - def write_file(self, path: str, content: str | bytes): - try: - file_path = self._resolve_path(path) - if file_path is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{path}") - return - file_path.parent.mkdir(parents=True, exist_ok=True) - if isinstance(content, bytes): - with open(file_path, "wb") as f: - f.write(content) - else: - with open(file_path, "w", encoding="utf-8") as f: - f.write(content) - except Exception as e: - Log.error("plugin-storage", f"写入文件失败 {self.plugin_name}/{path}: {type(e).__name__}: {e}") - - def delete_file(self, path: str) -> bool: - try: - file_path = self._resolve_path(path) - if file_path is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{path}") - return False - if file_path.exists() and file_path.is_file(): - file_path.unlink() - return True - return False - except Exception as e: - Log.error("plugin-storage", f"删除文件失败 {self.plugin_name}/{path}: {type(e).__name__}: {e}") - return False - - def list_files(self, prefix: str = "") -> list[str]: - try: - search_dir = self._resolve_path(prefix) if prefix else self.data_dir - if search_dir is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{prefix}") - return [] - if not search_dir.exists(): - return [] - files = [] - for f in search_dir.rglob("*"): - if f.is_file(): - files.append(str(f.relative_to(self.data_dir))) - return sorted(files) - except Exception as e: - Log.error("plugin-storage", f"列出文件失败:{type(e).__name__}: {e}") - return [] - - def file_exists(self, path: str) -> bool: - file_path = self._resolve_path(path) - if file_path is None: - return False - return file_path.exists() and file_path.is_file() - - def serve_file(self, path: str): - try: - file_path = self._resolve_path(path) - if file_path is None: - return Response(status=403, body="Forbidden: path traversal detected") - - if not file_path.exists() or not file_path.is_file(): - return Response(status=404, body=f"File not found: {path}") - - content_type, _ = mimetypes.guess_type(str(file_path)) - if not content_type: - content_type = "application/octet-stream" - - if content_type.startswith("text/") or content_type in ( - "application/json", "application/javascript", "application/xml", - "text/css", "text/html", "image/svg+xml" - ): - content = file_path.read_text(encoding="utf-8") - else: - content = file_path.read_bytes() - - return Response( - status=200, - headers={ - "Content-Type": content_type, - "Cache-Control": "public, max-age=3600", - }, - body=content, - ) - except Exception as e: - return Response(status=500, body=f"Error serving file: {e}") - - def _resolve_path(self, path: str) -> Optional[Path]: - """安全解析路径,防止路径穿越 - - 将 path 拼接到 data_dir 下,resolve 后校验是否仍在 data_dir 范围内。 - 如果 path 试图穿越到 data_dir 之外,返回 None。 - """ - try: - target = (self.data_dir / path).resolve() - # 校验是否仍在 data_dir 范围内 - target.relative_to(self.data_dir.resolve()) - return target - except (ValueError, OSError): - return None - - -class SharedStorage: - def __init__(self, manager, shared_dir: Path): - self._manager = manager - self._shared_dir = shared_dir - self._shared_dir.mkdir(parents=True, exist_ok=True) - - def get_shared(self, key: str, default: Any = None) -> Any: - shared_file = self._shared_dir / f"{key}.json" - if not shared_file.exists(): - return default - with open(shared_file, "r", encoding="utf-8") as f: - return json.load(f) - - def set_shared(self, key: str, value: Any): - shared_file = self._shared_dir / f"{key}.json" - with open(shared_file, "w", encoding="utf-8") as f: - json.dump(value, f, ensure_ascii=False, indent=2) - - def list_storages(self) -> list[str]: - return [p.stem for p in self._shared_dir.glob("*.json")] - - -class PluginStoragePlugin(Plugin): - def __init__(self): - self.storages: dict[str, PluginStorage] = {} - self.shared = None - self.config = {} - self.data_root = Path("./data") - - def init(self, deps: dict = None): - """初始化时加载配置并初始化共享存储""" - config_path = Path("./data/plugin-storage/config.json") - if config_path.exists(): - try: - with open(config_path, "r", encoding="utf-8") as f: - self.config = json.load(f) - self.data_root = Path(self.config.get("data_root", "./data")) - shared_dir_name = self.config.get("shared_dir", "DCIM") - shared_dir = self.data_root / shared_dir_name - except (json.JSONDecodeError, OSError) as e: - Log.error("plugin-storage", f"加载配置失败: {e}") - shared_dir = self.data_root / "DCIM" - else: - Log.warn("plugin-storage", "config.json 不存在,使用默认配置") - self.config = {"data_root": "./data", "shared_dir": "DCIM"} - self.data_root = Path("./data") - shared_dir = self.data_root / "DCIM" - - self.shared = SharedStorage(self, shared_dir=shared_dir) - - def start(self): - Log.info("plugin-storage", f"插件存储服务已启动 (root={self.data_root})") - - def stop(self): - Log.info("plugin-storage", "插件存储服务已停止") - - def get_storage(self, plugin_name: str) -> PluginStorage: - if plugin_name not in self.storages: - self.storages[plugin_name] = PluginStorage(plugin_name) - return self.storages[plugin_name] - - def remove_storage(self, plugin_name: str) -> bool: - if plugin_name in self.storages: - del self.storages[plugin_name] - data_dir = PluginStorage(plugin_name).data_dir - if data_dir.exists(): - shutil.rmtree(data_dir) - return True - return False - - def list_storages(self) -> list[str]: - return list(self.storages.keys()) - - -register_plugin_type("PluginStorage", PluginStorage) -register_plugin_type("SharedStorage", SharedStorage) - - -def New(): - return PluginStoragePlugin() diff --git a/store/NebulaShell/plugin-storage/manifest.json b/store/NebulaShell/plugin-storage/manifest.json deleted file mode 100644 index 3047ad1..0000000 --- a/store/NebulaShell/plugin-storage/manifest.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "metadata": { - "name": "plugin-storage", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件存储 - 为所有插件提供隔离的键值存储服务/多语言支持", - "type": "utility" - }, - "config": { - "enabled": true, - "args": { - "data_dir": "./data/storage", - "max_size_per_plugin": 104857600, - "compression_enabled": true, - "encryption_enabled": false, - "backup_enabled": true, - "backup_interval": 86400 - } - }, - "dependencies": ["i18n"], - "permissions": ["lifecycle"] -} diff --git a/store/NebulaShell/plugin_bridge b/store/NebulaShell/plugin_bridge deleted file mode 120000 index f7c65b4..0000000 --- a/store/NebulaShell/plugin_bridge +++ /dev/null @@ -1 +0,0 @@ -plugin-bridge \ No newline at end of file diff --git a/store/NebulaShell/polyglot-deploy/manifest.json b/store/NebulaShell/polyglot-deploy/manifest.json deleted file mode 100644 index 3e19a08..0000000 --- a/store/NebulaShell/polyglot-deploy/manifest.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "metadata": { - "name": "polyglot-deploy", - "version": "1.1.0", - "author": "NebulaShell", - "description": "多语言项目部署服务 - 支持 Node.js/Python/Java/Go/Rust 等项目的一键部署/WebUI 管理", - "type": "deployment" - }, - "config": { - "enabled": true, - "args": { - "supported_languages": ["python", "nodejs", "java", "go", "rust", "php", "ruby"], - "build_timeout": 300, - "deploy_timeout": 600, - "max_projects": 50, - "workspace_dir": "/workspace/polyglot-projects", - "auto_cleanup": true, - "cleanup_interval": 3600, - "log_level": "info", - "docker_enabled": true, - "docker_network": "polyglot-net" - } - }, - "dependencies": ["http-api", "i18n", "pkg-manager"], - "permissions": ["lifecycle", "plugin-storage"] -} diff --git a/store/NebulaShell/signature-verifier/SIGNATURE b/store/NebulaShell/signature-verifier/SIGNATURE deleted file mode 100644 index 5cd49a7..0000000 --- a/store/NebulaShell/signature-verifier/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "ymWLA+iMcmMAum/qQIJdTgk31K/dNejA9BtOe5yUjAi58yKKW4mPx8c+6x9I28XM+eG1KkSAMrb94NFVVARR/k/Lc4QnNwbJVoYheEfKscMih6G4meiVYPFGkjMwukNL+k8qon+HJwAtHtZ9DFWclfZEXDKVptbr+3juovmBGIYdGlZ9pS2AmrYLsxx5SjCtuJb+cuOE0U5S5GBxBsv7tvP5IQAZZP1Crf0Cxe9Op6+UzX0TYfzWswIqcYzZdgPEUbMorwUlRPVgHGiaYtoHqQGISjR2kAgk5XW3NCuPAAQoiY9XTe+YD+3wDfQ7ic0tkESIJBBt7Zq2VJMmh8lwWMTRi0+xVgZHua2HpLdHDatSggWoCXQiMakm7rA1Z/Xto30mx0Pk4fh2vcYeuThoY6a+GsPbMxG4Bj52jSzGwHu264cgnSe8wS+HmbU0Ch1t4qD0lAZh297qRBqdr4hW+Lzf/FxVd5kQiL3rYcq8mi3Kd4nmiQ4/gev5mX7uSf6fhByCrXqe4pvgAt5q7BgByC9C/krcTRNIwV+u9PQe7zboXZY7x0CzYzITKkWzIY98Fl+8qmvuanLoPQ3tqAdixJr5I1Lpk71l2B5cW5ht2iB48RL4b+oPNqksIweRmPSDyTGBf3jS2YQNY62tdt4yXkWHO00qIOU0UQkGF+BKRSw=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775969853.454207, - "plugin_hash": "762549e109001210eb9fb1fbfc2b9a3c25cbb3ea899b796d07357d6ee32949d5", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/signature-verifier/main.py b/store/NebulaShell/signature-verifier/main.py deleted file mode 100644 index 8df4b6f..0000000 --- a/store/NebulaShell/signature-verifier/main.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -Plugin Signature Verification Service -- Verify integrity and origin authenticity of official plugins -- Support multiple signers (Falck unique signature) -- RSA-SHA256 asymmetric encryption scheme -""" - -import os -import json -import hashlib -import base64 -from pathlib import Path -from typing import Optional, Dict, List, Tuple - -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa -from cryptography.hazmat.backends import default_backend -from cryptography.exceptions import InvalidSignature - -from oss.plugin.types import Plugin -from oss.config import get_config - - -FALCK_PUBLIC_KEY_PEM = "" - -NEBULASHELL_PUBLIC_KEY_PEM = "" - - -class SignatureError(Exception): - pass - - -class SignatureVerifier: - def __init__(self, key_dir: str = None): - config = get_config() - self.key_dir = Path(key_dir or str(config.get("SIGNATURE_KEYS_DIR", "./data/signature-verifier/keys"))) - self.key_dir.mkdir(parents=True, exist_ok=True) - self.public_keys: Dict[str, bytes] = {} - self._load_builtin_keys() - - def _load_builtin_keys(self): - pub_dir = self.key_dir / "public" - if not pub_dir.exists(): - return - for key_file in pub_dir.glob("*.pem"): - author_name = key_file.stem - self.public_keys[author_name] = key_file.read_bytes() - - def _compute_plugin_hash(self, plugin_dir: Path) -> str: - """Compute content hash of the plugin directory. - Includes relative path + content of all files. - """ - hasher = hashlib.sha256() - - files_to_hash = [] - for file_path in sorted(plugin_dir.rglob("*")): - if file_path.is_file() and file_path.name != "SIGNATURE": - rel_path = file_path.relative_to(plugin_dir) - files_to_hash.append((str(rel_path), file_path)) - - for rel_path, file_path in files_to_hash: - hasher.update(rel_path.encode("utf-8")) - hasher.update(file_path.read_bytes()) - - return hasher.hexdigest() - - def verify_plugin(self, plugin_dir: Path, author: str = "Falck") -> Tuple[bool, str]: - """Verify plugin signature. - Returns: (is_valid, details) - """ - signature_file = plugin_dir / "SIGNATURE" - - if not signature_file.exists(): - return False, f"Plugin missing signature file: {plugin_dir}" - - try: - sig_data = json.loads(signature_file.read_text()) - except json.JSONDecodeError as e: - return False, f"Signature file format error: {e}" - - required_fields = ["signature", "signer", "algorithm", "timestamp"] - for field in required_fields: - if field not in sig_data: - return False, f"Signature missing required field: {field}" - - signer = sig_data["signer"] - signature = base64.b64decode(sig_data["signature"]) - - if signer not in self.public_keys: - return False, f"Unknown signer: {signer}" - - try: - public_key = serialization.load_pem_public_key( - self.public_keys[signer], - backend=default_backend() - ) - except Exception as e: - return False, f"Public key load failed: {e}" - - current_hash = self._compute_plugin_hash(plugin_dir) - - try: - signed_data = f"{author}:{current_hash}".encode("utf-8") - public_key.verify( - signature, - signed_data, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - return True, f"Signature verified (signer: {signer})" - except InvalidSignature: - return False, f"Signature mismatch! Plugin may have been tampered with (signer: {signer})" - except Exception as e: - return False, f"Signature verification error: {e}" - - def is_official_plugin(self, plugin_dir: Path) -> bool: - pass - - -class PluginSigner: - def __init__(self, private_key_path: Optional[str] = None): - self.private_key = None - if private_key_path: - self.load_private_key(private_key_path) - - def load_private_key(self, key_path: str): - with open(key_path, "rb") as f: - pem_data = f.read() - self.private_key = serialization.load_pem_private_key( - pem_data, - password=None, - backend=default_backend() - ) - - def sign_plugin(self, plugin_dir: Path, signer_name: str, author: str = "Falck") -> str: - """Generate signature for a plugin. - Returns: path to the signature file - """ - if not self.private_key: - raise ValueError("Private key not loaded") - - hasher = hashlib.sha256() - files_to_hash = [] - for file_path in sorted(plugin_dir.rglob("*")): - if file_path.is_file() and file_path.name not in ("SIGNATURE",): - rel_path = file_path.relative_to(plugin_dir) - files_to_hash.append((str(rel_path), file_path)) - - for rel_path, file_path in files_to_hash: - hasher.update(rel_path.encode("utf-8")) - hasher.update(file_path.read_bytes()) - - plugin_hash = hasher.hexdigest() - - import time - signed_data = f"{author}:{plugin_hash}".encode("utf-8") - signature = self.private_key.sign( - signed_data, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - - sig_data = { - "signature": base64.b64encode(signature).decode(), - "signer": signer_name, - "algorithm": "RSA-SHA256", - "timestamp": time.time(), - "plugin_hash": plugin_hash, - "author": author - } - - signature_file = plugin_dir / "SIGNATURE" - signature_file.write_text(json.dumps(sig_data, indent=2)) - - return str(signature_file) - - -class SignatureVerifierPlugin(Plugin): - def __init__(self): - self.verifier = SignatureVerifier() - self.signer = None - - def verify(self, plugin_dir: Path, author: str = "Falck") -> Tuple[bool, str]: - return self.verifier.verify_plugin(plugin_dir, author) - - def is_official(self, plugin_dir: Path) -> bool: - return self.verifier.is_official_plugin(plugin_dir) - - def sign(self, plugin_dir: Path, signer_name: str, author: str = "Falck") -> str: - if not self.signer: - raise SignatureError("Private key not loaded, cannot sign") - return self.signer.sign_plugin(plugin_dir, signer_name, author) - - def generate_keypair(self, author: str, key_dir: str = None): - pass diff --git a/store/NebulaShell/signature-verifier/manifest.json b/store/NebulaShell/signature-verifier/manifest.json deleted file mode 100644 index bbcb400..0000000 --- a/store/NebulaShell/signature-verifier/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "metadata": { - "name": "signature-verifier", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件签名验证服务 - 验证官方插件完整性与来源真实性/安全增强", - "type": "core" - }, - "config": { - "enabled": true, - "args": { - "enforce_official": true, - "key_dir": "data/signature-verifier/keys", - "algorithm": "RSA-SHA256", - "key_size": 2048, - "auto_verify": true, - "cache_enabled": true, - "cache_ttl": 3600 - } - }, - "dependencies": ["plugin-storage", "i18n"], - "permissions": ["plugin-storage"] -} diff --git a/store/NebulaShell/webui/SIGNATURE b/store/NebulaShell/webui/SIGNATURE deleted file mode 100644 index 692b193..0000000 --- a/store/NebulaShell/webui/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "EHeyw9j4zTQyLiTHSqEHQQL1wrzJOjm4jRKHIWuIiRUY6YORSLip1aDVP+aGpCf+KYGROE/pMt3SDUI7h+5VWAh9x/AYf0UrCOq38dNJ4+5TeHxOwUZvic2Ua26LBWRp0GfdRq/t06/dtXkIwD+0albetQNJoPkORBTCuxPVZqGVU6WkKWuYJ9xuQDhpn266qy6ZQfVe88BcNPbO//AIR8+t5gpd+hRmhbhxV58Omm+R0jtlx3ABEOH4g2HGkX961UvUdFSaoVMw7KR4lv9GQU1rMraP/zyHTLAQQlt/SxJAi3db51KWzFuH8rDsGKnB7LbJvnV32ojUNQs0SIO8935UY6RuHnKr8KHuAxFNX/1GA4MdloHhrK0Fm6Tx5FDXamthUFqJzYvjMtsGGN24p7/DQwaHqonB9AJ5szRf/vBYmsGs1WTCX/e89IN/uiVUPuqEiRxiJBRMLwpr2mz0r6e3keozWdPuxZ58WVH3Gd3gXvLngs+Gx3FyCd7RLtn24gkq/w16bCuA3XBE+9+n6QvAUBfvjiODCb9fjdPL/YNoJRMKqE1iAhMI+I5Cmu0ISOdTL4aYZEjZP3YwjauMKlpXMhclOwIv2I2btNQIKPOJj4SormqPweK0QXAVbOr+u/S0Z4L2vISGwJBetQl8fpSpdL2NLVmZM9xAoa1AZTY=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.7199903, - "plugin_hash": "1aee0b23a28d31b62a8863d1feff8a53e0a1221572cba160642ac18d10a8f52f", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/webui/config.json b/store/NebulaShell/webui/config.json deleted file mode 100644 index dffed56..0000000 --- a/store/NebulaShell/webui/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "title": { - "type": "string", - "name": "网站标题", - "description": "侧边栏和页面标题显示的文字", - "default": "NebulaShell", - "order": 1 - }, - "port": { - "type": "number", - "name": "端口号", - "description": "WebUI 监听端口", - "default": 8080, - "min": 1024, - "max": 65535, - "order": 2 - }, - "theme": { - "type": "select", - "name": "主题", - "description": "界面主题风格", - "default": "dark", - "options": [ - { "label": "深色", "value": "dark" }, - { "label": "浅色", "value": "light" } - ], - "order": 3 - } -} diff --git a/store/NebulaShell/webui/config/database.sql b/store/NebulaShell/webui/config/database.sql deleted file mode 100644 index e7835d1..0000000 --- a/store/NebulaShell/webui/config/database.sql +++ /dev/null @@ -1,49 +0,0 @@ --- NebulaShell WebUI 数据库初始化脚本 --- 此脚本创建基础表结构,其他插件可以添加自己的表 - -CREATE DATABASE IF NOT EXISTS nebulashell -CHARACTER SET utf8mb4 -COLLATE utf8mb4_unicode_ci; - -USE nebulashell; - --- 用户表 (示例) -CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - email VARCHAR(100) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_username (username), - INDEX idx_email (email) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- 插件配置表 -CREATE TABLE IF NOT EXISTS plugin_configs ( - id INT AUTO_INCREMENT PRIMARY KEY, - plugin_name VARCHAR(100) NOT NULL, - config_key VARCHAR(100) NOT NULL, - config_value TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY unique_plugin_config (plugin_name, config_key), - INDEX idx_plugin_name (plugin_name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- 系统日志表 -CREATE TABLE IF NOT EXISTS system_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - level VARCHAR(20) NOT NULL DEFAULT 'INFO', - message TEXT NOT NULL, - source VARCHAR(100), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_level (level), - INDEX idx_created_at (created_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- 插入默认配置 -INSERT IGNORE INTO plugin_configs (plugin_name, config_key, config_value) VALUES -('webui', 'theme', 'dark'), -('webui', 'title', 'NebulaShell'), -('webui', 'version', '1.0.0'); diff --git a/store/NebulaShell/webui/core/__init__.py b/store/NebulaShell/webui/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/webui/core/server.py b/store/NebulaShell/webui/core/server.py deleted file mode 100644 index 1069f6d..0000000 --- a/store/NebulaShell/webui/core/server.py +++ /dev/null @@ -1,66 +0,0 @@ -class WebUIServer: - def __init__(self, router, config: dict): - self.router = router - self.config = config - self.frontend_dir = Path(__file__).parent.parent / "frontend" - - self.pages = {} - self.nav_items = [] - - def start(self): - self.pages[path] = content_provider - if nav_item: - nav_item['url'] = path - self.nav_items.append(nav_item) - - self.router.get(path, lambda req: self._render_page(path, req)) - - def _render_page(self, path: str, request): - page_title = self.config.get("title", "NebulaShell") - - template_file = self.frontend_dir / "views" / "layout.html" - with open(template_file, 'r', encoding='utf-8') as f: - html_template = f.read() - - html = html_template.replace('{{ pageTitle }}', page_title) - html = html.replace('{{ navItems }}', nav_html) - html = html.replace('{{ content }}', content) - - return Response( - status=200, - headers={"Content-Type": "text/html; charset=utf-8"}, - body=html - ) - - def _default_home_content(self) -> str: - return """所有功能皆可通过插件扩展,灵活定制您的系统
-进程级沙箱保护,确保插件运行安全
-内置国际化框架,支持全球多种语言
-Docker 容器化部署,一键启动服务
-暂无内容
-{message}
-按任意键返回
- - """ - self.load_page("/error", error_html) - self.render_current() - - def setup_default_bindings(self): - if self.current_page not in self.pages: - return - - html = self.pages[self.current_page] - converter = HTMLToTUIConverter(self.width, self.height) - converter.parse(html) - - for key, config in converter.get_keyboard_bindings().items(): - action = config.get('action', '') - target = config.get('target', '') - - if action == 'navigate' and target: - self.input_handler.bind_key(key, lambda t=target: self.navigate(t)) - elif action == 'quit': - self.input_handler.bind_key(key, self.quit) - elif action == 'refresh': - self.input_handler.bind_key(key, self.render_current) - - def run_event_loop(self): - self.running = False - - def start(self): - pass - - -def create_tui_manager(width: int = 80, height: int = 24): - global _tui_manager_instance - if _tui_manager_instance is None: - _tui_manager_instance = TUIManager(width, height) - return _tui_manager_instance diff --git a/store/NebulaShell/webui/tui/index.html b/store/NebulaShell/webui/tui/index.html deleted file mode 100644 index e0b2df7..0000000 --- a/store/NebulaShell/webui/tui/index.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - -