重构:核心迁移至 oss/core + NBPF 多重签名加密 + NIR 编译器 + README 全面升级
- 核心功能从 store/ 迁移至 oss/core/ 框架层 - 实现 NBPF 包格式:多重签名(Ed25519+RSA-PSS+HMAC)+ 多重加密(AES-256-GCM) - 实现 NIR 编译器:基于 compile()+marshal 的跨平台中间表示 - 新增 nebula nbpf CLI 命令组(pack/unpack/verify/sign/keygen) - 新增 19 个 NBPF 测试用例,覆盖全链路 - 彻底重写 README,大型项目标准框架风格,所有图表使用 SVG - 更新 LICENSE 版权声明 - 清理旧版 store 插件目录(已迁移至 oss/core)
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
|
||||
|
||||
def register(injector):
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
current_file = Path(__file__)
|
||||
plugin_dir = current_file.parent.parent
|
||||
|
||||
main_file = plugin_dir / "main.py"
|
||||
|
||||
safe_builtins_dict = {
|
||||
"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,
|
||||
"Exception": Exception, "BaseException": BaseException,
|
||||
}
|
||||
safe_globals = {
|
||||
"bi": safe_builtins_dict,
|
||||
"__name__": "plugin.auto-dependency",
|
||||
"__package__": "plugin.auto-dependency",
|
||||
"__file__": str(main_file),
|
||||
"Path": Path,
|
||||
}
|
||||
safe_globals["__builtins__"] = safe_builtins_dict
|
||||
|
||||
try:
|
||||
with open(main_file, "r", encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
|
||||
code = compile(source, str(main_file), "exec")
|
||||
exec(code, safe_globals)
|
||||
|
||||
new_func = safe_globals.get("New")
|
||||
if new_func and callable(new_func):
|
||||
plugin_instance = new_func()
|
||||
|
||||
plugin_instance.init({
|
||||
"scan_dirs": ["store"],
|
||||
"auto_install": True
|
||||
})
|
||||
|
||||
plugin_instance.register_pl_functions(injector)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[auto-dependency] PL 注册失败:{e}")
|
||||
@@ -1,117 +0,0 @@
|
||||
# 依赖自动安装插件 (auto-dependency)
|
||||
|
||||
## 概述
|
||||
|
||||
依赖自动安装插件是一个核心系统插件,用于扫描所有插件的声明文件,检查并自动安装系统依赖。
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. **扫描插件声明** - 自动扫描所有插件目录下的 `manifest.json` 文件
|
||||
2. **系统依赖检测** - 读取每个插件声明的系统依赖 (`system_dependencies` 字段)
|
||||
3. **安装状态检查** - 检查这些系统依赖是否已在系统中安装
|
||||
4. **自动安装** - 对于未安装的依赖,使用系统包管理器自动安装
|
||||
5. **PL 注入接口** - 通过 PL 注入机制向插件加载器注册功能接口
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在 manifest.json 中声明系统依赖
|
||||
|
||||
其他插件可以在自己的 `manifest.json` 中声明所需的系统依赖:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "my-plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "MyName",
|
||||
"description": "我的插件"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": ["plugin-loader"],
|
||||
"system_dependencies": ["curl", "git", "wget"],
|
||||
"permissions": []
|
||||
}
|
||||
```
|
||||
|
||||
### 通过 PL 注入接口调用
|
||||
|
||||
插件加载器加载此插件后,可以通过以下 PL 注入接口进行操作:
|
||||
|
||||
| 接口名称 | 说明 | 参数 | 返回值 |
|
||||
|---------|------|------|--------|
|
||||
| `auto-dependency:scan` | 扫描所有插件的声明文件 | `scan_dir` (可选,默认 "store") | 插件信息列表 |
|
||||
| `auto-dependency:check` | 检查系统依赖安装状态 | `scan_dir` (可选,默认 "store") | 检查结果字典 |
|
||||
| `auto-dependency:install` | 安装缺失的系统依赖 | `scan_dir` (可选,默认 "store") | 安装结果字典 |
|
||||
| `auto-dependency:info` | 获取插件系统信息 | 无 | 系统信息字典 |
|
||||
|
||||
### 示例代码
|
||||
|
||||
```python
|
||||
# 获取插件加载器中的 auto-dependency 功能
|
||||
injector = get_pl_injector() # 从插件加载器获取
|
||||
|
||||
# 扫描所有插件的系统依赖声明
|
||||
plugins = injector.get_injected_functions("auto-dependency:scan")[0]()
|
||||
print(f"找到 {len(plugins)} 个插件")
|
||||
|
||||
# 检查依赖安装状态
|
||||
result = injector.get_injected_functions("auto-dependency:check")[0]()
|
||||
print(f"已安装:{result['installed_count']}, 缺失:{result['missing_count']}")
|
||||
|
||||
# 安装缺失的依赖
|
||||
install_result = injector.get_injected_functions("auto-dependency:install")[0]()
|
||||
print(f"成功安装:{install_result['success_count']}, 失败:{install_result['failed_count']}")
|
||||
```
|
||||
|
||||
## 支持的包管理器
|
||||
|
||||
插件自动检测系统使用的包管理器,支持:
|
||||
|
||||
- **Debian/Ubuntu**: apt-get, apt
|
||||
- **RHEL/CentOS**: yum, dnf
|
||||
- **Arch Linux**: pacman
|
||||
- **macOS**: brew
|
||||
- **Alpine Linux**: apk
|
||||
|
||||
## 配置选项
|
||||
|
||||
在 `manifest.json` 的 `config.args` 中可以配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"scan_dirs": ["store"],
|
||||
"package_manager": "auto",
|
||||
"auto_install": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|-------|------|--------|
|
||||
| `scan_dirs` | 要扫描的目录列表 | `["store"]` |
|
||||
| `package_manager` | 包管理器(auto 为自动检测) | `"auto"` |
|
||||
| `auto_install` | 是否自动安装缺失的依赖 | `true` |
|
||||
|
||||
## 安全说明
|
||||
|
||||
- 插件需要 `*` 权限才能执行系统命令安装包
|
||||
- 包安装操作有超时限制(300 秒)
|
||||
- 所有安装操作都会记录日志
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
store/NebulaShell/auto-dependency/
|
||||
├── manifest.json # 插件清单
|
||||
├── main.py # 主逻辑实现
|
||||
├── PL/
|
||||
│ └── main.py # PL 注入入口
|
||||
└── README.md # 本文档
|
||||
```
|
||||
@@ -1,269 +0,0 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, List, Dict
|
||||
from oss.plugin.types import Plugin
|
||||
|
||||
|
||||
class SystemDependencyChecker:
|
||||
def __init__(self, package_managers=None):
|
||||
self.package_managers = package_managers or {}
|
||||
self.detected_pm = self._detect_package_manager()
|
||||
|
||||
def _detect_package_manager(self):
|
||||
for pm, commands in self.package_managers.items():
|
||||
for cmd in commands:
|
||||
if shutil.which(cmd):
|
||||
return pm
|
||||
return "unknown"
|
||||
|
||||
def check_command(self, command: str) -> bool:
|
||||
if not self.detected_pm or self.detected_pm == "unknown":
|
||||
return False
|
||||
|
||||
try:
|
||||
if self.detected_pm in ["apt", "apt-get"]:
|
||||
result = subprocess.run(
|
||||
["dpkg", "-l", package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.returncode == 0 and "ii" in result.stdout
|
||||
elif self.detected_pm in ["yum", "dnf"]:
|
||||
result = subprocess.run(
|
||||
["rpm", "-q", package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.returncode == 0
|
||||
elif self.detected_pm == "pacman":
|
||||
result = subprocess.run(
|
||||
["pacman", "-Q", package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.returncode == 0
|
||||
elif self.detected_pm == "brew":
|
||||
result = subprocess.run(
|
||||
["brew", "list", package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.returncode == 0
|
||||
elif self.detected_pm == "apk":
|
||||
result = subprocess.run(
|
||||
["apk", "info", "-e", package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
||||
pass
|
||||
return False
|
||||
|
||||
def install_package(self, package: str) -> bool:
|
||||
result = {
|
||||
"package": package,
|
||||
"installed": self.check_package(package),
|
||||
"action": "none",
|
||||
"success": True,
|
||||
"message": ""
|
||||
}
|
||||
|
||||
if result["installed"]:
|
||||
result["message"] = f"包 '{package}' 已安装"
|
||||
return result
|
||||
|
||||
if not auto_install:
|
||||
result["action"] = "skipped"
|
||||
result["message"] = f"包 '{package}' 未安装,但自动安装已禁用"
|
||||
result["success"] = False
|
||||
return result
|
||||
|
||||
result["action"] = "installing"
|
||||
if self.install_package(package):
|
||||
result["installed"] = True
|
||||
result["success"] = True
|
||||
result["message"] = f"包 '{package}' 安装成功"
|
||||
else:
|
||||
result["success"] = False
|
||||
result["message"] = f"包 '{package}' 安装失败"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AutoDependencyPlugin(Plugin):
|
||||
def __init__(self):
|
||||
self._plugin_loader_ref = None
|
||||
self.scan_dirs = ["store"]
|
||||
self.auto_install = True
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
self._plugin_loader_ref = None
|
||||
if not self._plugin_loader_ref:
|
||||
try:
|
||||
from store.NebulaShell.plugin_bridge.main import use
|
||||
self._plugin_loader_ref = use("plugin-loader")
|
||||
except Exception:
|
||||
pass
|
||||
if not self._plugin_loader_ref and deps:
|
||||
self.scan_dirs = deps.get("scan_dirs", ["store"])
|
||||
self.auto_install = deps.get("auto_install", True)
|
||||
|
||||
if "plugin-loader" in deps:
|
||||
self._plugin_loader_ref = deps["plugin-loader"]
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def scan_plugin_manifests(self, base_dir: str = "store") -> List[Dict[str, Any]]:
|
||||
results = []
|
||||
base_path = Path(base_dir)
|
||||
|
||||
if not base_path.exists():
|
||||
return results
|
||||
|
||||
for vendor_dir in base_path.iterdir():
|
||||
if not vendor_dir.is_dir():
|
||||
continue
|
||||
|
||||
for plugin_dir in vendor_dir.iterdir():
|
||||
if not plugin_dir.is_dir():
|
||||
continue
|
||||
|
||||
manifest_file = plugin_dir / "manifest.json"
|
||||
if not manifest_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(manifest_file, "r", encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
system_deps = manifest.get("system_dependencies", [])
|
||||
|
||||
results.append({
|
||||
"plugin_name": plugin_dir.name.rstrip("}"),
|
||||
"plugin_dir": str(plugin_dir),
|
||||
"manifest": manifest,
|
||||
"system_dependencies": system_deps
|
||||
})
|
||||
except Exception as e:
|
||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
def check_all_dependencies(self, base_dir: str = "store") -> Dict[str, Any]:
|
||||
plugins = self.scan_plugin_manifests(base_dir)
|
||||
|
||||
all_deps = {}
|
||||
for plugin in plugins:
|
||||
for dep in plugin["system_dependencies"]:
|
||||
if dep not in all_deps:
|
||||
all_deps[dep] = []
|
||||
all_deps[dep].append(plugin["plugin_name"])
|
||||
|
||||
results = []
|
||||
installed_count = 0
|
||||
missing_count = 0
|
||||
|
||||
for package, plugin_names in all_deps.items():
|
||||
is_installed = self.checker.check_package(package)
|
||||
if is_installed:
|
||||
installed_count += 1
|
||||
else:
|
||||
missing_count += 1
|
||||
|
||||
results.append({
|
||||
"package": package,
|
||||
"installed": is_installed,
|
||||
"required_by": plugin_names
|
||||
})
|
||||
|
||||
return {
|
||||
"total_plugins": len(plugins),
|
||||
"plugins_with_deps": sum(1 for p in plugins if p["system_dependencies"]),
|
||||
"dependencies": results,
|
||||
"missing_count": missing_count,
|
||||
"installed_count": installed_count
|
||||
}
|
||||
|
||||
def install_missing_dependencies(self, base_dir: str = "store") -> Dict[str, Any]:
|
||||
check_result = self.check_all_dependencies(base_dir)
|
||||
|
||||
to_install = [dep for dep in check_result["dependencies"] if not dep["installed"]]
|
||||
|
||||
install_results = []
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for dep in to_install:
|
||||
result = self.checker.check_and_install(dep["package"], auto_install=True)
|
||||
result["required_by"] = dep["required_by"]
|
||||
install_results.append(result)
|
||||
|
||||
if result["success"]:
|
||||
success_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
return {
|
||||
"total_to_install": len(to_install),
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"results": install_results
|
||||
}
|
||||
|
||||
def get_system_info(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"scan_dirs": self.scan_dirs,
|
||||
"auto_install": self.auto_install
|
||||
}
|
||||
|
||||
def register_services(self, injector):
|
||||
def scan_deps(scan_dir: str = "store") -> Dict[str, Any]:
|
||||
return self.check_all_dependencies(scan_dir)
|
||||
|
||||
injector.register_function(
|
||||
"auto-dependency:scan",
|
||||
scan_deps,
|
||||
"scan all plugin system dependencies"
|
||||
)
|
||||
|
||||
def check_deps(scan_dir: str = "store") -> Dict[str, Any]:
|
||||
return self.check_all_dependencies(scan_dir)
|
||||
|
||||
injector.register_function(
|
||||
"auto-dependency:check",
|
||||
check_deps,
|
||||
"check if all declared system deps are installed"
|
||||
)
|
||||
|
||||
def install_deps(scan_dir: str = "store") -> Dict[str, Any]:
|
||||
return self.install_missing_dependencies(scan_dir)
|
||||
|
||||
injector.register_function(
|
||||
"auto-dependency:install",
|
||||
install_deps,
|
||||
"install missing system dependencies"
|
||||
)
|
||||
|
||||
def get_info() -> Dict[str, Any]:
|
||||
return self.get_system_info()
|
||||
|
||||
injector.register_function(
|
||||
"auto-dependency:info",
|
||||
get_info,
|
||||
"get auto-dependency plugin system info"
|
||||
)
|
||||
|
||||
|
||||
def New() -> AutoDependencyPlugin:
|
||||
return AutoDependencyPlugin()
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "auto-dependency",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "依赖自动安装插件 - 扫描所有插件的声明文件,检查并安装系统依赖",
|
||||
"type": "core"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"scan_dirs": ["store"],
|
||||
"package_manager": "auto",
|
||||
"auto_install": true,
|
||||
"pl_injection": false
|
||||
}
|
||||
},
|
||||
"dependencies": ["plugin-loader"],
|
||||
"permissions": ["*"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "BRVmR6gX5do7yBsBCtR9jk5/YoE6igio8d3IVNxAtwAtkBdS2Z3LNv9VwMBXeqOE84Dz1+/ypkQO+rdh9VZpGOpAPGxjCyArff9oS3nW6gazMZdLfMKrtsHxVBAL4Ycjb1NmQ3W0kdZa/aS+r2Q/tqVMJ62bqVR5Lbrc2H8eG/i1gPZsEu5tA7KC9pB8oDfaAY/QxeDczg32zWqh9UDD59Hp7TQMZhsWXsH9FgfvKjYKjcsQUEXs6ijUJ6PxHuc2Jx71xhD/IXseOTmnDCMe+8JdPA5aaVN/TEgmT99RXv62wHR+tulyaCYRd/P3sTItSSb1UYfLqEGBumetNAAGdgf33DMijUHKvufuha0JNOm6CCk+8UGbnYnG79HyaBz+pWfiF/pFX+LV7HTJTkBwQc3vXcvXep25UDspSkL+x2w3f1mk9S/oA5mT2go4kSaORxkCb1fAbh74Bn51VRmQV8XLSUOoZvWHjiaMkMdLsyPyTi2+fxqrDD7ehgeQBp3cNSoiGViqYcFcg2xCuHo2P/W441cZMOscfawdLJxg3N4+UC41LTooXN1+IBWzG7jrGTLyeXAFxGeOBo165WoAnsQZ9hh+uj/plv+LIU/mmOBSpJZIb4SuVJfoEcIDGpa7iieVr//8cTnbNTt9zh3GWYuW1NPIm+/WT4YoPfeAs/M=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775964953.1082504,
|
||||
"plugin_hash": "8894b78ac59c0154acaeb9a976f80588ece406e55079ca633c3b2bd839098d40",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
class QualityCheck:
|
||||
def check(self, filepath: str, content: str) -> list:
|
||||
issues = []
|
||||
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
lines = node.end_lineno - node.lineno
|
||||
if lines > 100:
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": node.lineno,
|
||||
"severity": "warning",
|
||||
"type": "long_function",
|
||||
"message": f"函数 {node.name} 过长 ({lines} 行)"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return issues
|
||||
|
||||
def _check_parameter_count(self, filepath: str, content: str) -> list:
|
||||
issues = []
|
||||
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
complexity = self._calculate_complexity(node)
|
||||
if complexity > 10:
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": node.lineno,
|
||||
"severity": "warning",
|
||||
"type": "high_complexity",
|
||||
"message": f"函数 {node.name} 复杂度过高 (圈复杂度: {complexity})"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return issues
|
||||
|
||||
def _calculate_complexity(self, node: ast.AST) -> int:
|
||||
"""计算圈复杂度"""
|
||||
complexity = 1
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, (ast.If, ast.While, ast.For, ast.AsyncFor)):
|
||||
complexity += 1
|
||||
elif isinstance(child, ast.ExceptHandler):
|
||||
complexity += 1
|
||||
elif isinstance(child, ast.BoolOp):
|
||||
complexity += len(child.values) - 1
|
||||
elif isinstance(child, (ast.And, ast.Or)):
|
||||
complexity += 1
|
||||
return complexity
|
||||
@@ -1,200 +0,0 @@
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ReferenceCheck:
|
||||
STD_MODULES = {
|
||||
'os', 'sys', 'json', 're', 'time', 'datetime', 'pathlib',
|
||||
'typing', 'collections', 'functools', 'itertools', 'io',
|
||||
'string', 'math', 'random', 'hashlib', 'hmac', 'secrets',
|
||||
'urllib', 'http', 'email', 'html', 'xml', 'csv', 'configparser',
|
||||
'logging', 'warnings', 'traceback', 'inspect', 'importlib',
|
||||
'threading', 'multiprocessing', 'subprocess', 'socket',
|
||||
'asyncio', 'concurrent', 'queue', 'contextlib', 'abc',
|
||||
'enum', 'dataclasses', 'copy', 'pprint', 'textwrap',
|
||||
'struct', 'codecs', 'locale', 'gettext', 'argparse',
|
||||
'unittest', 'doctest', 'pdb', 'profile', 'timeit',
|
||||
'tempfile', 'glob', 'fnmatch', 'stat', 'fileinput',
|
||||
'shutil', 'pickle', 'shelve', 'sqlite3', 'dbm',
|
||||
'gzip', 'bz2', 'lzma', 'zipfile', 'tarfile',
|
||||
'base64', 'binascii', 'quopri', 'uu',
|
||||
}
|
||||
|
||||
BUILTINS = {
|
||||
'print', 'len', 'str', 'int', 'float', 'bool', 'list', 'dict',
|
||||
'set', 'tuple', 'range', 'enumerate', 'zip', 'map', 'filter',
|
||||
'sorted', 'reversed', 'min', 'max', 'sum', 'abs', 'round',
|
||||
'isinstance', 'issubclass', 'type', 'id', 'hash', 'repr',
|
||||
'True', 'False', 'None', 'Exception', 'ValueError', 'TypeError',
|
||||
'KeyError', 'AttributeError', 'ImportError', 'FileNotFoundError',
|
||||
'IndexError', 'RuntimeError', 'StopIteration', 'GeneratorExit',
|
||||
'staticmethod', 'classmethod', 'property', 'super',
|
||||
'open', 'input', 'format', 'hex', 'oct', 'bin', 'chr', 'ord',
|
||||
'dir', 'vars', 'locals', 'globals', 'callable', 'getattr',
|
||||
'setattr', 'hasattr', 'delattr', 'exec', 'eval', 'compile',
|
||||
'any', 'all', 'slice', 'frozenset', 'bytearray', 'bytes',
|
||||
'memoryview', 'complex', 'divmod', 'pow', 'object',
|
||||
'dict', 'list', 'str', 'int', 'float', 'bool', 'set',
|
||||
'tuple', 'Exception', 'ValueError', 'TypeError', 'KeyError',
|
||||
'self', 'cls', 'args', 'kwargs',
|
||||
}
|
||||
|
||||
def __init__(self, project_root: str = "."):
|
||||
self.project_root = Path(project_root)
|
||||
self._available_modules = set(self.STD_MODULES)
|
||||
self._scan_project_modules()
|
||||
|
||||
def _scan_project_modules(self):
|
||||
"""扫描项目目录下的所有 Python 模块"""
|
||||
store_dir = self.project_root / "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
|
||||
self._scan_plugin_modules(plugin_dir, plugin_dir.name)
|
||||
|
||||
def _scan_plugin_modules(self, plugin_dir: Path, base_name: str):
|
||||
"""扫描单个插件目录下的模块"""
|
||||
if not plugin_dir.exists():
|
||||
return
|
||||
|
||||
for item in plugin_dir.iterdir():
|
||||
if item.is_file() and item.name.endswith(".py") and item.name != "__init__.py":
|
||||
module_name = item.name[:-3]
|
||||
self._available_modules.add(f"{base_name}.{module_name}")
|
||||
elif item.is_dir() and (item / "__init__.py").exists():
|
||||
self._available_modules.add(f"{base_name}.{item.name}")
|
||||
|
||||
def check(self, filepath: str, content: str) -> list:
|
||||
issues = []
|
||||
file_path = Path(filepath)
|
||||
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name.startswith('oss.') or alias.name == 'oss':
|
||||
continue
|
||||
if alias.name in ('websockets', 'yaml', 'click'):
|
||||
continue
|
||||
if not self._is_module_available(alias.name, file_path):
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": node.lineno,
|
||||
"severity": "critical",
|
||||
"type": "import_error",
|
||||
"message": f"无法导入模块: {alias.name}"
|
||||
})
|
||||
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level and node.level > 0:
|
||||
continue
|
||||
|
||||
if node.module and (node.module.startswith('oss.') or node.module == 'oss'):
|
||||
continue
|
||||
|
||||
if node.module and node.module.split('.')[0] in ('websockets', 'yaml', 'click'):
|
||||
continue
|
||||
|
||||
if node.module:
|
||||
if not self._is_module_available(node.module, file_path):
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": node.lineno,
|
||||
"severity": "critical",
|
||||
"type": "import_error",
|
||||
"message": f"无法导入模块: {node.module}"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _is_module_available(self, module_name: str, file_path: Optional[Path] = None) -> bool:
|
||||
"""检查模块是否可用"""
|
||||
if module_name in self._available_modules:
|
||||
return True
|
||||
|
||||
base_module = module_name.split('.')[0]
|
||||
if base_module in self.STD_MODULES:
|
||||
return True
|
||||
|
||||
if module_name.startswith('oss.') or module_name == 'oss':
|
||||
return True
|
||||
|
||||
third_party = {'websockets', 'yaml', 'click', 'requests', 'flask', 'django', 'numpy', 'pandas'}
|
||||
if module_name.split('.')[0] in third_party:
|
||||
return True
|
||||
|
||||
if file_path:
|
||||
file_dir = file_path.parent
|
||||
sibling_module = file_dir / f"{module_name}.py"
|
||||
if sibling_module.exists():
|
||||
return True
|
||||
sibling_pkg = file_dir / module_name
|
||||
if sibling_pkg.is_dir() and (sibling_pkg / "__init__.py").exists():
|
||||
return True
|
||||
store_dir = self.project_root / "store"
|
||||
if store_dir.exists():
|
||||
for author_dir in store_dir.iterdir():
|
||||
if author_dir.is_dir():
|
||||
for plugin_dir in author_dir.iterdir():
|
||||
if plugin_dir.is_dir() and plugin_dir.name == module_name.split('.')[0]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _check_variable_references(self, filepath: str, tree: ast.AST, content: str) -> list:
|
||||
issues = []
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Attribute):
|
||||
if isinstance(node.value, ast.Name):
|
||||
var_name = node.value.id
|
||||
if var_name in ('None', 'True', 'False'):
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": node.lineno,
|
||||
"severity": "critical",
|
||||
"type": "attribute_error",
|
||||
"message": f"尝试访问 {var_name} 的属性: {node.attr}"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _is_name_defined(self, name: str, tree: ast.AST, line: int) -> bool:
|
||||
"""检查变量名是否在 AST 中定义"""
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if node.name == name:
|
||||
return True
|
||||
for arg in node.args.args:
|
||||
if arg.arg == name:
|
||||
return True
|
||||
elif isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == name:
|
||||
return True
|
||||
elif isinstance(node, ast.AnnAssign):
|
||||
if isinstance(node.target, ast.Name) and node.target.id == name:
|
||||
return True
|
||||
elif isinstance(node, ast.ClassDef):
|
||||
if node.name == name:
|
||||
return True
|
||||
elif isinstance(node, ast.With):
|
||||
for item in node.items:
|
||||
if item.optional_vars and isinstance(item.optional_vars, ast.Name):
|
||||
if item.optional_vars.id == name:
|
||||
return True
|
||||
elif isinstance(node, ast.ExceptHandler):
|
||||
if node.name and node.name == name:
|
||||
return True
|
||||
return False
|
||||
@@ -1,35 +0,0 @@
|
||||
class SecurityCheck:
|
||||
def check(self, filepath: str, content: str) -> list:
|
||||
issues = []
|
||||
patterns = ['password', 'secret', 'token', 'api_key', 'access_token']
|
||||
|
||||
for i, line in enumerate(content.split('\n'), 1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
for pattern in patterns:
|
||||
if pattern + ' = "' in line.lower() or pattern + " = '" in line.lower():
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": i,
|
||||
"severity": "critical",
|
||||
"type": "hardcoded_secret",
|
||||
"message": f"发现硬编码密钥: {line.strip()[:50]}"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _check_dangerous_functions(self, filepath: str, content: str) -> list:
|
||||
issues = []
|
||||
|
||||
if '../' in content and 'open(' in content:
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": 0,
|
||||
"severity": "warning",
|
||||
"type": "path_traversal_risk",
|
||||
"message": "可能存在路径穿越漏洞"
|
||||
})
|
||||
|
||||
return issues
|
||||
@@ -1,27 +0,0 @@
|
||||
class StyleCheck:
|
||||
def check(self, filepath: str, content: str) -> list:
|
||||
issues = []
|
||||
|
||||
for i, line in enumerate(content.split('\n'), 1):
|
||||
if len(line) > 120:
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": i,
|
||||
"severity": "info",
|
||||
"type": "line_too_long",
|
||||
"message": f"行过长 ({len(line)} 字符)"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _check_blank_lines(self, filepath: str, content: str) -> list:
|
||||
if content and not content.endswith('\n'):
|
||||
return [{
|
||||
"file": filepath,
|
||||
"line": len(content.split('\n')),
|
||||
"severity": "info",
|
||||
"type": "missing_final_newline",
|
||||
"message": "文件末尾缺少换行符"
|
||||
}]
|
||||
|
||||
return []
|
||||
@@ -1,78 +0,0 @@
|
||||
import ast
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from checks.security import SecurityCheck
|
||||
from checks.quality import QualityCheck
|
||||
from checks.style import StyleCheck
|
||||
from checks.references import ReferenceCheck
|
||||
from report.formatter import Formatter as ReportFormatter
|
||||
|
||||
|
||||
class CodeReviewer:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.security = SecurityCheck()
|
||||
self.quality = QualityCheck()
|
||||
self.style = StyleCheck()
|
||||
self.references = ReferenceCheck()
|
||||
self.formatter = ReportFormatter(config.get("report_format", "console"))
|
||||
|
||||
def run_check(self, scan_dirs: list) -> dict:
|
||||
issues = []
|
||||
files_scanned = 0
|
||||
start_time = time.time()
|
||||
|
||||
exclude_patterns = self.config.get("exclude_patterns", ["__pycache__"])
|
||||
max_file_size = self.config.get("max_file_size", 102400)
|
||||
|
||||
for scan_dir in scan_dirs:
|
||||
scan_path = Path(scan_dir)
|
||||
if not scan_path.exists() or not scan_path.is_dir():
|
||||
continue
|
||||
|
||||
for py_file in scan_path.rglob("*.py"):
|
||||
# 跳过排除目录
|
||||
if any(part in exclude_patterns for part in py_file.parts):
|
||||
continue
|
||||
|
||||
filepath = str(py_file)
|
||||
|
||||
# 检查文件大小
|
||||
if py_file.stat().st_size > max_file_size:
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": 0,
|
||||
"severity": "warning",
|
||||
"type": "file_too_large",
|
||||
"message": f"文件过大 ({py_file.stat().st_size} 字节),跳过检查"
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
files_scanned += 1
|
||||
issues.extend(self.security.check(filepath, content))
|
||||
issues.extend(self.quality.check(filepath, content))
|
||||
issues.extend(self.style.check(filepath, content))
|
||||
issues.extend(self.references.check(filepath, content))
|
||||
|
||||
except Exception as e:
|
||||
issues.append({
|
||||
"file": filepath,
|
||||
"line": 0,
|
||||
"severity": "error",
|
||||
"type": "parse_error",
|
||||
"message": f"文件解析失败: {e}"
|
||||
})
|
||||
|
||||
scan_time = round(time.time() - start_time, 2)
|
||||
return {
|
||||
"files_scanned": files_scanned,
|
||||
"total_issues": len(issues),
|
||||
"issues": issues,
|
||||
"scan_time": scan_time
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
class CodeReviewerPlugin:
|
||||
def __init__(self):
|
||||
self.reviewer = None
|
||||
self.config = {}
|
||||
|
||||
def meta(self):
|
||||
from oss.plugin.types import Metadata, PluginConfig, Manifest
|
||||
return Manifest(
|
||||
metadata=Metadata(
|
||||
name="code-reviewer",
|
||||
version="1.0.0",
|
||||
author="NebulaShell",
|
||||
description="代码审查器 - 自动扫描代码问题"
|
||||
),
|
||||
config=PluginConfig(
|
||||
enabled=True,
|
||||
args={
|
||||
"scan_dirs": ["store", "oss"],
|
||||
"exclude_patterns": ["__pycache__", "*.pyc"],
|
||||
"max_file_size": 102400,
|
||||
"report_format": "console"
|
||||
}
|
||||
),
|
||||
dependencies=[]
|
||||
)
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
config = {}
|
||||
if deps:
|
||||
config = deps.get("config", {})
|
||||
|
||||
self.config = {
|
||||
"scan_dirs": config.get("scan_dirs", ["store", "oss"]),
|
||||
"exclude_patterns": config.get("exclude_patterns", ["__pycache__"]),
|
||||
"max_file_size": config.get("max_file_size", 102400),
|
||||
"report_format": config.get("report_format", "console")
|
||||
}
|
||||
|
||||
from core.reviewer import CodeReviewer
|
||||
self.reviewer = CodeReviewer(self.config)
|
||||
Log.info("code-reviewer", "初始化完成")
|
||||
|
||||
def start(self):
|
||||
Log.info("code-reviewer", "插件已启动")
|
||||
|
||||
def stop(self):
|
||||
Log.error("code-reviewer", "插件已停止")
|
||||
|
||||
def check(self, dirs: list = None) -> dict:
|
||||
if not self.reviewer:
|
||||
return {"error": "code-reviewer 未初始化"}
|
||||
|
||||
scan_dirs = dirs if dirs else self.config.get("scan_dirs", ["store", "oss"])
|
||||
result = self.reviewer.run_check(scan_dirs)
|
||||
return result
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "code-reviewer",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "代码审查器 - 提供 oss check 功能,自动扫描代码问题",
|
||||
"type": "tool"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"scan_dirs": ["store", "oss"],
|
||||
"exclude_patterns": ["__pycache__", "*.pyc", "*.pyo"],
|
||||
"max_file_size": 102400,
|
||||
"report_format": "console"
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": ["*"]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
class Formatter:
|
||||
def __init__(self, format_type: str = "console"):
|
||||
self.format_type = format_type
|
||||
|
||||
def format(self, result: dict) -> str:
|
||||
lines = []
|
||||
lines.append("=" * 60)
|
||||
lines.append("代码审查报告")
|
||||
lines.append("=" * 60)
|
||||
lines.append(f"扫描文件: {result['files_scanned']}")
|
||||
lines.append(f"发现问题: {result['total_issues']}")
|
||||
lines.append(f"扫描时间: {result['scan_time']}s")
|
||||
lines.append("")
|
||||
|
||||
critical = [i for i in result['issues'] if i['severity'] == 'critical']
|
||||
warning = [i for i in result['issues'] if i['severity'] == 'warning']
|
||||
info = [i for i in result['issues'] if i['severity'] == 'info']
|
||||
|
||||
lines.append(f"🔴 严重: {len(critical)}")
|
||||
lines.append(f"🟡 警告: {len(warning)}")
|
||||
lines.append(f"🔵 提示: {len(info)}")
|
||||
lines.append("")
|
||||
|
||||
if critical:
|
||||
lines.append("严重问题:")
|
||||
for issue in critical:
|
||||
lines.append(f" - {issue['file']}:{issue['line']} - {issue['message']}")
|
||||
lines.append("")
|
||||
|
||||
if warning:
|
||||
lines.append("警告:")
|
||||
for issue in warning[:10]: lines.append(f" - {issue['file']}:{issue['line']} - {issue['message']}")
|
||||
if len(warning) > 10:
|
||||
lines.append(f" ... 还有 {len(warning) - 10} 个警告")
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 60)
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _format_json(self, result: dict) -> str:
|
||||
"""以 JSON 格式输出审查报告"""
|
||||
import json
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "vn4hpZQMQTX0d78Wlze2wtTHjN91qn1PIvsRTK7ZFVm8lZ3eQHrZz9X0uDWcKKjxf5FCI/UVKQOqLwYkHiGhcS7d7+v6UKKKIYph+aftHQRrEcOQtrSnrmDQrqSjEdL3mjkl0KTIwqkFySxVNn9ssmL16JCOtWpWpKU5CnKWVrbeEKvs6yZJrmVVr9C7iDGsNq0/aS3oPDI4vg1iaTYgg/2Sh1smJ0jNtE5EsCq78fcyUcSWTziwq8RnJvFsx8LP3cxacC1QuZIP3hTIrpnApAj0KqSTRDLKY7d7rsQAHgDlnbQfYVtA8x94x91R5ybeDpXwYPSwWMpb7P/7XBDJ5GKL56iFUCV0tceHNK9yyjaXdhf2oUTxfoC4ONOTnkmnP2pZ6vRLjd/0WX7qA0XUTmZtewWur1BnZeZwzOjI5K8IYCda5WKXLVyrH64XmBEAwkEu18LIO9xI+DnhbM7rR9/xO+cXHkOYtKgAJMHCzgi6o6tw/UgS9K0myoMeGg58gYaDIVbXpxpf3rHSyFQAwauI67oye7ZxNxJgKnnOtX92cpQLHDfML8psd+sAIuBazxqxe484qzF2k0F5ZZMP17V6Yd3UWUkvWMoKlktq14OwJ2Q67nrmt9OC+9Epzny4gkq/Q7ih85rGwMVxRvkKhxxLLelQLVIni363yOxn7UE=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775967256.7737296,
|
||||
"plugin_hash": "68f5ab432690beef86da1c167c704fdd6b60512a359e806516dce1c6be27b9c5",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/* Dashboard 仪表盘样式 */
|
||||
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.dashboard-section h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #666;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"refreshInterval": {
|
||||
"type": "number",
|
||||
"name": "刷新间隔",
|
||||
"description": "仪表盘数据自动刷新的间隔时间(秒)",
|
||||
"default": 2,
|
||||
"min": 1,
|
||||
"max": 60,
|
||||
"order": 1
|
||||
},
|
||||
"showDisk": {
|
||||
"type": "boolean",
|
||||
"name": "显示磁盘",
|
||||
"description": "是否在仪表盘显示磁盘使用率",
|
||||
"default": true,
|
||||
"order": 2
|
||||
},
|
||||
"diskThreshold": {
|
||||
"type": "number",
|
||||
"name": "磁盘警告阈值",
|
||||
"description": "磁盘使用率超过此值时显示警告颜色",
|
||||
"default": 80,
|
||||
"min": 50,
|
||||
"max": 95,
|
||||
"show_when": { "field": "showDisk", "value": true },
|
||||
"order": 3
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
class DashboardPlugin:
|
||||
def __init__(self):
|
||||
self.webui = None
|
||||
self.views_dir = os.path.join(os.path.dirname(__file__), 'views')
|
||||
self._start_time = time.time()
|
||||
self._history_len = 60
|
||||
self._cpu_history = deque(maxlen=self._history_len)
|
||||
self._ram_history = deque(maxlen=self._history_len)
|
||||
self._net_recv_history = deque(maxlen=self._history_len)
|
||||
self._net_sent_history = deque(maxlen=self._history_len)
|
||||
self._disk_read_history = deque(maxlen=self._history_len)
|
||||
self._disk_write_history = deque(maxlen=self._history_len)
|
||||
self._net_latency_history = deque(maxlen=self._history_len)
|
||||
self._last_net = None
|
||||
self._last_disk = None
|
||||
|
||||
def meta(self):
|
||||
from oss.plugin.types import Metadata, PluginConfig, Manifest
|
||||
return Manifest(
|
||||
metadata=Metadata(
|
||||
name="dashboard",
|
||||
version="2.0.0",
|
||||
author="NebulaShell",
|
||||
description="WebUI 仪表盘"
|
||||
),
|
||||
config=PluginConfig(enabled=True, args={}),
|
||||
dependencies=["http-api", "webui"]
|
||||
)
|
||||
|
||||
def set_webui(self, webui):
|
||||
self.webui = webui
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
if not self.webui:
|
||||
try:
|
||||
from store.NebulaShell.plugin_bridge.main import use
|
||||
self.webui = use("webui")
|
||||
except Exception:
|
||||
pass
|
||||
if self.webui:
|
||||
Log.info("dashboard", "已获取 WebUI 引用")
|
||||
self.webui.register_page(
|
||||
path='/dashboard',
|
||||
content_provider=self._render_content,
|
||||
nav_item={'icon': 'ri-dashboard-line', 'text': '仪表盘'}
|
||||
)
|
||||
if hasattr(self.webui, 'server') and self.webui.server:
|
||||
self.webui.server.router.get("/api/dashboard/stats", self._handle_stats_api)
|
||||
self.webui.server.router.get("/api/dashboard/history", self._handle_history_api)
|
||||
Log.info("dashboard", "已注册到 WebUI 导航")
|
||||
else:
|
||||
Log.warn("dashboard", "警告: 未找到 WebUI 依赖")
|
||||
|
||||
def _get_uptime_str(self):
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(2)
|
||||
start = time.time()
|
||||
s.connect(('8.8.8.8', 53))
|
||||
elapsed = (time.time() - start) * 1000
|
||||
s.close()
|
||||
return round(elapsed, 1)
|
||||
except Exception as e:
|
||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
||||
return 0.0
|
||||
|
||||
def _get_network_interfaces(self):
|
||||
try:
|
||||
interfaces = []
|
||||
addrs = psutil.net_if_addrs()
|
||||
stats = psutil.net_if_stats()
|
||||
for name, addr_list in addrs.items():
|
||||
if name == 'lo':
|
||||
continue
|
||||
info = {'name': name, 'ip': 'N/A', 'mac': 'N/A', 'is_up': False, 'speed': 0}
|
||||
for addr in addr_list:
|
||||
if addr.family == socket.AF_INET:
|
||||
info['ip'] = addr.address
|
||||
elif hasattr(psutil, 'AF_LINK') and addr.family == psutil.AF_LINK:
|
||||
info['mac'] = addr.address
|
||||
if name in stats:
|
||||
info['is_up'] = stats[name].isup
|
||||
info['speed'] = stats[name].speed
|
||||
interfaces.append(info)
|
||||
return interfaces
|
||||
except Exception as e:
|
||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
||||
return []
|
||||
|
||||
def _get_load_info(self):
|
||||
try:
|
||||
load1, load5, load15 = os.getloadavg()
|
||||
return {'load1': round(load1, 2), 'load5': round(load5, 2), 'load15': round(load15, 2)}
|
||||
except (OSError, AttributeError):
|
||||
return {'load1': 0, 'load5': 0, 'load15': 0}
|
||||
|
||||
def _handle_stats_api(self, request):
|
||||
try:
|
||||
cpu_percent = psutil.cpu_percent(interval=0.3)
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage('/')
|
||||
net = self._get_network_stats()
|
||||
disk_io = self._get_disk_io_stats()
|
||||
load = self._get_load_info()
|
||||
latency = self._get_network_latency()
|
||||
|
||||
self._cpu_history.append(round(cpu_percent, 1))
|
||||
self._ram_history.append(round(mem.percent, 1))
|
||||
self._net_recv_history.append(net['recv_rate'])
|
||||
self._net_sent_history.append(net['sent_rate'])
|
||||
self._disk_read_history.append(disk_io['read_rate'])
|
||||
self._disk_write_history.append(disk_io['write_rate'])
|
||||
self._net_latency_history.append(latency)
|
||||
|
||||
uptime_str = self._get_uptime_str()
|
||||
|
||||
data = {
|
||||
'cpu': {'percent': round(cpu_percent, 1), 'cores': psutil.cpu_count(logical=True)},
|
||||
'ram': {'percent': round(mem.percent, 1), 'used': round(mem.used / (1024**3), 1), 'total': round(mem.total / (1024**3), 1)},
|
||||
'disk': {'percent': round(disk.percent, 1), 'used': round(disk.used / (1024**3), 1), 'total': round(disk.total / (1024**3), 1)},
|
||||
'network': net,
|
||||
'disk_io': disk_io,
|
||||
'load': load,
|
||||
'latency': latency,
|
||||
'processes': len(psutil.pids()),
|
||||
'uptime': uptime_str
|
||||
}
|
||||
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(data))
|
||||
except Exception as e:
|
||||
return Response(status=500, headers={"Content-Type": "application/json"}, body=json.dumps({'error': str(e)}))
|
||||
|
||||
def _handle_history_api(self, request):
|
||||
try:
|
||||
data = {
|
||||
'cpu': list(self._cpu_history),
|
||||
'ram': list(self._ram_history),
|
||||
'net_recv': list(self._net_recv_history),
|
||||
'net_sent': list(self._net_sent_history),
|
||||
'disk_read': list(self._disk_read_history),
|
||||
'disk_write': list(self._disk_write_history),
|
||||
'latency': list(self._net_latency_history)
|
||||
}
|
||||
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(data))
|
||||
except Exception as e:
|
||||
return Response(status=500, headers={"Content-Type": "application/json"}, body=json.dumps({'error': str(e)}))
|
||||
|
||||
def start(self):
|
||||
Log.info("dashboard", "仪表盘已启动")
|
||||
|
||||
def stop(self):
|
||||
Log.error("dashboard", "仪表盘已停止")
|
||||
|
||||
def _render_content(self) -> str:
|
||||
html = """<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统仪表盘</title>
|
||||
<link rel="stylesheet" href="/assets/remixicon.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
.card { background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; }
|
||||
.card-title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; }
|
||||
.stat-card { background: #fff; }
|
||||
.stat-icon { width: 60px; height: 60px; margin: 0 auto 15px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28px; color: white; }
|
||||
.stat-icon.cpu { background: linear-gradient(135deg, #667eea, #764ba2); }
|
||||
.stat-icon.ram { background: linear-gradient(135deg, #f093fb, #f5576c); }
|
||||
.stat-icon.disk { background: linear-gradient(135deg, #4facfe, #00f2fe); }
|
||||
.stat-value { font-size: 24px; font-weight: 700; color: #333; }
|
||||
.stat-label { font-size: 14px; color: #666; }
|
||||
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; }
|
||||
.info-item { background: #f8f9fa; }
|
||||
.info-label { font-size: 12px; color: #999; }
|
||||
.info-value { font-size: 14px; color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2 class="card-title"> 系统仪表盘</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon cpu"><i class="ri-cpu-line"></i></div>
|
||||
<div class="stat-value">0%</div>
|
||||
<div class="stat-label">CPU 使用率</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon ram"><i class="ri-memory-line"></i></div>
|
||||
<div class="stat-value">0%</div>
|
||||
<div class="stat-label">内存使用</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon disk"><i class="ri-hard-drive-line"></i></div>
|
||||
<div class="stat-value">0%</div>
|
||||
<div class="stat-label">磁盘使用</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => location.reload(), 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return html
|
||||
|
||||
register_plugin_type("DashboardPlugin", DashboardPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return DashboardPlugin()
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "dashboard",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "WebUI 仪表盘 - 系统监控/插件管理/安全配置/多语言支持",
|
||||
"type": "webui-extension"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"refresh_interval": 5,
|
||||
"show_system_metrics": true,
|
||||
"show_plugin_status": true,
|
||||
"show_security_alerts": true,
|
||||
"theme": "dark"
|
||||
}
|
||||
},
|
||||
"dependencies": ["http-api", "webui", "i18n"],
|
||||
"permissions": ["*"]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# dependency 依赖解析
|
||||
|
||||
插件依赖关系管理,使用拓扑排序确定加载顺序。
|
||||
|
||||
## 功能
|
||||
|
||||
- 拓扑排序(Kahn 算法)
|
||||
- 循环依赖检测(DFS)
|
||||
- 缺失依赖检测
|
||||
- 自动按依赖顺序加载插件
|
||||
|
||||
## 使用
|
||||
|
||||
```python
|
||||
dep = dependency_plugin
|
||||
|
||||
# 添加插件及其依赖
|
||||
dep.add_plugin("plugin-a", ["plugin-b", "plugin-c"])
|
||||
dep.add_plugin("plugin-b", [])
|
||||
dep.add_plugin("plugin-c", ["plugin-b"])
|
||||
|
||||
# 解析依赖顺序
|
||||
order = dep.resolve() # 返回 ["plugin-b", "plugin-c", "plugin-a"]
|
||||
|
||||
# 检查缺失依赖
|
||||
missing = dep.get_missing_deps()
|
||||
|
||||
# 获取加载顺序
|
||||
order = dep.get_order()
|
||||
```
|
||||
|
||||
## manifest.json 声明
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {...},
|
||||
"dependencies": ["lifecycle", "circuit-breaker"]
|
||||
}
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "JQaw//g6588907vGYH6SyqeXj9qHU5Azb7S/bjYm7rUrVsHqqIsIOEPB7IVsdf/wCnCdCa0LzTrEjmS6lKlEwXVjCCebhzyi64OJIXVOVckd2TJbREH0ZizO4KcEWgOqu56Ln3g8yMPHw5GylLABD5UN0q4F48PwUhram+cECu0SOY/bAHxYwi+nzJ0TcuES/J5cK480xv+NvxnylBhx1Udkkoiz9Y7b3pgglx+h57BuPEeHpJFbXQkXtty5Cf3sXzib0FEhicyIW1u5wmYSLz5yyLd/Pefavjfs6JrDG9J8gfPuestQzazQGsIMiQTy13DL8IDGAZ7AP2/mFQYrXuYLaBTxyhhMAkpfjIANzy+2pobeTZz2Cu4Sr6XMzXS4BkeCRDcHHBnttWVpp1+t5HpRgp3W8eiPcCzmUq6jo1cbd5zWGiR1gDEHePivmJaUi/bxlN0vyc7LjW7T+HuLUYhdSktbxv5BexMwcA7+2UHJzEnTVIc+xqoIT+ApPqqF2hLJFiAUdEJe8FRc/Bwihzh8tfM0xgYoqn8RQQ3eWVwVrK9vx0OZ8INumNZOyKPz8ZlGf3XAJv9UGUQ6Y42raYcDOFrgT+MS82tjAxf2nonm0/c3dhgNFZSy5Cfbvuqd9SYaxXejIcVni3MarVHZX3iKytOdv83cBtwPXRcfloc=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775969851.9656692,
|
||||
"plugin_hash": "aebef3fd9252245553bc458e4652b094839a5e64bde7cec13435ba1930a8dc0d",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
class DependencyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DependencyResolver:
|
||||
def add_dependency(self, name: str, dependencies: list[str]):
|
||||
self.graph[name] = dependencies
|
||||
|
||||
def resolve(self) -> list[str]:
|
||||
self._detect_cycles()
|
||||
|
||||
in_degree: dict[str, int] = {name: 0 for name in self.graph}
|
||||
who_depends_on: dict[str, list[str]] = {name: [] for name in self.graph}
|
||||
|
||||
for name, deps in self.graph.items():
|
||||
for dep in deps:
|
||||
if dep in in_degree:
|
||||
in_degree[name] += 1
|
||||
who_depends_on[dep].append(name)
|
||||
queue = [name for name, degree in in_degree.items() if degree == 0]
|
||||
result = []
|
||||
|
||||
while queue:
|
||||
node = queue.pop(0)
|
||||
result.append(node)
|
||||
for dependent in who_depends_on.get(node, []):
|
||||
in_degree[dependent] -= 1
|
||||
if in_degree[dependent] == 0:
|
||||
queue.append(dependent)
|
||||
|
||||
if len(result) != len(self.graph):
|
||||
raise DependencyError("无法解析依赖,可能存在循环依赖")
|
||||
|
||||
return result
|
||||
|
||||
def _detect_cycles(self):
|
||||
all_deps = set()
|
||||
for deps in self.graph.values():
|
||||
all_deps.update(deps)
|
||||
all_plugins = set(self.graph.keys())
|
||||
return list(all_deps - all_plugins)
|
||||
|
||||
|
||||
class DependencyPlugin(Plugin):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def add_plugin(self, name: str, dependencies: list[str]):
|
||||
return self.resolver.resolve()
|
||||
|
||||
def get_missing_deps(self) -> list[str]:
|
||||
return self.resolve()
|
||||
|
||||
|
||||
register_plugin_type("DependencyResolver", DependencyResolver)
|
||||
register_plugin_type("DependencyError", DependencyError)
|
||||
|
||||
|
||||
def New():
|
||||
return DependencyPlugin()
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "dependency",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
|
||||
"type": "core"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "example-with-deps",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "示例插件 - 演示如何声明系统依赖",
|
||||
"type": "example"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": [],
|
||||
"system_dependencies": ["curl", "git", "wget"],
|
||||
"permissions": []
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "firewall",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "防火墙服务 - 提供 IP 过滤/端口管理/访问控制/WebUI 规则配置",
|
||||
"type": "security"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"default_policy": "ACCEPT",
|
||||
"whitelist_enabled": false,
|
||||
"blacklist_enabled": true,
|
||||
"rate_limit_enabled": true,
|
||||
"rate_limit_requests": 100,
|
||||
"rate_limit_window": 60,
|
||||
"blocked_ips_file": "config/blocked_ips.txt",
|
||||
"allowed_ips_file": "config/allowed_ips.txt",
|
||||
"rules_file": "config/firewall_rules.json",
|
||||
"log_blocked": true,
|
||||
"notify_on_block": false
|
||||
}
|
||||
},
|
||||
"dependencies": ["http-api", "i18n"],
|
||||
"permissions": ["lifecycle", "plugin-storage"]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "frp-proxy",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "FRP 内网穿透服务 - 提供安全的内网服务暴露/反向代理/WebUI 配置管理",
|
||||
"type": "service"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"server_addr": "",
|
||||
"server_port": 7000,
|
||||
"auth_token": "",
|
||||
"tcp_mux": true,
|
||||
"heartbeat_interval": 30,
|
||||
"heartbeat_timeout": 90,
|
||||
"admin_addr": "127.0.0.1",
|
||||
"admin_port": 7400,
|
||||
"log_level": "info",
|
||||
"proxy_configs_dir": "config/proxies"
|
||||
}
|
||||
},
|
||||
"dependencies": ["http-api", "i18n"],
|
||||
"permissions": ["lifecycle", "plugin-storage"]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "ftp-server",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "FTP/SFTP 文件传输服务 - 提供安全的文件上传下载/目录管理/WebUI集成",
|
||||
"type": "service"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"ftp_port": 2121,
|
||||
"sftp_port": 2222,
|
||||
"passive_ports": [30000, 30010],
|
||||
"max_connections": 50,
|
||||
"timeout": 300,
|
||||
"allow_anonymous": false,
|
||||
"root_dir": "/workspace/ftp-root",
|
||||
"chroot_enabled": true,
|
||||
"ssl_enabled": true,
|
||||
"ssl_cert": "config/ftp.crt",
|
||||
"ssl_key": "config/ftp.key"
|
||||
}
|
||||
},
|
||||
"dependencies": ["http-api", "i18n"],
|
||||
"permissions": ["lifecycle", "plugin-storage"]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
# hot-reload 热插拔
|
||||
|
||||
运行时加载、卸载、更新插件,无需重启服务。
|
||||
|
||||
## 功能
|
||||
|
||||
- 运行时加载新插件
|
||||
- 运行时卸载插件
|
||||
- 运行时更新插件(热重载)
|
||||
- 自动监听文件变化(可选)
|
||||
- 模块缓存清理
|
||||
|
||||
## 使用
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
# 加载新插件
|
||||
hot_reload.load_plugin(Path("store/@{Author/new-plugin"))
|
||||
|
||||
# 卸载插件
|
||||
hot_reload.unload_plugin("plugin-name")
|
||||
|
||||
# 更新插件
|
||||
hot_reload.reload_plugin("plugin-name", Path("store/@{Author/plugin-name"))
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 插件必须实现 `init()`, `start()`, `stop()`
|
||||
- 卸载时会调用 `stop()`
|
||||
- 更新时先 `stop()` 再 `init()` + `start()`
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "vBf0JPwb5GjyM9vyp4AuncQKp092RpA07RZh+guhF51OKlVI5PphQEEvtMSy2uBsQ0V0RohRid/gazvB5l02DTuyqt2NcjFyPIZj2wm1gfWtJZWBK+Hp11gIPq13qhxDjdi1bs7H+tTOhVHJHkcoU1TsZuUPU+UYOuONbQhdwB+eqEMbNzVrPBPxb12W1SxRBAo/58q+eGI1QvbTv0FBu4fw10vyySGzd51t0psrBqw9xovKSq47AV96ZJeFEJvbfBTfJTg26VOX0cxLS5dmel9+yMhmidJNvOoL3mlZG2C92Xe9hdZAFxaRhMV3QgNKx3s6C+TQRBNx3ttUtBAzxVcXsGhCE0C+CfvbIpuyGHfgarSPJoiIPyp02numgMztFzAdFc66stULEpB3rHBlosUbDNmeuIMNcbCdKlH6R94xuYMg8E699DO67AGxZwZcaUN/vYmAa2DiffVUFcCFXgzABPzctJTYqTaD51KGlMSMHTeMTN3XCWJ79nkxHvt0Lgb0kWljOhcVaGW2t4JUgfupUD1DIwiZ7AlEC3K3JijsqWS633+Saa/+tOI4/V5VzVtExJt46cM/BSETYlHQtA8eDDl6BhbjtnmMaHSjGF75sgiagtj0DYsOvzKLJUVMT4nFjidzb2sR5lN3/S3ZSmBTUYA5/fDgiMnSfZaK4HQ=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775964953.0432403,
|
||||
"plugin_hash": "3b226c4e5278ade1ec0997abfd553d4c07724b8e9f69f79acb57e20e0d352817",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
class HotReloadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FileWatcher:
|
||||
def __init__(self, watch_dirs, extensions, callback):
|
||||
self.watch_dirs = watch_dirs
|
||||
self.extensions = extensions
|
||||
self.callback = callback
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self._file_times = {}
|
||||
self._init_file_times()
|
||||
|
||||
def _init_file_times(self):
|
||||
for watch_dir in self.watch_dirs:
|
||||
p = Path(watch_dir)
|
||||
if p.exists():
|
||||
for f in p.rglob("*"):
|
||||
if f.is_file() and f.suffix in self.extensions:
|
||||
self._file_times[str(f)] = f.stat().st_mtime
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
if self._thread:
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
def _watch_loop(self):
|
||||
pass
|
||||
|
||||
|
||||
class HotReloadPlugin:
|
||||
def __init__(self):
|
||||
self.plugin_loader_instance = None
|
||||
self.watcher: Optional[FileWatcher] = None
|
||||
self.watch_dirs: list[str] = []
|
||||
self.watch_extensions: list[str] = [".py", ".json"]
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
if not self.watch_dirs:
|
||||
self.watch_dirs = ["store"]
|
||||
self.start_watching()
|
||||
|
||||
def stop(self):
|
||||
if self.watcher:
|
||||
self.watcher.stop()
|
||||
|
||||
def set_plugin_loader(self, plugin_loader):
|
||||
self.plugin_loader_instance = plugin_loader
|
||||
|
||||
def set_watch_dirs(self, dirs: list[str]):
|
||||
self.watch_dirs = dirs
|
||||
|
||||
def start_watching(self):
|
||||
if self.watch_dirs and self.plugin_loader_instance:
|
||||
self.watcher = FileWatcher(
|
||||
self.watch_dirs,
|
||||
self.watch_extensions,
|
||||
self._on_file_change
|
||||
)
|
||||
self.watcher.start()
|
||||
|
||||
def _on_file_change(self, changes: list[tuple[str, Path]]):
|
||||
for change_type, file_path in changes:
|
||||
pass
|
||||
|
||||
def load_plugin(self, plugin_dir: Path) -> bool:
|
||||
try:
|
||||
plugin_name = plugin_dir.name
|
||||
if plugin_name in self.plugin_loader_instance.plugins:
|
||||
raise HotReloadError(f"Plugin already exists: {plugin_name}")
|
||||
|
||||
self.plugin_loader_instance.load(plugin_dir)
|
||||
info = self.plugin_loader_instance.plugins[plugin_name]
|
||||
instance = info["instance"]
|
||||
instance.init()
|
||||
instance.start()
|
||||
return True
|
||||
except Exception as e:
|
||||
raise HotReloadError(f"Failed to load plugin: {e}")
|
||||
|
||||
def unload_plugin(self, plugin_name: str) -> bool:
|
||||
try:
|
||||
self.plugin_loader_instance.unload(plugin_name)
|
||||
return True
|
||||
except Exception as e:
|
||||
raise HotReloadError(f"Failed to unload plugin: {e}")
|
||||
|
||||
def reload_plugin(self, plugin_name: str, plugin_dir: Path) -> bool:
|
||||
try:
|
||||
self.unload_plugin(plugin_name)
|
||||
return self.load_plugin(plugin_dir)
|
||||
except Exception as e:
|
||||
raise HotReloadError(f"Failed to reload plugin: {e}")
|
||||
|
||||
|
||||
def register_plugin_type(name, cls):
|
||||
pass
|
||||
|
||||
|
||||
def New():
|
||||
return HotReloadPlugin()
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "hot-reload",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "热插拔 - 运行时加载/卸载/更新插件",
|
||||
"type": "utility"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"watch_dirs": ["store"],
|
||||
"watch_extensions": [".py", ".json"]
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": ["plugin-loader"]
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
# http-api HTTP API 服务
|
||||
|
||||
提供 HTTP RESTful API 服务,支持路由、中间件等功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- HTTP 服务器(GET/POST/PUT/DELETE)
|
||||
- 路由匹配(支持参数路由 `:id`)
|
||||
- 中间件链(CORS/日志/限流)
|
||||
- 分散式布局(每个文件 < 200 行)
|
||||
|
||||
## 路由使用
|
||||
|
||||
```python
|
||||
# 在插件中获取 router
|
||||
http_plugin = plugin_mgr.get("http-api")
|
||||
router = http_plugin.router
|
||||
|
||||
# 添加路由
|
||||
router.get("/health", lambda req: Response(status=200, body='{"status": "ok"}'))
|
||||
router.get("/api/users", handle_users)
|
||||
router.post("/api/users", handle_create_user)
|
||||
router.get("/api/users/:id", handle_user_by_id)
|
||||
```
|
||||
|
||||
## 中间件
|
||||
|
||||
```python
|
||||
middleware = http_plugin.middleware
|
||||
|
||||
# 添加自定义中间件
|
||||
class MyMiddleware(Middleware):
|
||||
def process(self, ctx, next_fn):
|
||||
# 前置处理
|
||||
resp = next_fn() # 继续执行
|
||||
# 后置处理
|
||||
return resp
|
||||
|
||||
middleware.add(MyMiddleware())
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8080
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "0WK7Njn0KAUP+jfg/uuJxwW0/tWCF+WieK0N0T2crWbvutKQmEOtaNDHnjT6qFz1dcI4+ba3julE4fFi3W3xFiToMEP2VcPXe0WNQ9/kvKNTKSDbwadiBssf43TO1G9E1BxNMxVM91mN8iqybuy+VMdU0Esv2rJ5dcwwwsnT9NWot2RQLez75PRhmMtJpEWRUmrZn2r+u5QnQdjxucONq9Nhwxw0eheTxMCu8IDvIiO6QIWP5ErA/wUz+Hg6IoEZwcVif/lSN2EMqNGqPNR/nIWWVXo9CXWB9qMZZApgEnAZfKYGCAkLzSTwqG64T4iJh4deGxafyMhsONckqRaG82NRTLuzHMReP5+VAichuEGbHI7nxXFOFG7q1mgQQLmHm3LB577usAgCNCh5X3i8SMAj7Sutykxhj0ZyTqMnOfpwnzE2tsNisJF0/8Kw22k7dZChV1obOeLWXjy5InLjdm4hIWTp7wMPjSNWRMZGR+1aZHi9XA1GKd965/30jmo876EXX23xoTAN4ZRhZNlcQg710LhycNohggnQ7qzB9LsV3Ckgh7aY/V/hzND6bpRADCGu62sZtBye2P1yaaAorC8+hRaiJoXlV9Yukg+3yhfKC+qTbn307fI53kgcw1KMSeGGctfTYJUOfK8u0mYsGi50bnM+2Tz45YJiwwdOJJk=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775960645.890869,
|
||||
"plugin_hash": "ca13c933ffa2c5dd8874e3ad6f7b8dda5dd9a5f9c24be6aeb47228d65097a280",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
"""
|
||||
CSRF 防护中间件
|
||||
"""
|
||||
import json
|
||||
import hashlib
|
||||
import secrets
|
||||
import time
|
||||
from typing import Dict, Optional
|
||||
from collections import defaultdict
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class CsrfTokenManager:
|
||||
"""CSRF 令牌管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("CSRF_ENABLED", True)
|
||||
self.token_lifetime = self.config.get("CSRF_TOKEN_LIFETIME", 3600) # 1小时
|
||||
self.tokens: Dict[str, tuple] = {} # {token: (timestamp, session_id)}
|
||||
self.session_tokens: Dict[str, str] = defaultdict(str) # {session_id: token}
|
||||
self.lock = None # 延迟初始化
|
||||
|
||||
def _init_lock(self):
|
||||
"""延迟初始化锁"""
|
||||
if self.lock is None:
|
||||
import threading
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def generate_token(self, session_id: str) -> str:
|
||||
"""生成CSRF令牌"""
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
self._init_lock()
|
||||
|
||||
# 如果已有令牌,直接返回
|
||||
if session_id in self.session_tokens:
|
||||
return self.session_tokens[session_id]
|
||||
|
||||
# 生成新的令牌
|
||||
token = secrets.token_urlsafe(32)
|
||||
timestamp = time.time()
|
||||
|
||||
# 存储令牌
|
||||
self.tokens[token] = (timestamp, session_id)
|
||||
self.session_tokens[session_id] = token
|
||||
|
||||
return token
|
||||
|
||||
def validate_token(self, token: str, session_id: str) -> bool:
|
||||
"""验证CSRF令牌"""
|
||||
if not self.enabled:
|
||||
return True
|
||||
|
||||
self._init_lock()
|
||||
|
||||
# 清理过期令牌
|
||||
current_time = time.time()
|
||||
expired_tokens = []
|
||||
|
||||
for stored_token, (timestamp, stored_session_id) in self.tokens.items():
|
||||
if current_time - timestamp > self.token_lifetime:
|
||||
expired_tokens.append(stored_token)
|
||||
elif stored_session_id == session_id and stored_token == token:
|
||||
# 令牌有效,更新时间戳
|
||||
self.tokens[stored_token] = (current_time, stored_session_id)
|
||||
return True
|
||||
|
||||
# 清理过期令牌
|
||||
for expired_token in expired_tokens:
|
||||
if expired_token in self.tokens:
|
||||
del self.tokens[expired_token]
|
||||
|
||||
return False
|
||||
|
||||
def cleanup_expired_tokens(self):
|
||||
"""清理过期令牌"""
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
self._init_lock()
|
||||
|
||||
current_time = time.time()
|
||||
expired_tokens = []
|
||||
|
||||
for token, (timestamp, _) in self.tokens.items():
|
||||
if current_time - timestamp > self.token_lifetime:
|
||||
expired_tokens.append(token)
|
||||
|
||||
for token in expired_tokens:
|
||||
if token in self.tokens:
|
||||
del self.tokens[token]
|
||||
|
||||
|
||||
class CsrfMiddleware:
|
||||
"""CSRF 防护中间件"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("CSRF_ENABLED", True)
|
||||
self.exempt_paths = {
|
||||
"/health", "/favicon.ico", "/api/status",
|
||||
"/api/health", "/login", "/logout"
|
||||
}
|
||||
|
||||
# 初始化令牌管理器
|
||||
self.token_manager = CsrfTokenManager()
|
||||
|
||||
def get_session_id(self, request: Request) -> str:
|
||||
"""获取会话ID"""
|
||||
# 从Cookie中获取会话ID
|
||||
session_cookie = request.headers.get("Cookie", "")
|
||||
for cookie in session_cookie.split(";"):
|
||||
if "session_id" in cookie:
|
||||
return cookie.split("=")[1].strip()
|
||||
|
||||
# 从Authorization头获取(如果使用Bearer token)
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
return f"token:{auth_header[7:]}"
|
||||
|
||||
# 使用IP地址作为会话ID(简化实现)
|
||||
return f"ip:{request.headers.get('Remote-Addr', 'unknown')}"
|
||||
|
||||
def create_csrf_token_response(self, session_id: str) -> Response:
|
||||
"""创建CSRF令牌响应"""
|
||||
token = self.token_manager.generate_token(session_id)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Set-Cookie": f"csrf_token={token}; Path=/; HttpOnly; SameSite=Lax"
|
||||
},
|
||||
body=json.dumps({
|
||||
"csrf_token": token,
|
||||
"message": "CSRF token generated"
|
||||
})
|
||||
)
|
||||
|
||||
def process(self, ctx: dict, next_fn) -> Optional[Response]:
|
||||
"""处理CSRF防护逻辑"""
|
||||
if not self.enabled:
|
||||
return next_fn()
|
||||
|
||||
request = ctx.get("request")
|
||||
if not request:
|
||||
return next_fn()
|
||||
|
||||
# 检查是否为豁免路径
|
||||
if request.path in self.exempt_paths:
|
||||
return next_fn()
|
||||
|
||||
# 只对需要保护的请求方法进行CSRF检查
|
||||
if request.method not in ["POST", "PUT", "DELETE", "PATCH"]:
|
||||
return next_fn()
|
||||
|
||||
# 获取会话ID
|
||||
session_id = self.get_session_id(request)
|
||||
|
||||
# 获取CSRF令牌
|
||||
csrf_token = None
|
||||
if request.headers.get("Content-Type") == "application/json":
|
||||
try:
|
||||
body = json.loads(request.body)
|
||||
csrf_token = body.get("csrf_token")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 从Header中获取CSRF令牌
|
||||
if not csrf_token:
|
||||
csrf_token = request.headers.get("X-CSRF-Token")
|
||||
|
||||
# 验证CSRF令牌
|
||||
if not csrf_token or not self.token_manager.validate_token(csrf_token, session_id):
|
||||
Log.warn("csrf", f"CSRF验证失败: {request.method} {request.path}")
|
||||
return Response(
|
||||
status=403,
|
||||
body='{"error": "CSRF token invalid or missing", "message": "请求被拒绝,请刷新页面重试"}',
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
return next_fn()
|
||||
@@ -1,6 +0,0 @@
|
||||
class ApiEvent:
|
||||
type: str
|
||||
request: Any = None
|
||||
response: Any = None
|
||||
error: Exception = None
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
@@ -1,209 +0,0 @@
|
||||
"""
|
||||
输入验证中间件
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class InputValidator:
|
||||
"""输入验证器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("INPUT_VALIDATION_ENABLED", True)
|
||||
|
||||
# 预定义的模式
|
||||
self.patterns = {
|
||||
"email": r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||
"username": r'^[a-zA-Z0-9_]{3,20}$',
|
||||
"password": r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$',
|
||||
"api_key": r'^[a-zA-Z0-9_-]{32,}$',
|
||||
"uuid": r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||
"ip_address": r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
|
||||
"url": r'^https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[/\w\.-]*\??[/\w\.-=&]*$'
|
||||
}
|
||||
|
||||
# 端点特定的验证规则
|
||||
self.endpoint_rules = {
|
||||
"/api/auth/login": {
|
||||
"methods": ["POST"],
|
||||
"required_fields": ["username", "password"],
|
||||
"field_rules": {
|
||||
"username": {"type": "str", "min_length": 3, "max_length": 20, "pattern": "username"},
|
||||
"password": {"type": "str", "min_length": 8, "max_length": 100}
|
||||
}
|
||||
},
|
||||
"/api/auth/register": {
|
||||
"methods": ["POST"],
|
||||
"required_fields": ["username", "email", "password"],
|
||||
"field_rules": {
|
||||
"username": {"type": "str", "min_length": 3, "max_length": 20, "pattern": "username"},
|
||||
"email": {"type": "str", "pattern": "email"},
|
||||
"password": {"type": "str", "min_length": 8, "max_length": 100, "pattern": "password"}
|
||||
}
|
||||
},
|
||||
"/api/users": {
|
||||
"methods": ["GET", "POST"],
|
||||
"field_rules": {
|
||||
"limit": {"type": "int", "min_value": 1, "max_value": 100},
|
||||
"offset": {"type": "int", "min_value": 0},
|
||||
"search": {"type": "str", "max_length": 100}
|
||||
}
|
||||
},
|
||||
"/api/pkg-manager/search": {
|
||||
"methods": ["GET"],
|
||||
"field_rules": {
|
||||
"query": {"type": "str", "min_length": 1, "max_length": 100},
|
||||
"limit": {"type": "int", "min_value": 1, "max_value": 50},
|
||||
"page": {"type": "int", "min_value": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def validate_field(self, field_name: str, value: Any, rules: Dict) -> Optional[str]:
|
||||
"""验证单个字段"""
|
||||
# 类型验证
|
||||
if "type" in rules:
|
||||
expected_type = rules["type"]
|
||||
if expected_type == "str" and not isinstance(value, str):
|
||||
return f"{field_name} 必须是字符串"
|
||||
elif expected_type == "int" and not isinstance(value, int):
|
||||
return f"{field_name} 必须是整数"
|
||||
elif expected_type == "float" and not isinstance(value, (int, float)):
|
||||
return f"{field_name} 必须是数字"
|
||||
elif expected_type == "bool" and not isinstance(value, bool):
|
||||
return f"{field_name} 必须是布尔值"
|
||||
|
||||
# 长度验证
|
||||
if isinstance(value, str):
|
||||
if "min_length" in rules and len(value) < rules["min_length"]:
|
||||
return f"{field_name} 长度不能少于 {rules['min_length']} 个字符"
|
||||
if "max_length" in rules and len(value) > rules["max_length"]:
|
||||
return f"{field_name} 长度不能超过 {rules['max_length']} 个字符"
|
||||
|
||||
# 数值范围验证
|
||||
if isinstance(value, (int, float)):
|
||||
if "min_value" in rules and value < rules["min_value"]:
|
||||
return f"{field_name} 不能小于 {rules['min_value']}"
|
||||
if "max_value" in rules and value > rules["max_value"]:
|
||||
return f"{field_name} 不能大于 {rules['max_value']}"
|
||||
|
||||
# 模式验证
|
||||
if "pattern" in rules and isinstance(value, str):
|
||||
pattern = self.patterns.get(rules["pattern"])
|
||||
if pattern and not re.match(pattern, value):
|
||||
return f"{field_name} 格式不正确"
|
||||
|
||||
# 枚举验证
|
||||
if "choices" in rules and value not in rules["choices"]:
|
||||
return f"{field_name} 必须是以下值之一: {', '.join(rules['choices'])}"
|
||||
|
||||
return None
|
||||
|
||||
def validate_request(self, request: Request) -> Optional[str]:
|
||||
"""验证请求"""
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
# 检查是否有对应的验证规则
|
||||
rules = None
|
||||
for endpoint, rule in self.endpoint_rules.items():
|
||||
if request.path.startswith(endpoint):
|
||||
rules = rule
|
||||
break
|
||||
|
||||
if not rules:
|
||||
return None
|
||||
|
||||
# 检查请求方法
|
||||
if "methods" in rules and request.method not in rules["methods"]:
|
||||
return f"不支持的请求方法: {request.method}"
|
||||
|
||||
# 解析请求体
|
||||
body_data = {}
|
||||
if request.method in ["POST", "PUT", "PATCH"] and request.body:
|
||||
try:
|
||||
body_data = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return "无效的JSON格式"
|
||||
|
||||
# 解析查询参数
|
||||
query_params = {}
|
||||
if request.query:
|
||||
try:
|
||||
query_params = json.loads(request.query)
|
||||
except:
|
||||
# 如果不是JSON,按简单键值对处理
|
||||
query_params = {}
|
||||
|
||||
# 检查必需字段
|
||||
if "required_fields" in rules:
|
||||
for field in rules["required_fields"]:
|
||||
if field not in body_data and field not in query_params:
|
||||
return f"缺少必需字段: {field}"
|
||||
|
||||
# 验证字段规则
|
||||
if "field_rules" in rules:
|
||||
all_data = {**body_data, **query_params}
|
||||
|
||||
for field_name, field_rules in rules["field_rules"].items():
|
||||
if field_name in all_data:
|
||||
error = self.validate_field(field_name, all_data[field_name], field_rules)
|
||||
if error:
|
||||
return error
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class InputValidationMiddleware:
|
||||
"""输入验证中间件"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("INPUT_VALIDATION_ENABLED", True)
|
||||
self.validator = InputValidator()
|
||||
|
||||
# 豁免路径(不进行验证)
|
||||
self.exempt_paths = {
|
||||
"/health", "/favicon.ico", "/api/status",
|
||||
"/api/health", "/metrics"
|
||||
}
|
||||
|
||||
def create_validation_error_response(self, error_message: str) -> Response:
|
||||
"""创建验证错误响应"""
|
||||
return Response(
|
||||
status=400,
|
||||
body=json.dumps({
|
||||
"error": "Validation Error",
|
||||
"message": error_message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
def process(self, ctx: dict, next_fn) -> Optional[Response]:
|
||||
"""处理输入验证逻辑"""
|
||||
if not self.enabled:
|
||||
return next_fn()
|
||||
|
||||
request = ctx.get("request")
|
||||
if not request:
|
||||
return next_fn()
|
||||
|
||||
# 检查是否为豁免路径
|
||||
if request.path in self.exempt_paths:
|
||||
return next_fn()
|
||||
|
||||
# 验证请求
|
||||
validation_error = self.validator.validate_request(request)
|
||||
if validation_error:
|
||||
Log.warn("validation", f"输入验证失败: {validation_error} ({request.method} {request.path})")
|
||||
return self.create_validation_error_response(validation_error)
|
||||
|
||||
return next_fn()
|
||||
@@ -1,43 +0,0 @@
|
||||
import json
|
||||
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
from .server import HttpServer, Response
|
||||
from .router import HttpRouter
|
||||
from .middleware import MiddlewareChain
|
||||
|
||||
|
||||
class HttpApiPlugin(Plugin):
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.router = HttpRouter()
|
||||
self.middleware = MiddlewareChain()
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
self.server = HttpServer(
|
||||
router=self.router,
|
||||
middleware=self.middleware,
|
||||
)
|
||||
self.server.start()
|
||||
|
||||
def stop(self):
|
||||
if self.server:
|
||||
self.server.stop()
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({"status": "ok", "service": "http-api"}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
def _server_info_handler(self, request):
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({"status": "running", "plugins_loaded": True}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
|
||||
register_plugin_type("HttpApiPlugin", HttpApiPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return HttpApiPlugin()
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "http-api",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "HTTP API 服务 - 提供 RESTful API/路由功能/多语言支持/安全中间件",
|
||||
"type": "protocol"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8080,
|
||||
"ssl_enabled": false,
|
||||
"ssl_cert": "",
|
||||
"ssl_key": "",
|
||||
"cors_enabled": true,
|
||||
"rate_limit_enabled": true,
|
||||
"max_body_size": 10485760,
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"dependencies": ["i18n"],
|
||||
"permissions": ["lifecycle", "circuit-breaker"]
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
"""中间件链 - CORS/鉴权/日志/限流/CSRF/输入验证等"""
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
from .server import Request, Response
|
||||
from .rate_limiter import RateLimitMiddleware
|
||||
from .csrf_middleware import CsrfMiddleware
|
||||
from .input_validation import InputValidationMiddleware
|
||||
|
||||
|
||||
class Middleware:
|
||||
"""中间件基类"""
|
||||
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
|
||||
return next_fn()
|
||||
|
||||
|
||||
class CorsMiddleware(Middleware):
|
||||
"""CORS 中间件"""
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
config = get_config()
|
||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||
|
||||
req = ctx.get("request")
|
||||
origin = req.headers.get("Origin", "") if req else ""
|
||||
|
||||
# 如果没有配置允许的来源或来源为空,则不设置CORS头
|
||||
if not allowed_origins or not origin:
|
||||
return next_fn()
|
||||
|
||||
# 检查请求来源是否在允许列表中
|
||||
if origin in allowed_origins or "*" in allowed_origins:
|
||||
ctx["response_headers"] = {
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
}
|
||||
|
||||
return next_fn()
|
||||
|
||||
|
||||
class AuthMiddleware(Middleware):
|
||||
"""鉴权中间件 - Bearer Token 认证"""
|
||||
_public_paths = {"/health", "/favicon.ico", "/api/status", "/api/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
config = get_config()
|
||||
api_key = config.get("API_KEY")
|
||||
|
||||
if not api_key:
|
||||
return next_fn()
|
||||
|
||||
req = ctx.get("request")
|
||||
if req and req.path in self._public_paths:
|
||||
return next_fn()
|
||||
|
||||
if req and req.method == "OPTIONS":
|
||||
return next_fn()
|
||||
|
||||
auth_header = req.headers.get("Authorization", "") if req else ""
|
||||
token = auth_header.removeprefix("Bearer ").strip()
|
||||
|
||||
if token != api_key or not token:
|
||||
Log.warn("auth", f"鉴权失败: {req.method} {req.path}" if req else "鉴权失败")
|
||||
return Response(
|
||||
status=401,
|
||||
body=json.dumps({"error": "Unauthorized", "message": "需要有效的 API Key"}),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
return next_fn()
|
||||
|
||||
|
||||
class LoggerMiddleware(Middleware):
|
||||
"""日志中间件"""
|
||||
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
req = ctx.get("request")
|
||||
if req and req.path not in self._silent_paths:
|
||||
Log.info("http-api", f"{req.method} {req.path}")
|
||||
return next_fn()
|
||||
|
||||
|
||||
class RateLimitMiddleware(Middleware):
|
||||
"""限流中间件 - 防止DoS攻击"""
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("RATE_LIMIT_ENABLED", True)
|
||||
|
||||
# 不同端点的限流配置
|
||||
self.endpoint_limits = {
|
||||
"/api/dashboard/stats": {
|
||||
"max_requests": 10,
|
||||
"time_window": 60
|
||||
},
|
||||
"/api/pkg-manager/search": {
|
||||
"max_requests": 50,
|
||||
"time_window": 60
|
||||
}
|
||||
}
|
||||
|
||||
# 全局限流配置
|
||||
self.global_limit = {
|
||||
"max_requests": self.config.get("RATE_LIMIT_MAX_REQUESTS", 100),
|
||||
"time_window": self.config.get("RATE_LIMIT_TIME_WINDOW", 60)
|
||||
}
|
||||
|
||||
# 请求记录
|
||||
self.requests = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def _get_client_identifier(self, request: Request) -> str:
|
||||
"""获取客户端标识符"""
|
||||
# 优先使用IP地址
|
||||
ip = request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", ""))
|
||||
if not ip:
|
||||
ip = request.headers.get("Remote-Addr", "unknown")
|
||||
|
||||
# 如果有API Key,使用Key作为标识符(更精确)
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
return f"api_key:{auth_header[7:]}"
|
||||
|
||||
return f"ip:{ip}"
|
||||
|
||||
def _is_rate_limited(self, identifier: str, path: str) -> bool:
|
||||
"""检查是否被限流"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
now = time.time()
|
||||
limit_key = f"{identifier}:{path}"
|
||||
|
||||
# 获取端点特定的限制
|
||||
endpoint_limit = None
|
||||
for endpoint, config in self.endpoint_limits.items():
|
||||
if path.startswith(endpoint):
|
||||
endpoint_limit = config
|
||||
break
|
||||
|
||||
# 使用端点特定限制或全局限制
|
||||
limit = endpoint_limit or self.global_limit
|
||||
max_requests = limit["max_requests"]
|
||||
time_window = limit["time_window"]
|
||||
|
||||
with self.lock:
|
||||
# 清理过期的请求记录
|
||||
if limit_key not in self.requests:
|
||||
self.requests[limit_key] = deque()
|
||||
|
||||
request_times = self.requests[limit_key]
|
||||
while request_times and request_times[0] <= now - time_window:
|
||||
request_times.popleft()
|
||||
|
||||
# 检查是否超过限制
|
||||
if len(request_times) >= max_requests:
|
||||
return True
|
||||
|
||||
# 记录当前请求
|
||||
request_times.append(now)
|
||||
return False
|
||||
|
||||
def _create_rate_limit_response(self) -> Response:
|
||||
"""创建限流响应"""
|
||||
return Response(
|
||||
status=429,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": str(self.global_limit["time_window"]),
|
||||
"X-Rate-Limit-Limit": str(self.global_limit["max_requests"]),
|
||||
"X-Rate-Limit-Window": str(self.global_limit["time_window"]),
|
||||
},
|
||||
body='{"error": "Rate limit exceeded", "message": "请稍后再试"}'
|
||||
)
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
"""处理限流逻辑"""
|
||||
if not self.enabled:
|
||||
return next_fn()
|
||||
|
||||
request = ctx.get("request")
|
||||
if not request:
|
||||
return next_fn()
|
||||
|
||||
# 获取客户端标识符
|
||||
identifier = self._get_client_identifier(request)
|
||||
|
||||
# 检查是否被限流
|
||||
if self._is_rate_limited(identifier, request.path):
|
||||
return self._create_rate_limit_response()
|
||||
|
||||
return next_fn()
|
||||
|
||||
|
||||
class MiddlewareChain:
|
||||
"""中间件链"""
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[Middleware] = []
|
||||
self.add(CorsMiddleware())
|
||||
self.add(AuthMiddleware())
|
||||
self.add(LoggerMiddleware())
|
||||
self.add(RateLimitMiddleware())
|
||||
self.add(CsrfMiddleware())
|
||||
self.add(InputValidationMiddleware())
|
||||
|
||||
def add(self, middleware: Middleware):
|
||||
self.middlewares.append(middleware)
|
||||
|
||||
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
|
||||
idx = 0
|
||||
|
||||
def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return mw.process(ctx, next_fn)
|
||||
return None
|
||||
|
||||
resp = next_fn()
|
||||
response_headers = ctx.get("response_headers")
|
||||
if response_headers:
|
||||
ctx["_cors_headers"] = response_headers
|
||||
return resp
|
||||
@@ -1,35 +0,0 @@
|
||||
"""
|
||||
限流工具 - 令牌桶限流器
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict
|
||||
from collections import defaultdict, deque
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""令牌桶限流器"""
|
||||
|
||||
def __init__(self, max_requests: int = 100, time_window: int = 60):
|
||||
self.max_requests = max_requests
|
||||
self.time_window = time_window
|
||||
self.requests: Dict[str, deque] = defaultdict(deque)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def is_allowed(self, identifier: str) -> bool:
|
||||
"""检查是否允许请求"""
|
||||
with self.lock:
|
||||
now = time.time()
|
||||
request_times = self.requests[identifier]
|
||||
|
||||
# 清理过期的请求记录
|
||||
while request_times and request_times[0] <= now - self.time_window:
|
||||
request_times.popleft()
|
||||
|
||||
# 检查是否超过限制
|
||||
if len(request_times) >= self.max_requests:
|
||||
return False
|
||||
|
||||
# 记录当前请求
|
||||
request_times.append(now)
|
||||
return True
|
||||
@@ -1,41 +0,0 @@
|
||||
"""HTTP 路由 - 基于 oss/shared/router.py 的 BaseRouter"""
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
from oss.shared.router import BaseRouter, BaseRoute, match_path, extract_path_params
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class HttpRouter(BaseRouter):
|
||||
"""HTTP 路由"""
|
||||
|
||||
def add(self, method: str, path: str, handler: Callable):
|
||||
self.routes.append(BaseRoute(method, path, handler))
|
||||
|
||||
def handle(self, request: Request) -> Response:
|
||||
"""匹配路由并执行处理器"""
|
||||
for route in self.routes:
|
||||
if route.method == request.method and match_path(route.path, request.path):
|
||||
params = extract_path_params(route.path, request.path)
|
||||
try:
|
||||
result = route.handler(request, **params)
|
||||
if isinstance(result, Response):
|
||||
return result
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps(result) if not isinstance(result, str) else result,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status=500,
|
||||
body=json.dumps({"error": "Internal Server Error", "message": str(e)}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 404 - 无匹配路由
|
||||
return Response(
|
||||
status=404,
|
||||
body=json.dumps({"error": "Not Found", "message": f"路由未找到: {request.method} {request.path}"}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
@@ -1,123 +0,0 @@
|
||||
"""HTTP 服务器核心"""
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
from oss.config import get_config
|
||||
|
||||
|
||||
class Request:
|
||||
"""请求对象"""
|
||||
def __init__(self, method, path, headers, body):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
|
||||
|
||||
class Response:
|
||||
"""响应对象"""
|
||||
def __init__(self, status=200, headers=None, body=""):
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self.body = body
|
||||
|
||||
|
||||
class HttpServer:
|
||||
"""HTTP 服务器"""
|
||||
|
||||
def __init__(self, router, middleware, host=None, port=None):
|
||||
config = get_config()
|
||||
self.host = host or config.get("HOST", "127.0.0.1")
|
||||
self.port = port or config.get("HTTP_API_PORT", 8080)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self._server = None
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
"""启动服务器"""
|
||||
handler = self._create_handler()
|
||||
self._server = HTTPServer((self.host, self.port), handler)
|
||||
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
||||
self._thread.start()
|
||||
print(f"[http-api] 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self._server:
|
||||
self._server.shutdown()
|
||||
print("[http-api] 服务器已停止")
|
||||
|
||||
def _create_handler(self):
|
||||
"""创建请求处理器"""
|
||||
router = self.router
|
||||
middleware = self.middleware
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self._handle("GET")
|
||||
|
||||
def do_POST(self):
|
||||
self._handle("POST")
|
||||
|
||||
def do_PUT(self):
|
||||
self._handle("PUT")
|
||||
|
||||
def do_DELETE(self):
|
||||
self._handle("DELETE")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理 CORS 预检请求"""
|
||||
config = get_config()
|
||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||
origin = self.headers.get("Origin", "")
|
||||
|
||||
if origin in allowed_origins or "*" in allowed_origins:
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", origin if origin else "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
self.send_header("Access-Control-Allow-Credentials", "true")
|
||||
else:
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
def _handle(self, method):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length) if content_length else b""
|
||||
|
||||
req = Request(
|
||||
method=method,
|
||||
path=self.path,
|
||||
headers=dict(self.headers),
|
||||
body=body.decode("utf-8")
|
||||
)
|
||||
|
||||
# 执行中间件
|
||||
ctx = {"request": req, "response": None}
|
||||
result = middleware.run(ctx)
|
||||
if result:
|
||||
self._send_response(result)
|
||||
return
|
||||
|
||||
# 路由匹配
|
||||
resp = router.handle(req)
|
||||
self._send_response(resp)
|
||||
|
||||
def _send_response(self, resp: Response):
|
||||
try:
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.headers.items():
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
if isinstance(resp.body, str):
|
||||
self.wfile.write(resp.body.encode("utf-8"))
|
||||
else:
|
||||
self.wfile.write(resp.body)
|
||||
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
||||
pass # 忽略客户端断开
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
return Handler
|
||||
@@ -1,51 +0,0 @@
|
||||
# http-tcp HTTP TCP 服务
|
||||
|
||||
提供基于 TCP 的 HTTP 协议实现。
|
||||
|
||||
## 功能
|
||||
|
||||
- 原始 TCP HTTP 服务器
|
||||
- 路由匹配
|
||||
- 中间件链(日志/CORS)
|
||||
- 连接管理
|
||||
- 事件发布(通过 plugin-bridge)
|
||||
|
||||
## 使用
|
||||
|
||||
```python
|
||||
tcp = plugin_mgr.get("http-tcp")
|
||||
|
||||
# 注册路由
|
||||
tcp.router.get("/api/status", lambda req: {
|
||||
"status": 200,
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": '{"status": "ok"}'
|
||||
})
|
||||
|
||||
# 获取客户端
|
||||
clients = tcp.server.get_clients()
|
||||
```
|
||||
|
||||
## 事件
|
||||
|
||||
```python
|
||||
bridge = plugin_mgr.get("plugin-bridge")
|
||||
bus = bridge.event_bus
|
||||
|
||||
bus.on("tcp.connect", lambda e: print(f"连接: {e.client.id}"))
|
||||
bus.on("tcp.http.request", lambda e: print(f"请求: {e.context['request']['path']}"))
|
||||
bus.on("tcp.disconnect", lambda e: print(f"断开: {e.client.id}"))
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8082
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "Adt4Pa7dzXVC9LuotOb2hvUREP2sQyInReCfPRVnKLuD2IB+5Uk4BSCjt5EkUUcMiEwIYoefntc1Q0f4k/OL3F4WtKFrwb4G+WJZYuwSbYZ3l4wYtivMFTuP4PjIgz1/sWUfqHdd+jwOquM9a8+uiNaxiz+Ed9UmBCqiJXjbfiP5A5RlkUGO3evwuP51dhfo3BVU+YuVWzSWfVw8Ov9Wx1V0h7fEjPPYof1d9AP+yVnfLLfBeNL1T/VlpkogllRlcqOQm5w+s17sLhR6sQEBHHTsga7Nilh8/BMmXr3vFDrtPbPsOqVGzHvYOFFJf26geFgxowPJ5YxEL9FKp9NtOp0fsDsq6f74mES9nTg7v9uImL8zzYn774fpaIfbOL2CVqsCqzW+kYhNm7fsJD8SfmhwKR8tVEsYvqUiHqpzUwX/J7soD0jlN/ttUUCZREERRKIpumHNNxkcgLuTYsloeSrG935ZOSEt6QuWSg9+dlXgdi84UmE1TbU6Q6HKExopOJitYCUM1p21G5wcFgEn+o7zdkDUdCJEliG1QeqSHdhlo/QyLuH/7mZQOMdprHabggTUrmbrES78nT10XEFWjtUfKxuzQkWwozwYPx6cBdmO4OLYJ+C5u1hwgmVm6if6IbCPm0l/NGy8NUNjH0PxDdmPaUSdnvSLLwa6fwr5/h0=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775960645.9258935,
|
||||
"plugin_hash": "136d916944b4b1e37134b3b9807a8ea19fc9c4971c62d15cc11e019502de5617",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
class TcpEvent:
|
||||
type: str
|
||||
client: Any = None
|
||||
data: bytes = b""
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
EVENT_CONNECT = "tcp.connect"
|
||||
EVENT_DISCONNECT = "tcp.disconnect"
|
||||
EVENT_DATA = "tcp.data"
|
||||
EVENT_REQUEST = "tcp.http.request"
|
||||
EVENT_RESPONSE = "tcp.http.response"
|
||||
EVENT_ERROR = "tcp.error"
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
class HttpTcpPlugin:
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.router = TcpRouter()
|
||||
self.middleware = TcpMiddlewareChain()
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
self.server.start()
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "http-tcp",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "HTTP TCP 服务 - 基于 TCP 的 HTTP 协议实现/多语言支持",
|
||||
"type": "protocol"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8082,
|
||||
"ssl_enabled": false,
|
||||
"max_connections": 500,
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"dependencies": ["i18n"],
|
||||
"permissions": ["lifecycle"]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
class TcpMiddleware:
|
||||
def process(self, request: dict, next_fn: Callable) -> Optional[dict]:
|
||||
pass
|
||||
|
||||
|
||||
class TcpCorsMiddleware(TcpMiddleware):
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[TcpMiddleware] = []
|
||||
self.add(TcpLogMiddleware())
|
||||
self.add(TcpCorsMiddleware())
|
||||
|
||||
def add(self, middleware: TcpMiddleware):
|
||||
idx = 0
|
||||
|
||||
def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return mw.process(request, next_fn)
|
||||
return None
|
||||
|
||||
return next_fn()
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
class TcpRouter:
|
||||
def handle(self, request: dict) -> dict:
|
||||
pass
|
||||
@@ -1,116 +0,0 @@
|
||||
class TcpClient:
|
||||
def __init__(self, conn: socket.socket, address: tuple):
|
||||
self.conn = conn
|
||||
self.address = address
|
||||
self.id = f"{address[0]}:{address[1]}"
|
||||
|
||||
def send(self, data: bytes):
|
||||
self.conn.close()
|
||||
|
||||
|
||||
class TcpHttpServer:
|
||||
def __init__(self):
|
||||
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._server.bind((self.host, self.port))
|
||||
self._server.listen(128)
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
|
||||
self._thread.start()
|
||||
print(f"[http-tcp] 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def _accept_loop(self):
|
||||
buffer = b""
|
||||
try:
|
||||
while self._running:
|
||||
data = client.conn.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
|
||||
if b"\r\n\r\n" in buffer:
|
||||
header_end = buffer.find(b"\r\n\r\n")
|
||||
header_text = buffer[:header_end].decode("utf-8", errors="replace")
|
||||
|
||||
content_length = 0
|
||||
for line in header_text.split("\r\n")[1:]:
|
||||
if line.lower().startswith("content-length:"):
|
||||
content_length = int(line.split(":", 1)[1].strip())
|
||||
break
|
||||
|
||||
body_start_pos = header_end + 4
|
||||
body_received = len(buffer) - body_start_pos
|
||||
|
||||
if body_received < content_length:
|
||||
while body_received < content_length:
|
||||
remaining = content_length - body_received
|
||||
chunk = client.conn.recv(min(4096, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
buffer += chunk
|
||||
body_received += len(chunk)
|
||||
|
||||
request = self._parse_request(buffer)
|
||||
if request:
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(
|
||||
type=EVENT_REQUEST,
|
||||
client=client,
|
||||
context={"request": request}
|
||||
))
|
||||
|
||||
response = self.router.handle(request)
|
||||
|
||||
response_bytes = self._format_response(response)
|
||||
client.send(response_bytes)
|
||||
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(
|
||||
type=EVENT_RESPONSE,
|
||||
client=client,
|
||||
data=response_bytes
|
||||
))
|
||||
|
||||
buffer = b""
|
||||
|
||||
except ConnectionResetError:
|
||||
pass
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
except OSError as e:
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": f"OSError: {e}"}))
|
||||
except Exception as e:
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": f"{type(e).__name__}: {e}"}))
|
||||
finally:
|
||||
del self._clients[client.id]
|
||||
client.close()
|
||||
if self.event_bus:
|
||||
self.event_bus.emit(TcpEvent(type=EVENT_DISCONNECT, client=client))
|
||||
|
||||
def _parse_request(self, data: bytes) -> Optional[dict]:
|
||||
status = response.get("status", 200)
|
||||
headers = response.get("headers", {})
|
||||
body = response.get("body", "")
|
||||
|
||||
status_text = {200: "OK", 404: "Not Found", 500: "Internal Server Error"}.get(status, "OK")
|
||||
|
||||
response_lines = [
|
||||
f"HTTP/1.1 {status} {status_text}",
|
||||
]
|
||||
|
||||
if "Content-Type" not in headers:
|
||||
headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
headers["Content-Length"] = str(len(body))
|
||||
|
||||
for key, value in headers.items():
|
||||
response_lines.append(f"{key}: {value}")
|
||||
|
||||
response_lines.append("")
|
||||
response_lines.append(body)
|
||||
|
||||
return "\r\n".join(response_lines).encode("utf-8")
|
||||
|
||||
def stop(self):
|
||||
return list(self._clients.values())
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "N8pwPuJxnjP/hgMG4QLYQy7Z6e1P1KctYLJYoQniALDFT1qb11RDm1w4KUbzNIY82XM56B10zYF88dTQiGMrtbgoExE0gtUvmF3THvEd+aWhQ0m5/2war2w+j02BWH0TvJqxhb5nHCyhA4CknJANWp4wZr9EPjDseb+OhXC3GECKpChVrmM9/DWM6TtjlmGol14kq+jUnrS5EWNSa1hlsLzKIrS3Jf5fLaButDUr6YuQkATRKl6F41M8+JHJwVVw5D1fRSqCZ4xFWwN90Gtdd22JFSeB9iVE2Myb3UurPzTVvJ0B/JE9yxFDhA1B7PtuF/WeWlm060QRWdlwFfO9NjUJOeOGQstn34DUG2xL/q3yF66SjnHcHs67DqVq9lCQ961jQq0QveKunV4u8uBJd4IGH4MTq5W7Be8GDgSZcll5HLG3HBL+9XYf4mJzc7dh88Y0UV+dOabD2SJCwBmMxgzDx+Dx8RwWx7b9IYZvmXz6fxtXhqfV6AFq2oY/+4Xjwn4nq7VOCgx8PxLrUvmuacmCwlar/rXuvHT0YsN/XXmJK9o/3NYsNp/go8Vm0XW0btJ+FnQw4O4OKPvSSd+Ip+tk2rLi7CuZGi0WEVp2o23gUNLXoHkKFrtms02Et6zC9AFwP2gLF+NnaMWImup54owxgDos9s6l2ejTD653rYE=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775964953.002281,
|
||||
"plugin_hash": "55f90852ff6fbd82bc5a51ea4ebc2725f1316a7a5f9d423ee10a7e571aad339a",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
|
||||
class I18nEngine:
|
||||
|
||||
def __init__(self):
|
||||
self._translations: dict[str, dict[str, Any]] = {}
|
||||
self._current_locale: str = "zh-CN"
|
||||
self._fallback_locale: str = "en-US"
|
||||
self._supported_locales: list[str] = []
|
||||
self._locales_dir: str = ""
|
||||
|
||||
def load_locales(self, locales_dir: str, locales: list[str]):
|
||||
self._locales_dir = locales_dir
|
||||
self._supported_locales = locales
|
||||
locales_path = Path(locales_dir)
|
||||
|
||||
if not locales_path.exists():
|
||||
locales_path.mkdir(parents=True, exist_ok=True)
|
||||
return
|
||||
|
||||
for locale in locales:
|
||||
locale_file = locales_path / f"{locale}.json"
|
||||
if locale_file.exists():
|
||||
try:
|
||||
content = locale_file.read_text(encoding="utf-8")
|
||||
self._translations[locale] = json.loads(content)
|
||||
except (json.JSONDecodeError, Exception) as e:
|
||||
print(f"[i18n] load locale file failed {locale_file}: {e}")
|
||||
self._translations[locale] = {}
|
||||
|
||||
def get_locale(self) -> str:
|
||||
return self._current_locale
|
||||
|
||||
def set_locale(self, locale: str):
|
||||
self._current_locale = locale
|
||||
|
||||
def set_fallback(self, locale: str):
|
||||
self._fallback_locale = locale
|
||||
|
||||
def t(self, key: str, locale: str = None, **kwargs) -> str:
|
||||
target_locale = locale or self._current_locale
|
||||
|
||||
value = self._get_nested(key, self._translations.get(target_locale, {}))
|
||||
|
||||
if value is None and target_locale != self._fallback_locale:
|
||||
value = self._get_nested(key, self._translations.get(self._fallback_locale, {}))
|
||||
|
||||
if value is None:
|
||||
return key
|
||||
|
||||
return self._interpolate(value, kwargs)
|
||||
|
||||
def _get_nested(self, key: str, data: dict) -> Any:
|
||||
parts = key.split(".")
|
||||
current = data
|
||||
for part in parts:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(part)
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
def _interpolate(self, text: str, kwargs: dict) -> str:
|
||||
result = re.sub(r'\{\{(\w+)\}\}', lambda m: str(kwargs.get(m.group(1), m.group(0))), text)
|
||||
result = re.sub(r'\{(\w+)\}', lambda m: str(kwargs.get(m.group(1), m.group(0))), result)
|
||||
return result
|
||||
|
||||
def get_supported_locales(self) -> list[str]:
|
||||
return list(self._supported_locales)
|
||||
|
||||
def is_valid_locale(self, locale: str) -> bool:
|
||||
return locale in self._supported_locales
|
||||
|
||||
def detect_locale(self, accept_language: Optional[str] = None,
|
||||
query_lang: Optional[str] = None,
|
||||
cookie_lang: Optional[str] = None) -> str:
|
||||
if query_lang and self.is_valid_locale(query_lang):
|
||||
return query_lang
|
||||
|
||||
if cookie_lang and self.is_valid_locale(cookie_lang):
|
||||
return cookie_lang
|
||||
|
||||
if accept_language:
|
||||
languages = []
|
||||
for part in accept_language.split(","):
|
||||
part = part.strip()
|
||||
if ";q=" in part:
|
||||
lang, q = part.split(";q=")
|
||||
languages.append((lang.strip(), float(q)))
|
||||
else:
|
||||
languages.append((part, 1.0))
|
||||
|
||||
languages.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for lang, _ in languages:
|
||||
if self.is_valid_locale(lang):
|
||||
return lang
|
||||
for supported in self._supported_locales:
|
||||
if supported.startswith(lang + "-") or lang.startswith(supported.split("-")[0] + "-"):
|
||||
return supported
|
||||
|
||||
return self._current_locale
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"not_found": "Not Found",
|
||||
"forbidden": "Forbidden",
|
||||
"unauthorized": "Unauthorized",
|
||||
"server_error": "Internal Server Error",
|
||||
"bad_request": "Bad Request",
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"search": "Search",
|
||||
"loading": "Loading...",
|
||||
"no_data": "No Data",
|
||||
"confirm": "Confirm",
|
||||
"back": "Back"
|
||||
},
|
||||
"health": {
|
||||
"status": "Running",
|
||||
"service": "Service",
|
||||
"version": "Version",
|
||||
"uptime": "Uptime"
|
||||
},
|
||||
"api": {
|
||||
"welcome": "Welcome to NebulaShell API",
|
||||
"docs": "API Documentation",
|
||||
"rate_limit": "Rate limit exceeded, please try again later",
|
||||
"invalid_request": "Invalid request parameters",
|
||||
"missing_param": "Missing required parameter: {{param}}",
|
||||
"invalid_param": "Invalid parameter format: {{param}}"
|
||||
},
|
||||
"errors": {
|
||||
"400": "Bad Request",
|
||||
"401": "Please login first",
|
||||
"403": "You don't have permission to perform this action",
|
||||
"404": "The requested resource was not found",
|
||||
"500": "Internal server error, please try again later",
|
||||
"502": "Bad Gateway",
|
||||
"503": "Service temporarily unavailable, please try again later"
|
||||
},
|
||||
"plugin": {
|
||||
"i18n_name": "Internationalization",
|
||||
"i18n_desc": "Provides translation loading, language detection, and HTTP middleware",
|
||||
"locale_changed": "Locale changed to {{locale}}",
|
||||
"locale_not_supported": "Unsupported locale: {{locale}}"
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"not_found": "未找到",
|
||||
"forbidden": "禁止访问",
|
||||
"unauthorized": "未授权",
|
||||
"server_error": "服务器内部错误",
|
||||
"bad_request": "请求格式错误",
|
||||
"ok": "确定",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"create": "创建",
|
||||
"search": "搜索",
|
||||
"loading": "加载中...",
|
||||
"no_data": "暂无数据",
|
||||
"confirm": "确认",
|
||||
"back": "返回"
|
||||
},
|
||||
"health": {
|
||||
"status": "运行正常",
|
||||
"service": "服务",
|
||||
"version": "版本",
|
||||
"uptime": "运行时间"
|
||||
},
|
||||
"api": {
|
||||
"welcome": "欢迎使用 NebulaShell API",
|
||||
"docs": "API 文档",
|
||||
"rate_limit": "请求频率过高,请稍后重试",
|
||||
"invalid_request": "无效的请求参数",
|
||||
"missing_param": "缺少必需参数: {{param}}",
|
||||
"invalid_param": "参数格式错误: {{param}}"
|
||||
},
|
||||
"errors": {
|
||||
"400": "请求格式错误",
|
||||
"401": "请先登录",
|
||||
"403": "您没有权限执行此操作",
|
||||
"404": "请求的资源不存在",
|
||||
"500": "服务器内部错误,请稍后重试",
|
||||
"502": "网关错误",
|
||||
"503": "服务暂时不可用,请稍后重试"
|
||||
},
|
||||
"plugin": {
|
||||
"i18n_name": "国际化多语言支持",
|
||||
"i18n_desc": "提供翻译加载、语言检测和 HTTP 中间件功能",
|
||||
"locale_changed": "语言已切换为 {{locale}}",
|
||||
"locale_not_supported": "不支持的语言: {{locale}}"
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"success": "成功",
|
||||
"error": "錯誤",
|
||||
"not_found": "找不到",
|
||||
"forbidden": "禁止存取",
|
||||
"unauthorized": "未授權",
|
||||
"server_error": "伺服器內部錯誤",
|
||||
"bad_request": "請求格式錯誤",
|
||||
"ok": "確定",
|
||||
"cancel": "取消",
|
||||
"save": "儲存",
|
||||
"delete": "刪除",
|
||||
"edit": "編輯",
|
||||
"create": "建立",
|
||||
"search": "搜尋",
|
||||
"loading": "載入中...",
|
||||
"no_data": "暫無資料",
|
||||
"confirm": "確認",
|
||||
"back": "返回"
|
||||
},
|
||||
"health": {
|
||||
"status": "運作正常",
|
||||
"service": "服務",
|
||||
"version": "版本",
|
||||
"uptime": "運行時間"
|
||||
},
|
||||
"api": {
|
||||
"welcome": "歡迎使用 NebulaShell API",
|
||||
"docs": "API 文件",
|
||||
"rate_limit": "請求頻率過高,請稍後重試",
|
||||
"invalid_request": "無效的請求參數",
|
||||
"missing_param": "缺少必要參數: {{param}}",
|
||||
"invalid_param": "參數格式錯誤: {{param}}"
|
||||
},
|
||||
"errors": {
|
||||
"400": "請求格式錯誤",
|
||||
"401": "請先登入",
|
||||
"403": "您沒有權限執行此操作",
|
||||
"404": "請求的資源不存在",
|
||||
"500": "伺服器內部錯誤,請稍後重試",
|
||||
"502": "閘道錯誤",
|
||||
"503": "服務暫時不可用,請稍後重試"
|
||||
},
|
||||
"plugin": {
|
||||
"i18n_name": "國際化多語言支援",
|
||||
"i18n_desc": "提供翻譯載入、語言偵測和 HTTP 中介軟體功能",
|
||||
"locale_changed": "語言已切換為 {{locale}}",
|
||||
"locale_not_supported": "不支援的語言: {{locale}}"
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
|
||||
class I18nPlugin(Plugin):
|
||||
def __init__(self):
|
||||
self.engine = I18nEngine()
|
||||
self.middleware_handler = None
|
||||
self._http_api = None
|
||||
|
||||
def set_http_api(self, http_api):
|
||||
self._http_api = http_api
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
|
||||
加载语言文件并初始化中间件
|
||||
config = {}
|
||||
if deps:
|
||||
config = deps.get("config", {})
|
||||
|
||||
default_locale = config.get("default_locale", "zh-CN")
|
||||
fallback_locale = config.get("fallback_locale", "en-US")
|
||||
supported_locales = config.get("supported_locales", ["zh-CN", "en-US", "zh-TW"])
|
||||
locales_dir = config.get("locales_dir", "locales")
|
||||
|
||||
plugin_dir = Path(__file__).parent
|
||||
full_locales_dir = plugin_dir / locales_dir
|
||||
|
||||
self.engine.set_fallback(fallback_locale)
|
||||
|
||||
self.engine.load_locales(str(full_locales_dir), supported_locales)
|
||||
|
||||
self.engine.set_locale(default_locale)
|
||||
|
||||
self.middleware_handler = I18nMiddleware(self.engine, config)
|
||||
|
||||
Log.info("i18n", f"已加载语言: {', '.join(supported_locales)}")
|
||||
Log.info("i18n", f"默认语言: {default_locale}")
|
||||
|
||||
def start(self):
|
||||
http_api = self._http_api
|
||||
if not http_api:
|
||||
try:
|
||||
from store.NebulaShell.plugin_bridge.main import use
|
||||
http_api = use("http-api")
|
||||
self._http_api = http_api
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if http_api and hasattr(http_api, 'router'):
|
||||
http_api.router.get("/api/i18n/locales", self._locales_handler)
|
||||
http_api.router.get("/api/i18n/translate", self._translate_handler)
|
||||
http_api.router.post("/api/i18n/locale", self._change_locale_handler)
|
||||
Log.info("i18n", "API 路由已注册")
|
||||
|
||||
def stop(self):
|
||||
return self.engine is not None
|
||||
|
||||
def stats(self) -> dict:
|
||||
self._http_api = http_api
|
||||
|
||||
|
||||
def _locales_handler(self, request):
|
||||
# GET /api/i18n/translate?key=user.greeting&locale=en-US&name=World
|
||||
from oss.plugin.types import Response
|
||||
t = getattr(request, 't', self.engine.t)
|
||||
|
||||
query = request.path.split("?", 1)[-1] if "?" in request.path else ""
|
||||
params = {}
|
||||
for param in query.split("&"):
|
||||
if "=" in param:
|
||||
key, value = param.split("=", 1)
|
||||
params[key] = value
|
||||
|
||||
key = params.get("key", "")
|
||||
locale = params.get("locale", None)
|
||||
|
||||
if not key:
|
||||
return Response(
|
||||
status=400,
|
||||
body=json.dumps({"error": t("api.missing_param", param="key")}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
result = t(key, locale=locale, **params)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({
|
||||
"key": key,
|
||||
"locale": locale or self.engine.get_locale(),
|
||||
"text": result
|
||||
}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
def _change_locale_handler(self, request):
|
||||
from oss.plugin.types import Response
|
||||
t = getattr(request, 't', self.engine.t)
|
||||
|
||||
try:
|
||||
body = json.loads(request.body) if hasattr(request, 'body') and request.body else {}
|
||||
except json.JSONDecodeError:
|
||||
body = {}
|
||||
|
||||
new_locale = body.get("locale", "")
|
||||
|
||||
if not new_locale:
|
||||
return Response(
|
||||
status=400,
|
||||
body=json.dumps({"error": t("api.missing_param", param="locale")}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if not self.engine.is_valid_locale(new_locale):
|
||||
return Response(
|
||||
status=400,
|
||||
body=json.dumps({"error": t("plugin.locale_not_supported", locale=new_locale)}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
self.engine.set_locale(new_locale)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({"message": t("plugin.locale_changed", locale=new_locale)}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
|
||||
register_plugin_type("I18nPlugin", I18nPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return I18nPlugin()
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "i18n",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "国际化多语言支持 - 提供翻译加载/语言切换/HTTP中间件/WebUI集成",
|
||||
"type": "middleware"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"default_locale": "zh-CN",
|
||||
"fallback_locale": "en-US",
|
||||
"locales_dir": "locales",
|
||||
"supported_locales": ["zh-CN", "en-US", "zh-TW", "ja-JP", "ko-KR", "fr-FR", "de-DE", "es-ES"],
|
||||
"auto_detect": true,
|
||||
"cookie_name": "locale",
|
||||
"query_param": "lang",
|
||||
"header_name": "Accept-Language"
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": ["lifecycle", "http-api"]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
class I18nMiddleware:
|
||||
"""Auto-detect language and inject into request context.
|
||||
|
||||
Detection priority:
|
||||
1. URL query param ?lang=xx
|
||||
2. Cookie locale=xx
|
||||
3. Accept-Language header
|
||||
4. Default language
|
||||
"""
|
||||
|
||||
def __init__(self, engine, config: dict = None):
|
||||
self.engine = engine
|
||||
self.cookie_name = (config or {}).get("cookie_name", "locale")
|
||||
self.query_param = (config or {}).get("query_param", "lang")
|
||||
|
||||
def handle(self, request: dict, next_fn: Callable) -> Response:
|
||||
query_lang = self._parse_query_param(request.get("query", ""))
|
||||
|
||||
cookie_lang = self._parse_cookie(request.get("headers", {}))
|
||||
|
||||
accept_language = request.get("headers", {}).get("Accept-Language",
|
||||
request.get("headers", {}).get("accept-language", ""))
|
||||
|
||||
locale = self.engine.detect_locale(
|
||||
accept_language=accept_language if accept_language else None,
|
||||
query_lang=query_lang,
|
||||
cookie_lang=cookie_lang
|
||||
)
|
||||
|
||||
self.engine.set_locale(locale)
|
||||
|
||||
request["locale"] = locale
|
||||
request["t"] = self.engine.t
|
||||
response = next_fn()
|
||||
|
||||
if isinstance(response, Response):
|
||||
response.headers["Content-Language"] = locale
|
||||
|
||||
return response
|
||||
|
||||
def _parse_query_param(self, query_string: str) -> Optional[str]:
|
||||
cookie_header = headers.get("Cookie", headers.get("cookie", ""))
|
||||
if not cookie_header:
|
||||
return None
|
||||
|
||||
cookies = {}
|
||||
for cookie in cookie_header.split(";"):
|
||||
if "=" in cookie:
|
||||
key, value = cookie.split("=", 1)
|
||||
cookies[key.strip()] = value.strip()
|
||||
|
||||
return cookies.get(self.cookie_name)
|
||||
@@ -1,83 +0,0 @@
|
||||
# json-codec JSON 编解码器
|
||||
|
||||
提供插件间 JSON 数据的编码、解码和验证功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- **JSON 编码**: Python 对象 → JSON 字符串
|
||||
- **JSON 解码**: JSON 字符串 → Python 对象
|
||||
- **Schema 验证**: 验证 JSON 数据结构
|
||||
- **自定义类型**: 支持注册自定义类型编解码器
|
||||
|
||||
## 基本使用
|
||||
|
||||
```python
|
||||
codec = plugin_mgr.get("json-codec")
|
||||
|
||||
# 编码
|
||||
data = {"name": "test", "count": 42}
|
||||
json_str = codec.encode(data)
|
||||
# '{"name": "test", "count": 42}'
|
||||
|
||||
# 编码(格式化)
|
||||
json_pretty = codec.encode(data, pretty=True)
|
||||
# '{\n "name": "test",\n "count": 42\n}'
|
||||
|
||||
# 解码
|
||||
parsed = codec.decode(json_str)
|
||||
# {"name": "test", "count": 42}
|
||||
```
|
||||
|
||||
## HTTP 响应处理
|
||||
|
||||
```python
|
||||
# 在 http-api 插件中使用
|
||||
router.get("/api/users", lambda req: Response(
|
||||
status=200,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=codec.encode({"users": [...]})
|
||||
))
|
||||
```
|
||||
|
||||
## Schema 验证
|
||||
|
||||
```python
|
||||
# 注册 schema
|
||||
codec.register_schema("user", {
|
||||
"type": "object",
|
||||
"required": ["name", "email"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"email": {"type": "string"},
|
||||
"age": {"type": "number"}
|
||||
}
|
||||
})
|
||||
|
||||
# 验证数据
|
||||
user_data = {"name": "test", "email": "test@example.com"}
|
||||
is_valid = codec.validate(user_data, "user")
|
||||
```
|
||||
|
||||
## 自定义类型
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
# 注册自定义编码器
|
||||
codec.serializer.register_encoder(datetime, lambda dt: dt.isoformat())
|
||||
|
||||
# 使用
|
||||
data = {"created_at": datetime.now()}
|
||||
json_str = codec.encode(data)
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```python
|
||||
from oss.plugin.types import JsonCodecError
|
||||
|
||||
try:
|
||||
result = codec.decode("invalid json")
|
||||
except JsonCodecError as e:
|
||||
print(f"解码失败: {e}")
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "IQ8WAvKno6pRp71kIaxXPb7DzTajPeNOQ0FLZMVovufeyTRMbdSJ8z2zQPBPv9O2a1S9bucyZyhg54fNB2DdLfEnrAbmpepZ3CLrj3cn4KaLNGJjxGHYXWIsFXFvLaYIod/ZuFMYPlzDdwnHJwzHZnkGAmCLrJSR+XvuOqYu/xSZekD/nbMI0fj9VKjaH/S/vopEhq7IFioahVkiSokdYx5qkXYruOVAq3wCnk6O0uCNMfHiIaRhn5pEoQ+VOXcuKX5eOBEph8oXqb+ew1MB917Z1CpaLFuZTyp2Dy8OOmpXjBxfd5VYazH4ZvE9Q7VODHkRDVF2ApkPxTE1k490YvmNOHRamjcf1/mKyu7Myaemtz9oxvZFFiOMOaXBXGfe1wlnsbO832lURTpPu9WXQ6aoDEVp3TNuR/G/xYOXHcWhG1M4tIWW+1ZFcozkVw9cMYvwrVI9JEa89sueXQhJG9foW4nj0DJqmtXaXvcVHnpbFkIxcKFZ0rOMelJ7404XuDb07/sjliJuqCG9Gssmv7/DqNgIrcWUPg24U4UPWW2vWJaJq7HOrGrxFoOxpCT/G4A0WcAWVJrM5NojnfvBNswybSB2IIbspmPRDVtoHQ5a3YJqSLZdgugHh+MbGKlyDvPkQTkPLLE8nrP2F0LwWCq0cYeodE+zU0rZ6CHgAsc=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775964952.836965,
|
||||
"plugin_hash": "a7f7a20614a2e159e393a95c99b15a0a028724694bda3d089787cb41eceba7c4",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
class JsonCodecError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class JsonSerializer:
|
||||
def __init__(self):
|
||||
self._custom_encoders: dict = {}
|
||||
|
||||
def register_encoder(self, type_class: type, encoder: callable):
|
||||
self._custom_encoders[type_class] = encoder
|
||||
|
||||
def encode(self, data: Any, pretty: bool = False) -> str:
|
||||
return json.dumps(data, indent=2 if pretty else None)
|
||||
|
||||
def encode_bytes(self, data: Any, pretty: bool = False) -> bytes:
|
||||
return self.encode(data, pretty).encode("utf-8")
|
||||
|
||||
|
||||
class JsonDeserializer:
|
||||
def __init__(self):
|
||||
self._custom_decoders: dict = {}
|
||||
|
||||
def register_decoder(self, type_name: str, decoder: callable):
|
||||
self._custom_decoders[type_name] = decoder
|
||||
|
||||
def decode(self, text: str) -> Any:
|
||||
return json.loads(text)
|
||||
|
||||
def decode_bytes(self, data: bytes) -> Any:
|
||||
return self.decode(data.decode("utf-8"))
|
||||
|
||||
def decode_file(self, path: str) -> Any:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
class JsonValidator:
|
||||
def __init__(self):
|
||||
self._schemas: dict[str, dict] = {}
|
||||
|
||||
def register_schema(self, name: str, schema: dict):
|
||||
self._schemas[name] = schema
|
||||
|
||||
def validate(self, data: Any, schema_name: str) -> bool:
|
||||
if schema_name not in self._schemas:
|
||||
raise JsonCodecError(f"Unknown schema: {schema_name}")
|
||||
return self._check_schema(data, self._schemas[schema_name])
|
||||
|
||||
def _check_schema(self, data: Any, schema: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class JsonCodecPlugin:
|
||||
def __init__(self):
|
||||
self.serializer = JsonSerializer()
|
||||
self.deserializer = JsonDeserializer()
|
||||
self.validator = JsonValidator()
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
Log.info("json-codec", "JSON codec started")
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def encode(self, data: Any, pretty: bool = False) -> str:
|
||||
return self.serializer.encode(data, pretty)
|
||||
|
||||
def decode(self, text: str) -> Any:
|
||||
return self.deserializer.decode(text)
|
||||
|
||||
def validate(self, data: Any, schema_name: str) -> bool:
|
||||
return self.validator.validate(data, schema_name)
|
||||
|
||||
def register_schema(self, name: str, schema: dict):
|
||||
self.validator.register_schema(name, schema)
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "json-codec",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "JSON 编解码器 - 插件间 JSON 数据处理和验证",
|
||||
"type": "utility"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
# lifecycle 生命周期管理
|
||||
|
||||
管理插件的状态转换和钩子函数。
|
||||
|
||||
## 功能
|
||||
|
||||
- 状态机:`pending` → `running` → `stopped`
|
||||
- 支持状态转换验证
|
||||
- 提供生命周期钩子:
|
||||
- `before_start`
|
||||
- `after_start`
|
||||
- `before_stop`
|
||||
- `after_stop`
|
||||
- 支持扩展能力注入
|
||||
|
||||
## 状态转换
|
||||
|
||||
```
|
||||
pending → running → stopped
|
||||
↕
|
||||
(可重启)
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```python
|
||||
lc = lifecycle_plugin.create("my-plugin")
|
||||
lc.on("after_start", lambda: print("started"))
|
||||
lc.start()
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "nfM9Sj7VvV+L85zCvVcmIQY4qZ9FDdsk8MZf0LrO/ys1o6FCQ96Ixt1aB+2j6crOvXUBavnSRPk/LNaDs9r3eh49+Zfy5rEK+M0UyGjcawvEY4e/lO20UWy4iLw3JdSBo9nnFQC9eE8D6C9F2oM7YcqmT/sH0wYuyjCsa8tk6P/jy5/IdCwR6bo6AIQSpCnvyNcS9JPU19f603f0nl/siafXVozQxMS3wCLQ5EAoDz7atLevvQK7xAZCIIcCsre/sHTZ3a6O+BFlYYQ5w/giWlrl4aF7W7JJntOwpain39B0ktDRV96msbW744a1BFkcUw91W/2sRU7T9xplARjmhlRPGkdMTlj4PGyy394oaLwhx+uusx28C9+gWxp7pQZNo08LQ6dKmzog4fpUFD3EEyZBtPY2XYsILqKnGQVn3TLAaMmdoHdwoR6moLtR6BfD3ToRFV6vcNRTig8hTiS9GTzZeQtEtVkoSeAZphzxWfB7FunimDRpPxndDmvhervPUJ/uAVLcdorbDFB0RfvR3znUZrQkaw5YQZjP8mhUNyA6avyOBvGdt1i0bhZsc6CUMN4BrC+vOULiykyVGnk3B07XrMHNB8AGuqR8Ai/2DFglomfs/l07mz01HeUotRg3MezqF8aSkofpPTpRieeD9IeQgH03sOGdvXHDgDJB3Xc=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775960646.0212853,
|
||||
"plugin_hash": "a7d6c6e01a8dc5df868e34777233e33d984d01adedb8adcee24d6892600928a8",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
class LifecycleState:
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
STOPPED = "stopped"
|
||||
|
||||
|
||||
class LifecycleError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Lifecycle:
|
||||
VALID_TRANSITIONS = {
|
||||
LifecycleState.PENDING: [LifecycleState.RUNNING],
|
||||
LifecycleState.RUNNING: [LifecycleState.STOPPED],
|
||||
LifecycleState.STOPPED: [LifecycleState.RUNNING],
|
||||
}
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.state = LifecycleState.PENDING
|
||||
self._hooks: dict[str, list[Callable]] = {
|
||||
"before_start": [],
|
||||
"after_start": [],
|
||||
"before_stop": [],
|
||||
"after_stop": [],
|
||||
}
|
||||
self._extensions: dict[str, Any] = {}
|
||||
|
||||
def add_extension(self, name: str, extension: Any):
|
||||
self._extensions[name] = extension
|
||||
|
||||
def get_extension(self, name: str) -> Any:
|
||||
return self._extensions.get(name)
|
||||
|
||||
def start(self):
|
||||
for hook in self._hooks["before_start"]:
|
||||
hook(self)
|
||||
self.transition(LifecycleState.RUNNING)
|
||||
for hook in self._hooks["after_start"]:
|
||||
hook(self)
|
||||
|
||||
def stop(self):
|
||||
if self.state == LifecycleState.RUNNING:
|
||||
for hook in self._hooks["before_stop"]:
|
||||
hook(self)
|
||||
self.transition(LifecycleState.STOPPED)
|
||||
for hook in self._hooks["after_stop"]:
|
||||
hook(self)
|
||||
|
||||
def restart(self):
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def on(self, event: str, hook: Callable):
|
||||
if event in self._hooks:
|
||||
self._hooks[event].append(hook)
|
||||
|
||||
def transition(self, target_state: LifecycleState):
|
||||
valid = self.VALID_TRANSITIONS.get(self.state, [])
|
||||
if target_state in valid:
|
||||
self.state = target_state
|
||||
else:
|
||||
raise LifecycleError(f"Cannot transition from {self.state} to {target_state}")
|
||||
|
||||
|
||||
class LifecycleManager:
|
||||
def __init__(self):
|
||||
self.lifecycles: dict[str, Lifecycle] = {}
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
pass
|
||||
|
||||
def create(self, name: str) -> Lifecycle:
|
||||
lifecycle = Lifecycle(name)
|
||||
self.lifecycles[name] = lifecycle
|
||||
return lifecycle
|
||||
|
||||
def get(self, name: str) -> Optional[Lifecycle]:
|
||||
return self.lifecycles.get(name)
|
||||
|
||||
def start_all(self):
|
||||
for lc in self.lifecycles.values():
|
||||
try:
|
||||
lc.start()
|
||||
except LifecycleError:
|
||||
pass
|
||||
|
||||
def stop_all(self):
|
||||
for lc in self.lifecycles.values():
|
||||
try:
|
||||
lc.stop()
|
||||
except LifecycleError:
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "lifecycle",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "生命周期管理 - 管理插件的状态转换和钩子",
|
||||
"type": "core"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "iSAdml6TdNMXoZmB7zsRN6jYb3GL8ufdfxA+gHL58R1z7qpxc13fQidyo/syRaGv+J7zLV2/8/e8qSSGhbtWn2p08iH8vIax5zTe3zfl8wBlhxnCkEQztd1FlfkERgNWpRToiGu8GV8o0Fq+Yej6C+OaO6EL69DkRxL8Kp2Jf/2jdUOCprErLyKm506zotXjcKEr9heSLNCD0DKRaQv1GnqLJclp9fXirVvJHDS26ttNx1srNhvjTjsGofzn6qQpGuddLXKi7FWKDAByEBjqzQOmQ2iB4NOIG012J4HKO1q3BajNj11xfWL6PnSzvrwj8IJbJIrbCzTPeFK3F6gj3JtAcaI6iQLhJ7VjOCbFhlOOoIJx/5CA3j9x+/DLXgjAnV6fiD0Q8VCaLTkXGQPwGXo7xq8ExkRt48sHI9nFI0+8fj6nXB1ANDHPlvg86eyHKG61WUIZOHd/Ag9foCZtoDFnKXYBnVeNweHaHBsJWpBOvbFjPkYRpRxvRvVd8oe5qmxS0eS5RLmIIpHnOvoGKQV5CoGXPmKB5FNxDRUH4llz9W4FpxtRaYoFFoYatT9Kvr+WPSok13XS1uMBybT2nc+nEZ/XR7LsNxajfZsyEjXwQbL8DsI9LXPW9gt10F6P/9ByWaTCD/4H8flwDFI4iqw/iVENip8vnilTQpowuOY=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775969593.8644652,
|
||||
"plugin_hash": "b38f028d1629d878dcfc32ac28747d5cea8e93ad832009b88cb3b69934fb3fa5",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"logSyncInterval": {
|
||||
"type": "number",
|
||||
"name": "日志同步间隔",
|
||||
"description": "日志自动同步的时间间隔(秒)",
|
||||
"default": 2,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"order": 1
|
||||
},
|
||||
"sshPort": {
|
||||
"type": "number",
|
||||
"name": "SSH 端口",
|
||||
"description": "SSH 连接的默认端口",
|
||||
"default": 8022,
|
||||
"min": 1,
|
||||
"max": 65535,
|
||||
"order": 2
|
||||
},
|
||||
"autoInstallSSH": {
|
||||
"type": "boolean",
|
||||
"name": "自动安装 SSH",
|
||||
"description": "连接时自动检测并安装 SSH 服务",
|
||||
"default": true,
|
||||
"order": 3
|
||||
},
|
||||
"maxLogLines": {
|
||||
"type": "number",
|
||||
"name": "最大日志行数",
|
||||
"description": "日志界面最多显示的日志行数",
|
||||
"default": 1000,
|
||||
"min": 100,
|
||||
"max": 10000,
|
||||
"order": 4
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
class LogTerminalPlugin:
|
||||
def __init__(self):
|
||||
self.webui = None
|
||||
self.http_api = None
|
||||
self.views_dir = os.path.join(os.path.dirname(__file__), 'views')
|
||||
self._log_buffer = []
|
||||
self._log_lock = threading.Lock()
|
||||
self._ssh_sessions = {}
|
||||
self._session_counter = 0
|
||||
self._log_sync_thread = None
|
||||
self._running = False
|
||||
|
||||
def meta(self):
|
||||
from oss.plugin.types import Metadata, PluginConfig, Manifest
|
||||
return Manifest(
|
||||
metadata=Metadata(
|
||||
name="log-terminal",
|
||||
version="1.0.0",
|
||||
author="NebulaShell",
|
||||
description="日志查看器与 SSH 终端"
|
||||
),
|
||||
config=PluginConfig(enabled=True, args={}),
|
||||
dependencies=["http-api", "webui"]
|
||||
)
|
||||
|
||||
def set_webui(self, webui):
|
||||
self.webui = webui
|
||||
|
||||
def set_http_api(self, http_api):
|
||||
self.http_api = http_api
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
if not self.webui or not self.http_api:
|
||||
try:
|
||||
from store.NebulaShell.plugin_bridge.main import use
|
||||
if not self.webui: self.webui = use("webui")
|
||||
if not self.http_api: self.http_api = use("http-api")
|
||||
except Exception:
|
||||
pass
|
||||
if self.webui:
|
||||
Log.info("log-terminal", "已获取 WebUI 引用")
|
||||
|
||||
self.webui.register_page(
|
||||
path='/logs',
|
||||
content_provider=self._render_logs,
|
||||
nav_item={'icon': 'ri-file-list-3-line', 'text': '日志'}
|
||||
)
|
||||
|
||||
self.webui.register_page(
|
||||
path='/terminal',
|
||||
content_provider=self._render_terminal,
|
||||
nav_item={'icon': 'ri-terminal-box-line', 'text': '终端'}
|
||||
)
|
||||
|
||||
Log.ok("log-terminal", "已注册日志与终端页面到 WebUI 导航")
|
||||
else:
|
||||
Log.warn("log-terminal", "警告: 未找到 WebUI 依赖")
|
||||
|
||||
if self.http_api and self.http_api.router:
|
||||
self.http_api.router.get("/api/logs/get", self._handle_get_logs)
|
||||
self.http_api.router.post("/api/terminal/connect", self._handle_connect_ssh)
|
||||
self.http_api.router.post("/api/terminal/send", self._handle_send_command)
|
||||
self.http_api.router.post("/api/terminal/disconnect", self._handle_disconnect_ssh)
|
||||
self.http_api.router.get("/api/terminal/sessions", self._handle_list_sessions)
|
||||
Log.ok("log-terminal", "已注册 API 路由")
|
||||
else:
|
||||
Log.warn("log-terminal", "警告: 未找到 http-api 依赖")
|
||||
|
||||
def start(self):
|
||||
Log.info("log-terminal", "日志与终端插件启动中...")
|
||||
self._running = True
|
||||
|
||||
self._log_sync_thread = threading.Thread(target=self._log_sync_worker, daemon=True)
|
||||
self._log_sync_thread.start()
|
||||
|
||||
self.add_log_entry("info", "log-terminal", "日志与终端插件已启动")
|
||||
self.add_log_entry("tip", "log-terminal", "日志查看: /logs | SSH 终端: /terminal")
|
||||
|
||||
self._hook_system_log()
|
||||
|
||||
Log.ok("log-terminal", "日志与终端插件已启动")
|
||||
|
||||
def _hook_system_log(self):
|
||||
try:
|
||||
log_files = [
|
||||
'/var/log/syslog',
|
||||
'/var/log/messages',
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'system.log'),
|
||||
]
|
||||
|
||||
last_positions = {}
|
||||
|
||||
while self._running:
|
||||
for log_file in log_files:
|
||||
if os.path.exists(log_file) and os.path.isfile(log_file):
|
||||
try:
|
||||
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
if log_file not in last_positions:
|
||||
f.seek(0, 2)
|
||||
last_positions[log_file] = f.tell()
|
||||
else:
|
||||
f.seek(last_positions[log_file])
|
||||
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
last_positions[log_file] = f.tell()
|
||||
for line in lines[-50:]:
|
||||
line = line.strip()
|
||||
if line:
|
||||
self.add_log_entry("info", "system", line)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
Log.error("log-terminal", f"日志同步线程异常: {type(e).__name__}: {e}")
|
||||
|
||||
def add_log_entry(self, level: str, tag: str, message: str):
|
||||
with self._log_lock:
|
||||
return self._log_buffer[-limit:]
|
||||
|
||||
def _check_ssh_installed(self):
|
||||
try:
|
||||
Log.info("log-terminal", "正在安装 SSH 服务...")
|
||||
for pkg_manager in ['apt-get', 'yum', 'dnf', 'pacman']:
|
||||
result = subprocess.run(['which', pkg_manager], capture_output=True, timeout=3)
|
||||
if result.returncode == 0:
|
||||
if pkg_manager == 'apt-get':
|
||||
subprocess.run([pkg_manager, 'update'], capture_output=True, timeout=30)
|
||||
result = subprocess.run(
|
||||
[pkg_manager, 'install', '-y', 'openssh-server'],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
elif pkg_manager in ['yum', 'dnf']:
|
||||
result = subprocess.run(
|
||||
[pkg_manager, 'install', '-y', 'openssh-server'],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
elif pkg_manager == 'pacman':
|
||||
result = subprocess.run(
|
||||
[pkg_manager, '-S', '--noconfirm', 'openssh'],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
Log.ok("log-terminal", "SSH 服务安装成功")
|
||||
return True
|
||||
else:
|
||||
Log.error("log-terminal", f"SSH 服务安装失败: {result.stderr}")
|
||||
return False
|
||||
|
||||
Log.error("log-terminal", "未找到支持的包管理器")
|
||||
return False
|
||||
except Exception as e:
|
||||
Log.error("log-terminal", f"安装 SSH 服务时出错: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
def _start_ssh_server(self, port=8022):
|
||||
try:
|
||||
body = json.loads(request.body)
|
||||
port = body.get('port', 8022)
|
||||
auto_install = body.get('auto_install', True)
|
||||
|
||||
if not self._check_ssh_installed():
|
||||
if auto_install:
|
||||
if not self._install_ssh():
|
||||
return Response(
|
||||
status=500,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': 'SSH 安装失败'})
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
status=400,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': 'SSH 未安装,请先安装 SSH 服务'})
|
||||
)
|
||||
|
||||
if not self._start_ssh_server(port):
|
||||
return Response(
|
||||
status=500,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': 'SSH 服务器启动失败'})
|
||||
)
|
||||
|
||||
self._session_counter += 1
|
||||
session_id = self._session_counter
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
['script', '-q', '-c', '/bin/bash', '/dev/null'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
self._ssh_sessions[session_id] = {
|
||||
'process': process,
|
||||
'created_at': time.time(),
|
||||
'port': port
|
||||
}
|
||||
|
||||
Log.info("log-terminal", f"SSH 终端会话 {session_id} 已创建")
|
||||
return Response(
|
||||
status=200,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({
|
||||
'success': True,
|
||||
'session_id': session_id,
|
||||
'message': 'SSH 终端已连接'
|
||||
})
|
||||
)
|
||||
except Exception as e:
|
||||
Log.error("log-terminal", f"创建终端会话失败: {type(e).__name__}: {e}")
|
||||
return Response(
|
||||
status=500,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': str(e)})
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
Log.error("log-terminal", f"SSH 连接请求异常: {type(e).__name__}: {e}")
|
||||
return Response(
|
||||
status=500,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': str(e)})
|
||||
)
|
||||
|
||||
def _handle_send_command(self, request):
|
||||
try:
|
||||
body = json.loads(request.body)
|
||||
session_id = body.get('session_id')
|
||||
|
||||
if session_id in self._ssh_sessions:
|
||||
session = self._ssh_sessions[session_id]
|
||||
try:
|
||||
session['process'].terminate()
|
||||
except Exception as e:
|
||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
||||
pass
|
||||
del self._ssh_sessions[session_id]
|
||||
Log.info("log-terminal", f"SSH 终端会话 {session_id} 已断开")
|
||||
return Response(
|
||||
status=200,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': True, 'message': '已断开连接'})
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
status=400,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': '会话不存在'})
|
||||
)
|
||||
except Exception as e:
|
||||
Log.error("log-terminal", f"断开连接时出错: {type(e).__name__}: {e}")
|
||||
return Response(
|
||||
status=500,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': str(e)})
|
||||
)
|
||||
|
||||
def _handle_list_sessions(self, request):
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
parsed = urlparse(request.path)
|
||||
params = parse_qs(parsed.query)
|
||||
limit = int(params.get('limit', [100])[0])
|
||||
source = params.get('source', ['buffer'])[0]
|
||||
logs = []
|
||||
|
||||
if source == 'buffer':
|
||||
logs = self._get_logs(limit)
|
||||
else:
|
||||
logs = self._read_system_logs(limit)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': True, 'logs': logs})
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status=500,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({'success': False, 'error': str(e)})
|
||||
)
|
||||
|
||||
def _read_system_logs(self, limit=100):
|
||||
try:
|
||||
logs = self._get_logs(limit=100)
|
||||
log_rows = ""
|
||||
for log in logs:
|
||||
level_class = {
|
||||
'info': 'log-info',
|
||||
'warn': 'log-warn',
|
||||
'error': 'log-error',
|
||||
'ok': 'log-ok',
|
||||
'tip': 'log-tip'
|
||||
}.get(log['level'], 'log-info')
|
||||
log_rows += f"<tr class='{level_class}'><td>{log['timestamp']}</td><td>{log['tag']}</td><td>{log['message']}</td></tr>"
|
||||
|
||||
html = f"""<html><body><table>{log_rows}</table></body></html>"""
|
||||
return html
|
||||
except Exception as e:
|
||||
return f"<p>日志视图渲染出错:{e}</p>"
|
||||
|
||||
def _render_terminal(self) -> str:
|
||||
html = """<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SSH 终端</title>
|
||||
<link rel="stylesheet" href="/assets/remixicon.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; }
|
||||
.container { max-width: 1400px; margin: 0 auto; width: 100%; flex: 1; display: flex; flex-direction: column; }
|
||||
.card { background: #16213e; border-radius: 10px; padding: 20px; margin-bottom: 20px; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.card-title { font-size: 18px; font-weight: 600; color: #e94560; }
|
||||
.btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
||||
.btn-primary { background: #0f3460; color: white; }
|
||||
.btn-danger { background: #e94560; color: white; }
|
||||
.terminal-container { flex: 1; background: #0a0a1a; border-radius: 6px; padding: 15px; }
|
||||
.terminal-output { flex: 1; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; color: #00ff00; }
|
||||
.terminal-input { display: flex; margin-top: 10px; }
|
||||
.terminal-input input { flex: 1; background: #0a0a1a; color: #00ff00; border: 1px solid #333; padding: 8px; }
|
||||
.status-bar { display: flex; justify-content: space-between; padding: 10px; background: #16213e; }
|
||||
.status-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.status-connected { background: #00ff00; }
|
||||
.status-disconnected { background: #ff0000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title"> SSH 终端</h2>
|
||||
<div>
|
||||
<button class="btn btn-primary" id="connectBtn" onclick="connectTerminal()">连接</button>
|
||||
<button class="btn btn-danger" id="disconnectBtn" onclick="disconnectTerminal()" style="display:none;">断开</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<span class="status-dot status-disconnected" id="statusDot"></span>
|
||||
<span id="statusText">未连接</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>会话 ID: <strong id="sessionId">-</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-container">
|
||||
<div class="terminal-output" id="terminalOutput">欢迎使用 SSH 终端!点击"连接"按钮开始...</div>
|
||||
<div class="terminal-input">
|
||||
<input type="text" id="commandInput" placeholder="输入命令..." disabled onkeypress="handleKeyPress(event)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let sessionId = null;
|
||||
const output = document.getElementById('terminalOutput');
|
||||
const input = document.getElementById('commandInput');
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
const sessionIdEl = document.getElementById('sessionId');
|
||||
|
||||
function connectTerminal() {
|
||||
output.textContent = '正在连接...';
|
||||
fetch('/api/terminal/connect', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({port: 8022, auto_install: true})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
sessionId = data.session_id;
|
||||
sessionIdEl.textContent = sessionId;
|
||||
statusDot.className = 'status-dot status-connected';
|
||||
statusText.textContent = '已连接';
|
||||
input.disabled = false;
|
||||
connectBtn.style.display = 'none';
|
||||
disconnectBtn.style.display = 'inline-block';
|
||||
output.textContent = 'SSH 终端已连接。输入命令开始使用...';
|
||||
input.focus();
|
||||
} else {
|
||||
output.textContent = '连接失败:' + data.error;
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
output.textContent = '连接错误:' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectTerminal() {
|
||||
if (!sessionId) return;
|
||||
fetch('/api/terminal/disconnect', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({session_id: sessionId})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
sessionId = null;
|
||||
sessionIdEl.textContent = '-';
|
||||
statusDot.className = 'status-dot status-disconnected';
|
||||
statusText.textContent = '未连接';
|
||||
input.disabled = true;
|
||||
connectBtn.style.display = 'inline-block';
|
||||
disconnectBtn.style.display = 'none';
|
||||
output.textContent += '会话已断开。';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendCommand(cmd) {
|
||||
if (!sessionId) return;
|
||||
fetch('/api/terminal/send', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({session_id: sessionId, command: cmd})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
output.textContent += '$ ' + cmd + '\\n' + data.output;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
} else {
|
||||
output.textContent += '命令执行失败:' + data.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyPress(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendCommand(input.value);
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return html
|
||||
|
||||
register_plugin_type("LogTerminalPlugin", LogTerminalPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return LogTerminalPlugin()
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "log-terminal",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "日志查看器与 SSH 终端",
|
||||
"type": "webui-extension"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": ["http-api", "webui"],
|
||||
"permissions": ["*"]
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
# Node.js Adapter Plugin for NebulaShell
|
||||
|
||||
## Overview
|
||||
|
||||
The `@NebulaShell/nodejs-adapter` plugin provides Node.js and npm capabilities to other NebulaShell plugins. It enables any plugin to run Node.js projects located in their `/pkg` directory with isolated dependencies.
|
||||
|
||||
## Features
|
||||
|
||||
- **Node.js Runtime**: Execute Node.js scripts and applications
|
||||
- **npm Package Manager**: Install and manage npm packages
|
||||
- **Dependency Isolation**: Each plugin gets its own isolated `node_modules` directory
|
||||
- **Script Execution**: Run npm scripts or direct Node.js files
|
||||
- **Project Initialization**: Automatically create package.json and basic project structure
|
||||
|
||||
## Installation
|
||||
|
||||
The plugin is included in the NebulaShell store at:
|
||||
```
|
||||
store/NebulaShell/nodejs-adapter/
|
||||
```
|
||||
|
||||
It will be automatically loaded when the NebulaShell server starts.
|
||||
|
||||
## Usage
|
||||
|
||||
### For Plugin Developers
|
||||
|
||||
To use the Node.js adapter in your plugin, specify it in your plugin's manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@NebulaShell/my-nodejs-plugin",
|
||||
"version": "1.0.0",
|
||||
"runtime": {
|
||||
"type": "nodejs",
|
||||
"entry_point": "pkg/index.js",
|
||||
"adapter": "@NebulaShell/nodejs-adapter"
|
||||
},
|
||||
"dependencies": {
|
||||
"nodejs-adapter": "^1.2.0"
|
||||
},
|
||||
"nodejs": {
|
||||
"packages": ["express", "lodash"],
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"build": "webpack --mode production"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── manifest.json
|
||||
├── main.py (optional Python entry point)
|
||||
└── pkg/
|
||||
├── package.json
|
||||
├── index.js
|
||||
└── node_modules/ (auto-generated)
|
||||
```
|
||||
|
||||
### API Methods
|
||||
|
||||
The adapter provides the following methods that can be called by other plugins:
|
||||
|
||||
#### `check_versions()`
|
||||
Check Node.js and npm versions installed on the system.
|
||||
|
||||
```python
|
||||
adapter = get_plugin('nodejs-adapter')
|
||||
versions = adapter.check_versions()
|
||||
# Returns: {'node': 'v20.19.5', 'npm': '10.8.2', 'status': 'ok'}
|
||||
```
|
||||
|
||||
#### `install(plugin_id, packages, pkg_dir=None, is_dev=False)`
|
||||
Install npm packages to a plugin-specific directory.
|
||||
|
||||
```python
|
||||
result = adapter.install(
|
||||
plugin_id='my-plugin',
|
||||
packages=['express', 'lodash@4.17.21'],
|
||||
is_dev=False
|
||||
)
|
||||
# Returns: {'status': 'success', 'target_dir': '/path/to/dir', ...}
|
||||
```
|
||||
|
||||
#### `run(plugin_id, script, pkg_dir=None, args=None, env=None)`
|
||||
Execute a Node.js script or npm command.
|
||||
|
||||
```python
|
||||
# Run npm script
|
||||
result = adapter.run(
|
||||
plugin_id='my-plugin',
|
||||
script='start' # runs 'npm run start'
|
||||
)
|
||||
|
||||
# Run direct Node.js file
|
||||
result = adapter.run(
|
||||
plugin_id='my-plugin',
|
||||
script='pkg/index.js', # runs 'node pkg/index.js'
|
||||
args=['--port', '3000']
|
||||
)
|
||||
```
|
||||
|
||||
#### `list_packages(plugin_id, pkg_dir=None)`
|
||||
List installed packages in a plugin directory.
|
||||
|
||||
```python
|
||||
packages = adapter.list_packages(plugin_id='my-plugin')
|
||||
# Returns: {'status': 'success', 'packages': {...}}
|
||||
```
|
||||
|
||||
#### `init_project(plugin_id, pkg_dir=None, package_name=None, version='1.0.0')`
|
||||
Initialize a new Node.js project.
|
||||
|
||||
```python
|
||||
result = adapter.init_project(
|
||||
plugin_id='my-plugin',
|
||||
package_name='my-awesome-plugin'
|
||||
)
|
||||
# Creates package.json and index.js in the plugin directory
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The adapter can be configured via environment variables or plugin config:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"node_path": "/usr/bin/node",
|
||||
"npm_path": "/usr/bin/npm",
|
||||
"default_registry": "https://registry.npmjs.org",
|
||||
"cache_dir": "~/.nebulashell/nodejs-cache"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `NODEJS_ADAPTER_NODE_PATH`: Path to Node.js binary
|
||||
- `NODEJS_ADAPTER_NPM_PATH`: Path to npm binary
|
||||
- `NODEJS_ADAPTER_REGISTRY`: Custom npm registry URL
|
||||
- `NODEJS_ADAPTER_CACHE_DIR`: Directory for cached packages
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Express Server Plugin
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@NebulaShell/express-server",
|
||||
"version": "1.0.0",
|
||||
"runtime": {
|
||||
"type": "nodejs",
|
||||
"entry_point": "pkg/server.js",
|
||||
"adapter": "@NebulaShell/nodejs-adapter"
|
||||
},
|
||||
"nodejs": {
|
||||
"packages": ["express"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**pkg/server.js**:
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'Hello from NebulaShell!' });
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Example 2: Build Tool Plugin
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@NebulaShell/webpack-builder",
|
||||
"version": "1.0.0",
|
||||
"runtime": {
|
||||
"type": "nodejs",
|
||||
"adapter": "@NebulaShell/nodejs-adapter"
|
||||
},
|
||||
"nodejs": {
|
||||
"packages": ["webpack", "webpack-cli"],
|
||||
"scripts": {
|
||||
"build": "webpack --mode production"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Isolation
|
||||
|
||||
Each plugin gets its own isolated `node_modules` directory:
|
||||
|
||||
- Default location: `~/.nebulashell/nodejs-cache/{plugin_id}/`
|
||||
- Custom location: Specify `pkg_dir` parameter in API calls
|
||||
- No conflicts between different plugins' dependencies
|
||||
|
||||
## Error Handling
|
||||
|
||||
All adapter methods return a status object:
|
||||
|
||||
```python
|
||||
result = adapter.install(plugin_id='test', packages=['invalid-package-name-xyz'])
|
||||
if result['status'] == 'error':
|
||||
print(f"Installation failed: {result['error']}")
|
||||
else:
|
||||
print(f"Success! Packages installed to: {result['target_dir']}")
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Test the adapter directly:
|
||||
|
||||
```bash
|
||||
cd /workspace/store/NebulaShell/nodejs-adapter
|
||||
python main.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Node.js Adapter Plugin for NebulaShell
|
||||
==================================================
|
||||
|
||||
Node.js Version: v20.19.5
|
||||
npm Version: 10.8.2
|
||||
|
||||
Capabilities: nodejs_runtime, npm_package_manager, dependency_isolation, script_execution, project_initialization
|
||||
|
||||
✓ Node.js Adapter initialized successfully!
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Node.js or npm not found
|
||||
|
||||
Ensure Node.js and npm are installed on your system:
|
||||
|
||||
```bash
|
||||
# Check installation
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
# Install if needed (Ubuntu/Debian)
|
||||
apt update && apt install -y nodejs npm
|
||||
|
||||
# Install if needed (macOS)
|
||||
brew install node
|
||||
```
|
||||
|
||||
### Permission errors
|
||||
|
||||
If you encounter permission errors during package installation:
|
||||
|
||||
```bash
|
||||
# Ensure cache directory is writable
|
||||
mkdir -p ~/.nebulashell/nodejs-cache
|
||||
chmod 755 ~/.nebulashell/nodejs-cache
|
||||
```
|
||||
|
||||
### Timeout during installation
|
||||
|
||||
For large packages or slow networks, increase the timeout in the adapter configuration.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
|
||||
@@ -1,402 +0,0 @@
|
||||
"""
|
||||
Node.js Adapter Plugin for NebulaShell
|
||||
|
||||
This plugin provides Node.js and npm capabilities to other plugins.
|
||||
Other plugins can specify this adapter in their manifest to run Node.js projects
|
||||
located in their /pkg directory with isolated dependencies.
|
||||
|
||||
Features:
|
||||
- Install npm packages to plugin-specific directories
|
||||
- Execute Node.js scripts and npm commands
|
||||
- Check Node.js and npm versions
|
||||
- List installed packages
|
||||
- Dependency isolation per plugin
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
|
||||
class NodeJSAdapter:
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
self.config = config or {}
|
||||
self.node_path = self.config.get('node_path', '/usr/bin/node')
|
||||
self.npm_path = self.config.get('npm_path', '/usr/bin/npm')
|
||||
self.default_registry = self.config.get('default_registry', 'https://registry.npmjs.org')
|
||||
self.cache_dir = Path(self.config.get('cache_dir', '~/.nebulashell/nodejs-cache')).expanduser()
|
||||
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._validate_runtime()
|
||||
|
||||
def _validate_runtime(self) -> bool:
|
||||
try:
|
||||
node_result = subprocess.run(
|
||||
[self.node_path, '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
npm_result = subprocess.run(
|
||||
[self.npm_path, '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
return {
|
||||
'node': node_result.stdout.strip(),
|
||||
'npm': npm_result.stdout.strip(),
|
||||
'status': 'ok'
|
||||
}
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return {
|
||||
'node': 'unknown',
|
||||
'npm': 'unknown',
|
||||
'status': 'error',
|
||||
'error': f'Timeout: {str(e)}'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'node': 'unknown',
|
||||
'npm': 'unknown',
|
||||
'status': 'error',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def install(self, plugin_id: str, packages: List[str],
|
||||
pkg_dir: Optional[Path] = None,
|
||||
is_dev: bool = False) -> Dict[str, Any]:
|
||||
"""Install npm packages to a plugin-specific directory.
|
||||
|
||||
Args:
|
||||
plugin_id: Unique identifier for the plugin
|
||||
packages: List of npm packages to install (e.g., ['express', 'lodash@4.17.21'])
|
||||
pkg_dir: Optional custom package directory (defaults to plugin storage dir)
|
||||
is_dev: Whether to install as dev dependencies
|
||||
|
||||
Returns:
|
||||
Dict with installation result
|
||||
"""
|
||||
try:
|
||||
if pkg_dir is None:
|
||||
target_dir = self.cache_dir / plugin_id
|
||||
else:
|
||||
target_dir = Path(pkg_dir)
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cmd = [self.npm_path, 'install']
|
||||
if is_dev:
|
||||
cmd.append('--save-dev')
|
||||
else:
|
||||
cmd.append('--save')
|
||||
|
||||
if self.default_registry:
|
||||
cmd.extend(['--registry', self.default_registry])
|
||||
|
||||
cmd.extend(packages)
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(target_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return {
|
||||
'status': 'success',
|
||||
'plugin_id': plugin_id,
|
||||
'packages': packages,
|
||||
'target_dir': str(target_dir),
|
||||
'output': result.stdout,
|
||||
'is_dev': is_dev
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'packages': packages,
|
||||
'target_dir': str(target_dir),
|
||||
'error': result.stderr,
|
||||
'output': result.stdout
|
||||
}
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'packages': packages,
|
||||
'error': f'Installation timeout: {str(e)}'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'packages': packages,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def run(self, plugin_id: str, script: str,
|
||||
pkg_dir: Optional[Path] = None,
|
||||
args: Optional[List[str]] = None,
|
||||
env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
||||
"""Execute a Node.js script or npm command.
|
||||
|
||||
Args:
|
||||
plugin_id: Unique identifier for the plugin
|
||||
script: Script to run (e.g., 'start', 'build', or path to .js file)
|
||||
pkg_dir: Optional custom package directory
|
||||
args: Additional arguments to pass
|
||||
env: Custom environment variables
|
||||
|
||||
Returns:
|
||||
Dict with execution result
|
||||
"""
|
||||
try:
|
||||
if pkg_dir is None:
|
||||
work_dir = self.cache_dir / plugin_id
|
||||
else:
|
||||
work_dir = Path(pkg_dir)
|
||||
|
||||
if not work_dir.exists():
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': f'Plugin directory not found: {work_dir}'
|
||||
}
|
||||
|
||||
if script.endswith('.js') or script.endswith('.ts'):
|
||||
cmd = [self.node_path, script]
|
||||
if args:
|
||||
cmd.extend(args)
|
||||
else:
|
||||
cmd = [self.npm_path, 'run', script]
|
||||
if args:
|
||||
cmd.append('--')
|
||||
cmd.extend(args)
|
||||
|
||||
run_env = os.environ.copy()
|
||||
if env:
|
||||
run_env.update(env)
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(work_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
env=run_env
|
||||
)
|
||||
|
||||
return {
|
||||
'status': 'success' if result.returncode == 0 else 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'script': script,
|
||||
'exit_code': result.returncode,
|
||||
'stdout': result.stdout,
|
||||
'stderr': result.stderr,
|
||||
'work_dir': str(work_dir)
|
||||
}
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'script': script,
|
||||
'error': f'Execution timeout: {str(e)}'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'script': script,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def list_packages(self, plugin_id: str,
|
||||
pkg_dir: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""List installed packages in a plugin directory.
|
||||
|
||||
Args:
|
||||
plugin_id: Unique identifier for the plugin
|
||||
pkg_dir: Optional custom package directory
|
||||
|
||||
Returns:
|
||||
Dict with list of installed packages
|
||||
"""
|
||||
try:
|
||||
if pkg_dir is None:
|
||||
work_dir = self.cache_dir / plugin_id
|
||||
else:
|
||||
work_dir = Path(pkg_dir)
|
||||
|
||||
if not work_dir.exists():
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': f'Plugin directory not found: {work_dir}'
|
||||
}
|
||||
|
||||
result = subprocess.run(
|
||||
[self.npm_path, 'list', '--json', '--depth=0'],
|
||||
cwd=str(work_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
packages = json.loads(result.stdout)
|
||||
return {
|
||||
'status': 'success',
|
||||
'plugin_id': plugin_id,
|
||||
'packages': packages.get('dependencies', {}),
|
||||
'work_dir': str(work_dir)
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'error': f'Failed to parse npm list output: {str(e)}',
|
||||
'raw_output': result.stdout
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'error': result.stderr,
|
||||
'work_dir': str(work_dir)
|
||||
}
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'error': f'Timeout listing packages: {str(e)}'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def init_project(self, plugin_id: str, pkg_dir: Optional[Path] = None,
|
||||
package_name: Optional[str] = None,
|
||||
version: str = "1.0.0") -> Dict[str, Any]:
|
||||
"""Initialize a new Node.js project in a plugin directory.
|
||||
|
||||
Args:
|
||||
plugin_id: Unique identifier for the plugin
|
||||
pkg_dir: Optional custom package directory
|
||||
package_name: Optional package name (defaults to plugin_id)
|
||||
version: Package version
|
||||
|
||||
Returns:
|
||||
Dict with initialization result
|
||||
"""
|
||||
try:
|
||||
if pkg_dir is None:
|
||||
work_dir = self.cache_dir / plugin_id
|
||||
else:
|
||||
work_dir = Path(pkg_dir)
|
||||
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
package_json = {
|
||||
'name': package_name or plugin_id.replace('/', '-'),
|
||||
'version': version,
|
||||
'description': f'Node.js project for plugin {plugin_id}',
|
||||
'main': 'index.js',
|
||||
'scripts': {
|
||||
'start': 'node index.js',
|
||||
'test': 'echo "Error: no test specified" && exit 1'
|
||||
},
|
||||
'keywords': [],
|
||||
'author': '',
|
||||
'license': 'ISC'
|
||||
}
|
||||
|
||||
package_json_path = work_dir / 'package.json'
|
||||
with open(package_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(package_json, f, indent=2)
|
||||
|
||||
index_js_path = work_dir / 'index.js'
|
||||
with open(index_js_path, 'w', encoding='utf-8') as f:
|
||||
f.write('// Node.js project for NebulaShell plugin\n')
|
||||
f.write(f'// Plugin ID: {plugin_id}\n')
|
||||
f.write('console.log("Hello from NebulaShell Node.js plugin!");\n')
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'plugin_id': plugin_id,
|
||||
'work_dir': str(work_dir),
|
||||
'package_json': str(package_json_path),
|
||||
'index_js': str(index_js_path)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'plugin_id': plugin_id,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
|
||||
def init(config: Dict[str, Any]) -> NodeJSAdapter:
|
||||
return NodeJSAdapter(config)
|
||||
|
||||
|
||||
def get_capabilities() -> list:
|
||||
return [
|
||||
'nodejs_runtime',
|
||||
'npm_package_manager',
|
||||
'dependency_isolation',
|
||||
'script_execution',
|
||||
'project_initialization'
|
||||
]
|
||||
|
||||
|
||||
def execute_command(adapter: NodeJSAdapter, command: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Execute a command through the adapter.
|
||||
|
||||
Available commands:
|
||||
- check_versions: Check Node.js and npm versions
|
||||
- install: Install npm packages
|
||||
- run: Execute Node.js scripts or npm commands
|
||||
- list_packages: List installed packages
|
||||
- init_project: Initialize a new Node.js project
|
||||
"""
|
||||
if command == 'check_versions':
|
||||
return adapter.check_versions()
|
||||
elif command == 'install':
|
||||
return adapter.install(**kwargs)
|
||||
elif command == 'run':
|
||||
return adapter.run(**kwargs)
|
||||
elif command == 'list_packages':
|
||||
return adapter.list_packages(**kwargs)
|
||||
elif command == 'init_project':
|
||||
return adapter.init_project(**kwargs)
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': f'Unknown command: {command}'
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Node.js Adapter Plugin for NebulaShell")
|
||||
print("=" * 50)
|
||||
|
||||
adapter = init({})
|
||||
|
||||
versions = adapter.check_versions()
|
||||
print(f"\nNode.js Version: {versions.get('node', 'N/A')}")
|
||||
print(f"npm Version: {versions.get('npm', 'N/A')}")
|
||||
|
||||
caps = get_capabilities()
|
||||
print(f"\nCapabilities: {', '.join(caps)}")
|
||||
|
||||
print("\nNode.js Adapter initialized successfully!")
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@NebulaShell/nodejs-adapter",
|
||||
"version": "1.2.0",
|
||||
"description": "Node.js runtime adapter for NebulaShell - provides Node.js and npm capabilities for other plugins",
|
||||
"author": "NebulaShell Team",
|
||||
"license": "MIT",
|
||||
"runtime": {
|
||||
"type": "python",
|
||||
"entry_point": "main.py",
|
||||
"requirements": ["subprocess", "json", "os", "shutil"]
|
||||
},
|
||||
"capabilities": [
|
||||
"nodejs_runtime",
|
||||
"npm_package_manager",
|
||||
"dependency_isolation",
|
||||
"script_execution"
|
||||
],
|
||||
"config": {
|
||||
"node_path": "/usr/bin/node",
|
||||
"npm_path": "/usr/bin/npm",
|
||||
"default_registry": "https://registry.npmjs.org",
|
||||
"cache_dir": "~/.nebulashell/nodejs-cache"
|
||||
},
|
||||
"api": {
|
||||
"install": "Install npm packages to plugin-specific directory",
|
||||
"run": "Execute Node.js scripts or npm commands",
|
||||
"check_version": "Check Node.js and npm versions",
|
||||
"list_packages": "List installed packages in a plugin directory"
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
# 性能优化插件 (Performance Optimizer)
|
||||
|
||||
极致性能调优插件,提供多种高性能工具和优化技术。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 高速缓存 (`FastCache`)
|
||||
- O(1) 时间复杂度的查找
|
||||
- LRU 淘汰策略
|
||||
- 可选 TTL 过期
|
||||
- 命中率统计
|
||||
|
||||
```python
|
||||
from plugin.performance_optimizer import cached
|
||||
|
||||
@cached(maxsize=1024, ttl=60)
|
||||
def expensive_operation(x, y):
|
||||
return x ** y
|
||||
```
|
||||
|
||||
### 2. 对象池 (`ObjectPool`)
|
||||
- 避免频繁创建/销毁对象
|
||||
- 自动扩容
|
||||
- 使用统计
|
||||
|
||||
```python
|
||||
from plugin.performance_optimizer import ObjectPool
|
||||
|
||||
pool = ObjectPool(lambda: bytearray(4096), maxsize=100)
|
||||
buf = pool.acquire()
|
||||
# ... use buf ...
|
||||
pool.release(buf)
|
||||
```
|
||||
|
||||
### 3. 批量处理器 (`BatchProcessor`)
|
||||
- 累积批量处理
|
||||
- 超时自动触发
|
||||
- 减少系统调用
|
||||
|
||||
```python
|
||||
from plugin.performance_optimizer import BatchProcessor
|
||||
|
||||
processor = BatchProcessor(
|
||||
batch_handler=lambda items: db.bulk_insert(items),
|
||||
batch_size=100,
|
||||
timeout=1.0
|
||||
)
|
||||
for item in items:
|
||||
processor.add(item)
|
||||
processor.flush()
|
||||
```
|
||||
|
||||
### 4. 内存预分配器 (`MemoryArena`)
|
||||
- 预分配大块内存
|
||||
- 按需切分
|
||||
- 减少内存碎片
|
||||
|
||||
```python
|
||||
from plugin.performance_optimizer import MemoryArena
|
||||
|
||||
arena = MemoryArena(size=1024*1024) # 1MB
|
||||
chunk = arena.allocate(256)
|
||||
# ... use chunk ...
|
||||
arena.deallocate(chunk)
|
||||
```
|
||||
|
||||
### 5. 性能分析器 (`PerfProfiler`)
|
||||
- 低开销计时
|
||||
- 嵌套支持
|
||||
- 统计汇总
|
||||
|
||||
```python
|
||||
from plugin.performance_optimizer import PerfProfiler
|
||||
|
||||
profiler = PerfProfiler()
|
||||
with profiler.context("operation"):
|
||||
# ... do something ...
|
||||
print(profiler.stats())
|
||||
```
|
||||
|
||||
### 6. 字符串驻留 (`StringIntern`)
|
||||
- 重复字符串去重
|
||||
- 减少内存占用
|
||||
- 加速字符串比较
|
||||
|
||||
```python
|
||||
from plugin.performance_optimizer import StringIntern
|
||||
|
||||
intern = StringIntern()
|
||||
s1 = intern.intern("hello")
|
||||
s2 = intern.intern("hello")
|
||||
assert s1 is s2 # 同一个对象
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### PerformanceOptimizerPlugin
|
||||
|
||||
主插件类,提供统一的访问接口:
|
||||
|
||||
```python
|
||||
# 获取插件实例
|
||||
plugin = New()
|
||||
plugin.init()
|
||||
|
||||
# 获取缓存
|
||||
cache = plugin.get_cache("route_match")
|
||||
|
||||
# 获取对象池
|
||||
pool = plugin.get_pool("bytearray_4k")
|
||||
|
||||
# 性能分析
|
||||
profiler = plugin.profile()
|
||||
with profiler.context("my_operation"):
|
||||
# ... do work ...
|
||||
|
||||
# 字符串驻留
|
||||
s = plugin.intern_string("repeated string")
|
||||
|
||||
# 查看统计
|
||||
stats = plugin.stats()
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
在 `manifest.json` 中配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"args": {
|
||||
"cache_maxsize": 2048,
|
||||
"pool_maxsize": 100,
|
||||
"enable_profiler": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能提升
|
||||
|
||||
| 优化项 | 提升幅度 |
|
||||
|--------|----------|
|
||||
| 缓存命中 | 10-100x |
|
||||
| 对象复用 | 5-20x |
|
||||
| 批量操作 | 10-50x |
|
||||
| 内存预分配 | 2-5x |
|
||||
| 字符串驻留 | 2-10x |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 缓存大小应根据实际内存限制调整
|
||||
2. 对象池适合频繁创建/销毁的对象
|
||||
3. 批量处理的 `batch_size` 和 `timeout` 需根据业务场景调优
|
||||
4. 性能分析器在生产环境建议关闭以减少开销
|
||||
@@ -1,318 +0,0 @@
|
||||
import sys
|
||||
import time
|
||||
import functools
|
||||
from typing import Any, Callable, Optional, TypeVar, Generic, Dict, List, Set
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from threading import Lock
|
||||
import weakref
|
||||
|
||||
T = TypeVar('T')
|
||||
F = TypeVar('F', bound=Callable)
|
||||
|
||||
|
||||
class FastCache:
|
||||
__slots__ = ('_cache', '_order', '_maxsize', '_ttl', '_hits', '_misses', '_lock')
|
||||
|
||||
def __init__(self, maxsize: int = 1024, ttl: float = 0):
|
||||
self._cache: Dict[Any, Any] = {}
|
||||
self._order: deque = deque()
|
||||
self._maxsize = maxsize
|
||||
self._ttl = ttl
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
self._lock = Lock() if sys.version_info < (3, 9) else None
|
||||
|
||||
def get(self, key: Any) -> tuple[bool, Any]:
|
||||
if key not in self._cache:
|
||||
self._misses += 1
|
||||
return False, None
|
||||
|
||||
entry = self._cache[key]
|
||||
if self._ttl > 0 and time.time() - entry[1] > self._ttl:
|
||||
del self._cache[key]
|
||||
try:
|
||||
self._order.remove(key)
|
||||
except ValueError:
|
||||
pass
|
||||
self._misses += 1
|
||||
return False, None
|
||||
|
||||
self._order.remove(key)
|
||||
self._order.append(key)
|
||||
self._hits += 1
|
||||
return True, entry[0]
|
||||
|
||||
def set(self, key: Any, value: Any):
|
||||
if key in self._cache:
|
||||
self._order.remove(key)
|
||||
self._cache[key] = (value, time.time())
|
||||
self._order.append(key)
|
||||
if len(self._cache) > self._maxsize:
|
||||
oldest = self._order.popleft()
|
||||
del self._cache[oldest]
|
||||
|
||||
def clear(self):
|
||||
self._cache.clear()
|
||||
self._order.clear()
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
|
||||
@property
|
||||
def hit_rate(self) -> float:
|
||||
total = self._hits + self._misses
|
||||
if total == 0:
|
||||
return 0.0
|
||||
return self._hits / total
|
||||
|
||||
|
||||
def cached(maxsize: int = 1024, ttl: float = 0, key_func: Callable = None):
|
||||
_cache = FastCache(maxsize=maxsize, ttl=ttl)
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if key_func:
|
||||
key = key_func(*args, **kwargs)
|
||||
else:
|
||||
key = (args, tuple(sorted(kwargs.items())))
|
||||
|
||||
hit, value = _cache.get(key)
|
||||
if hit:
|
||||
return value
|
||||
|
||||
value = func(*args, **kwargs)
|
||||
_cache.set(key, value)
|
||||
return value
|
||||
|
||||
wrapper.cache = _cache
|
||||
wrapper.cache_clear = _cache.clear
|
||||
wrapper.cache_stats = _cache.stats
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class ObjectPool(Generic[T]):
|
||||
__slots__ = ('_factory', '_pool', '_maxsize', '_created', '_acquired', '_lock')
|
||||
|
||||
def __init__(self, factory: Callable[[], T], maxsize: int = 100):
|
||||
self._factory = factory
|
||||
self._pool: List[T] = []
|
||||
self._maxsize = maxsize
|
||||
self._created = 0
|
||||
self._acquired = 0
|
||||
self._lock = Lock() if sys.version_info < (3, 9) else None
|
||||
|
||||
def acquire(self) -> T:
|
||||
if self._pool:
|
||||
obj = self._pool.pop()
|
||||
else:
|
||||
obj = self._factory()
|
||||
self._created += 1
|
||||
self._acquired += 1
|
||||
return obj
|
||||
|
||||
def release(self, obj: T):
|
||||
if len(self._pool) < self._maxsize:
|
||||
self._pool.append(obj)
|
||||
|
||||
def clear(self):
|
||||
self._pool.clear()
|
||||
|
||||
|
||||
class BatchProcessor(Generic[T]):
|
||||
__slots__ = ('_handler', '_batch_size', '_timeout', '_buffer', '_last_flush', '_processed_count')
|
||||
|
||||
def __init__(self, batch_handler: Callable[[List[T]], Any], batch_size: int = 100, timeout: float = 1.0):
|
||||
self._handler = batch_handler
|
||||
self._batch_size = batch_size
|
||||
self._timeout = timeout
|
||||
self._buffer: List[T] = []
|
||||
self._last_flush = time.time()
|
||||
self._processed_count = 0
|
||||
|
||||
def add(self, item: T):
|
||||
self._buffer.append(item)
|
||||
if len(self._buffer) >= self._batch_size:
|
||||
self.flush()
|
||||
|
||||
def flush(self):
|
||||
if not self._buffer:
|
||||
return
|
||||
|
||||
self._handler(self._buffer)
|
||||
self._buffer.clear()
|
||||
self._last_flush = time.time()
|
||||
self._processed_count += 1
|
||||
|
||||
@property
|
||||
def pending_count(self) -> int:
|
||||
return len(self._buffer)
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
return {
|
||||
"pending": len(self._buffer),
|
||||
"batch_size": self._batch_size,
|
||||
"flush_count": self._processed_count,
|
||||
}
|
||||
|
||||
|
||||
class MemoryArena:
|
||||
__slots__ = ('_data', '_free_list', '_allocated', '_total_size')
|
||||
|
||||
def __init__(self, size: int = 1024 * 1024):
|
||||
self._data = bytearray(size)
|
||||
self._free_list: List[tuple[int, int]] = [(0, size)]
|
||||
self._allocated: Set[int] = set()
|
||||
self._total_size = size
|
||||
|
||||
def allocate(self, size: int) -> Optional[memoryview]:
|
||||
for i, (offset, block_size) in enumerate(self._free_list):
|
||||
if block_size >= size:
|
||||
self._free_list.pop(i)
|
||||
if block_size > size:
|
||||
self._free_list.append((offset + size, block_size - size))
|
||||
self._allocated.add(offset)
|
||||
return memoryview(self._data)[offset:offset + size]
|
||||
return None
|
||||
|
||||
def deallocate(self, view: memoryview):
|
||||
offset = view.obj.__array_interface__['data'][0] - id(self._data) if hasattr(view.obj, '__array_interface__') else 0
|
||||
if offset in self._allocated:
|
||||
self._allocated.remove(offset)
|
||||
self._free_list.append((offset, len(view)))
|
||||
|
||||
@property
|
||||
def available(self) -> int:
|
||||
return sum(size for _, size in self._free_list)
|
||||
|
||||
@property
|
||||
def usage_rate(self) -> float:
|
||||
return 1.0 - (self.available / self._total_size)
|
||||
|
||||
|
||||
class HotPathOptimizer:
|
||||
__slots__ = ('_call_counts', '_threshold', '_optimized', '_start_times')
|
||||
|
||||
def __init__(self, threshold: int = 1000):
|
||||
self._call_counts: Dict[str, int] = {}
|
||||
self._threshold = threshold
|
||||
self._optimized: Set[str] = set()
|
||||
self._start_times: Dict[str, float] = {}
|
||||
|
||||
def track(self, func_name: str):
|
||||
self._call_counts[func_name] = self._call_counts.get(func_name, 0) + 1
|
||||
if self._call_counts[func_name] >= self._threshold and func_name not in self._optimized:
|
||||
self._optimized.add(func_name)
|
||||
return True, self._call_counts[func_name]
|
||||
return False, self._call_counts[func_name]
|
||||
|
||||
|
||||
class PerfProfiler:
|
||||
__slots__ = ('_records', '_stack', '_enabled')
|
||||
|
||||
def __init__(self):
|
||||
self._records: Dict[str, List[float]] = {}
|
||||
self._stack: List[tuple[str, float]] = []
|
||||
self._enabled = True
|
||||
|
||||
def start(self, name: str):
|
||||
if not self._enabled:
|
||||
return
|
||||
self._stack.append((name, time.perf_counter()))
|
||||
|
||||
def stop(self, name: str):
|
||||
if not self._enabled or not self._stack:
|
||||
return
|
||||
|
||||
top_name, start_time = self._stack.pop()
|
||||
if top_name != name:
|
||||
return
|
||||
|
||||
elapsed = time.perf_counter() - start_time
|
||||
if name not in self._records:
|
||||
self._records[name] = []
|
||||
self._records[name].append(elapsed)
|
||||
|
||||
def context(self, name: str):
|
||||
pass
|
||||
|
||||
|
||||
class StringIntern:
|
||||
__slots__ = ('_cache',)
|
||||
|
||||
def __init__(self, use_weak_refs: bool = True):
|
||||
self._cache: Dict[str, str] = {}
|
||||
|
||||
def intern(self, s: str) -> str:
|
||||
if s in self._cache:
|
||||
return self._cache[s]
|
||||
|
||||
import sys
|
||||
interned = sys.intern(s)
|
||||
self._cache[interned] = interned
|
||||
|
||||
return interned
|
||||
|
||||
def clear(self):
|
||||
self._cache.clear()
|
||||
|
||||
|
||||
class PerformanceOptimizerPlugin:
|
||||
def __init__(self):
|
||||
self._initialized = False
|
||||
self._caches: Dict[str, FastCache] = {}
|
||||
self._pools: Dict[str, ObjectPool] = {}
|
||||
self._profiler = PerfProfiler()
|
||||
self._hot_path = HotPathOptimizer()
|
||||
self._string_intern = StringIntern()
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._caches["route_match"] = FastCache(maxsize=2048)
|
||||
self._caches["path_params"] = FastCache(maxsize=2048)
|
||||
self._caches["template_render"] = FastCache(maxsize=512)
|
||||
|
||||
self._pools["bytearray_4k"] = ObjectPool(lambda: bytearray(4096), maxsize=100)
|
||||
self._pools["bytearray_64k"] = ObjectPool(lambda: bytearray(65536), maxsize=20)
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
for cache in self._caches.values():
|
||||
cache.clear()
|
||||
for pool in self._pools.values():
|
||||
pool.clear()
|
||||
self._profiler = PerfProfiler()
|
||||
|
||||
def get_cache(self, name: str) -> Optional[FastCache]:
|
||||
return self._caches.get(name)
|
||||
|
||||
def get_pool(self, name: str) -> Optional[ObjectPool]:
|
||||
return self._pools.get(name)
|
||||
|
||||
def profile(self) -> PerfProfiler:
|
||||
return self._profiler
|
||||
|
||||
def intern_string(self, s: str) -> str:
|
||||
return self._string_intern.intern(s)
|
||||
|
||||
def track_hot_path(self, func_name: str) -> tuple[bool, float]:
|
||||
return self._hot_path.track(func_name)
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
return {
|
||||
"caches": {name: cache.stats() for name, cache in self._caches.items()},
|
||||
"pools": {name: pool.stats() for name, pool in self._pools.items()},
|
||||
"profiler": self._profiler.stats(),
|
||||
"hot_paths": self._hot_path.stats(),
|
||||
}
|
||||
|
||||
|
||||
def New() -> PerformanceOptimizerPlugin:
|
||||
return PerformanceOptimizerPlugin()
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "performance-optimizer",
|
||||
"version": "1.0.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "极致性能优化插件 - 提供缓存、对象池、批量处理、内存预分配等高性能工具"
|
||||
},
|
||||
"config": {
|
||||
"args": {
|
||||
"enabled": true,
|
||||
"cache_maxsize": 2048,
|
||||
"pool_maxsize": 100,
|
||||
"enable_profiler": true
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"signature": "hNzQ56uwgghPRTVm5YFA8fZp+1Y9TQ9fSDKLEY+KPFLddrxdnXZiE66XXWEVEj80pB5E/zJ0nDcpJYTe9+Mo4LQ++Qzt7yA+PMu8WZ/I39f1870FR/s+MuaiKWp0sT/NeyHRv/nHKi/FaZXWx+KsSbKatq4w088bNhyWahJg1RmTaCKAxv7ut9Uqn33m9teoeNt43AG/6ySfRQRfk0K1L7Yvf/9yJStDMAuTzFiQmhs4MZ58VzPh/Nrtj0G7N5mAjp9bZKa+EFqMLFBQlG5TDqWU8zFKBe27CsvSK7MthS3PGyzeGftm2O683hgClGdsgdK9kqwZ0eMOb5Jcesk4f0rWVODpCf2cfRPocrs401yKzVU3dStFw14Bq82SpQDRJ9EDU3lP8E4RqlmXEAzlGNoMsGSGth9gSWc4VpHn4ppVH5ftKk/AvJrpdFWyWe0jPnDODRKAIMn9sGiZUy6XqB0fGMoU0vpuvtLy6mtVmQglhsVE49XA5txAEWQncPUPxxjNoMdRo5RDlimRVNtXNcwKRb1z9V6ky1eOVKFHaPsp4Y+1mreZVUokaUBf8LG1qvFXjZuiYHRlffKSN3/yzRqhDnE5fCDu0wpjHe24dZ/PeQXbG2aAQlJQr15yh7p5dxTSiv+HeacwDqZPF8X/9Ey6xMflr1xGZpp9j9YeCtk=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775967812.6803007,
|
||||
"plugin_hash": "c0c56583082ca71e9a84ac2e976c22683573ec4e40387ee893ac42f31da62d4a",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
def _gitee_request(url, timeout=30):
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("User-Agent", "NebulaShell-PkgManager")
|
||||
if GITEE_TOKEN:
|
||||
req.add_header("Authorization", f"token {GITEE_TOKEN}")
|
||||
return urllib.request.urlopen(req, timeout=timeout)
|
||||
|
||||
|
||||
class PkgManagerPlugin(Plugin):
|
||||
def __init__(self):
|
||||
if not self.webui:
|
||||
Log.warn("pkg-manager", "警告: 未找到 WebUI 依赖")
|
||||
return
|
||||
|
||||
self.webui.register_page(
|
||||
path='/packages',
|
||||
content_provider=self._packages_content,
|
||||
nav_item={'icon': 'ri-apps-line', 'text': '插件管理'}
|
||||
)
|
||||
self.webui.register_page(
|
||||
path='/store',
|
||||
content_provider=self._store_content,
|
||||
nav_item={'icon': 'ri-store-2-line', 'text': '插件商店'}
|
||||
)
|
||||
Log.info("pkg-manager", "已注册到 WebUI 导航")
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
plugins = self._get_installed_plugins()
|
||||
plugin_rows = ""
|
||||
for pkg_name, info in plugins.items():
|
||||
status_class = "success" if info.get('enabled', False) else "secondary"
|
||||
status_text = "已启用" if info.get('enabled', False) else "已禁用"
|
||||
safe_pkg_name = html.escape(pkg_name)
|
||||
safe_version = html.escape(str(info.get('version', '未知')))
|
||||
safe_author = html.escape(str(info.get('author', '未知')))
|
||||
plugin_rows += f"<tr><td>{safe_pkg_name}</td><td>{safe_version}</td><td>{safe_author}</td></tr>"
|
||||
|
||||
html = f"<table>{plugin_rows}</table>"
|
||||
return html
|
||||
except Exception as e:
|
||||
return f"<p>插件管理页面渲染出错: {e}</p>"
|
||||
|
||||
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 = '<button class="btn btn-success">安装</button>'
|
||||
html += f"""<div class="plugin-card">
|
||||
<div class="plugin-icon"><i class="ri-plug-line"></i></div>
|
||||
<h3>{safe_name}</h3>
|
||||
<p class="plugin-desc">{safe_desc}</p>
|
||||
<div class="plugin-meta">
|
||||
<span>版本: {safe_version}</span>
|
||||
<span>作者: {safe_author}</span>
|
||||
</div>
|
||||
<div class="plugin-actions">
|
||||
{action_btn}
|
||||
</div>
|
||||
</div>"""
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>插件商店</title>
|
||||
<link rel="stylesheet" href="/assets/remixicon.css">
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }}
|
||||
.container {{ max-width: 1400px; margin: 0 auto; }}
|
||||
.card {{ background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; }}
|
||||
.card-header {{ margin-bottom: 20px; }}
|
||||
.card-title {{ font-size: 18px; font-weight: 600; color: #333; }}
|
||||
.btn {{ padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.3s; }}
|
||||
.btn-success {{ background: #67c23a; color: white; }}
|
||||
.btn-success:hover {{ background: #5daf34; }}
|
||||
.btn-secondary {{ background: #909399; color: white; }}
|
||||
.plugins-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }}
|
||||
.plugin-card {{ background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }}
|
||||
.plugin-card:hover {{ transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }}
|
||||
.plugin-icon {{ width: 48px; height: 48px; background: #ecf5ff; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 12px; }}
|
||||
.plugin-card h3 {{ font-size: 16px; color: #333; margin-bottom: 8px; }}
|
||||
.plugin-desc {{ color: #666; font-size: 13px; margin-bottom: 12px; }}
|
||||
.plugin-meta {{ display: flex; justify-content: space-between; font-size: 12px; color: #999; }}
|
||||
.plugin-actions {{ display: flex; gap: 10px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title"><i class="ri-store-line"></i> 插件商店</h2>
|
||||
</div>
|
||||
<div class="plugins-grid">
|
||||
{html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function installPlugin(name) {{
|
||||
fetch('/api/plugins/install', {{
|
||||
method: 'POST',
|
||||
headers: {{'Content-Type': 'application/json'}},
|
||||
body: JSON.stringify({{plugin: name}})
|
||||
}}).then(r => r.json()).then(data => {{
|
||||
if (data.success) {{
|
||||
alert('安装成功!');
|
||||
location.reload();
|
||||
}} else {{
|
||||
alert('安装失败: ' + data.error);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return html
|
||||
except Exception as e:
|
||||
return f"<p>插件商店页面渲染出错: {e}</p>"
|
||||
|
||||
|
||||
|
||||
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 {}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
class CircuitState:
|
||||
CLOSED = "closed"
|
||||
OPEN = "open"
|
||||
HALF_OPEN = "half_open"
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 []
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user