重构:核心迁移至 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:
Falck
2026-05-05 07:29:43 +08:00
parent 4441a968db
commit 3a096f59a9
184 changed files with 5715 additions and 10066 deletions

View File

@@ -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}")

View File

@@ -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 # 本文档
```

View File

@@ -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()

View File

@@ -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": ["*"]
}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 []

View File

@@ -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
}

View File

@@ -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

View File

@@ -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": ["*"]
}

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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": ["*"]
}

View File

@@ -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"]
}
```

View File

@@ -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"
}

View File

@@ -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()

View File

@@ -1,15 +0,0 @@
{
"metadata": {
"name": "dependency",
"version": "1.0.0",
"author": "NebulaShell",
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -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": []
}

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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()`

View File

@@ -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"
}

View File

@@ -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()

View File

@@ -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"]
}

View File

@@ -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
}
}
}
```

View File

@@ -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"
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"}
)

View File

@@ -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

View File

@@ -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
}
}
}
```

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -1,4 +0,0 @@
class TcpRouter:
def handle(self, request: dict) -> dict:
pass

View File

@@ -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())

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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}}"
}
}

View File

@@ -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}}"
}
}

View File

@@ -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}}"
}
}

View File

@@ -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()

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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}")
```

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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": []
}

View File

@@ -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()
```

View File

@@ -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"
}

View File

@@ -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

View File

@@ -1,15 +0,0 @@
{
"metadata": {
"name": "lifecycle",
"version": "1.0.0",
"author": "NebulaShell",
"description": "生命周期管理 - 管理插件的状态转换和钩子",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -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"
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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": ["*"]
}

View File

@@ -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.

View File

@@ -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!")

View File

@@ -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"
}
}

View File

@@ -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. 性能分析器在生产环境建议关闭以减少开销

View File

@@ -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()

View File

@@ -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": []
}

View File

@@ -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"
}

View File

@@ -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 {}

View File

@@ -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"]
}

View File

@@ -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()
```

View File

@@ -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"
}

View File

@@ -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()

View File

@@ -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"]
}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -1,4 +0,0 @@
class CircuitState:
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"

View File

@@ -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)

View File

@@ -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

View File

@@ -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 []

View File

@@ -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())

View File

@@ -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

View File

@@ -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