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

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

测试覆盖: 14→91个测试, 全部通过
2026-05-12 11:40:06 +08:00

194 lines
7.2 KiB
Python

"""
Node.js Runtime Adapter for NebulaShell
=====================================
This plugin acts as a pure service provider (Adapter). It does NOT contain its own business logic or pkg.
Instead, it exposes standard interfaces for OTHER plugins to execute Node.js/npm commands
within THEIR own contexts (specifically their ./pkg directories).
Usage by other plugins:
1. Get this adapter from the shared service registry.
2. Call adapter.execute_in_context(plugin_root="./path/to/other-plugin", command="npm start")
3. The adapter will automatically switch CWD to "./path/to/other-plugin/pkg" and run the command.
"""
import os
import sys
import json
import subprocess
import shutil
from typing import Dict, Any, List, Optional
class NodeJSAdapter:
"""Pure Node.js Runtime Adapter.
Provides execution context switching for other plugins."""
def __init__(self):
self.name = "nodejs-adapter"
self.version = "1.0.0"
self.description = "Stateless Node.js runtime adapter for cross-plugin execution"
self.node_path = None
self.npm_path = None
self._detect_runtime()
def _detect_runtime(self):
self.node_path = shutil.which('node')
self.npm_path = shutil.which('npm')
def get_info(self):
versions = self.check_versions()
return {
'available': bool(self.node_path),
'npm_available': bool(self.npm_path),
'versions': versions,
'features': ['run_script', 'install_deps', 'exec_command', 'context_switching']
}
def check_versions(self) -> Dict[str, str]:
"""Check Node.js and npm versions."""
versions = {}
if self.node_path:
try:
result = subprocess.run([self.node_path, '--version'], capture_output=True, text=True, timeout=30)
versions['node'] = result.stdout.strip()
except Exception as e:
versions['node'] = f'Error: {e}'
if self.npm_path:
try:
result = subprocess.run([self.npm_path, '--version'], capture_output=True, text=True, timeout=30)
versions['npm'] = result.stdout.strip()
except Exception as e:
versions['npm'] = f'Error: {e}'
return versions
def execute_in_context(self, plugin_root: str, command_args: List[str], is_npm: bool = False) -> Dict[str, Any]:
"""Execute a command within the context of another plugin.
Args:
plugin_root: The root directory of the CALLING plugin (e.g., /workspace/oss/plugins/my-web-app)
command_args: The command arguments (e.g., ['start'] or ['install', 'express'])
is_npm: If True, uses 'npm'. If False, uses 'node'.
Behavior:
1. Targets the './pkg' subdirectory inside plugin_root.
2. Sets cwd to that pkg directory.
3. Executes the command.
4. Ensures dependencies install into that specific pkg folder.
"""
if not self.node_path:
return {'success': False, 'error': 'Node.js runtime not found'}
if is_npm and not self.npm_path:
return {'success': False, 'error': 'npm not found'}
work_dir = os.path.join(plugin_root, 'pkg')
if not os.path.exists(work_dir):
return {'success': False, 'error': f'Target pkg directory not found: {work_dir}'}
try:
executable = self.npm_path if is_npm else self.node_path
cmd = [executable] + command_args
env = os.environ.copy()
env['npm_config_prefix'] = work_dir
env['NODE_PATH'] = os.path.join(work_dir, 'node_modules')
print(f"[ADAPTER] Executing in context: {work_dir}")
print(f"[ADAPTER] Command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
cwd=work_dir,
env=env,
capture_output=True,
text=True,
timeout=300)
return {
'success': result.returncode == 0,
'stdout': result.stdout,
'stderr': result.stderr,
'returncode': result.returncode,
'cwd': work_dir
}
except subprocess.TimeoutExpired:
return {'success': False, 'error': 'Command execution timeout'}
except Exception as e:
return {'success': False, 'error': f'{type(e).__name__} - {e}'}
def install_dependencies(self, plugin_root: str, packages: List[str] = None) -> Dict[str, Any]:
"""Helper: Install dependencies for a specific plugin.
If packages is None, runs 'npm install' (installs from package.json).
If packages is provided, runs 'npm install <pkg1> <pkg2>...'."""
args = ['install']
if packages:
args.extend(packages)
return self.execute_in_context(plugin_root, args, is_npm=True)
def run_script(self, plugin_root: str, script_name: str, extra_args: List[str] = None) -> Dict[str, Any]:
"""Helper: Run an npm script (e.g., 'start', 'build') for a specific plugin."""
args = ['run', script_name]
if extra_args:
args.append('--')
args.extend(extra_args)
return self.execute_in_context(plugin_root, args, is_npm=True)
def run_file(self, plugin_root: str, file_path: str, args: List[str] = None) -> Dict[str, Any]:
"""Helper: Run a specific JS file within a plugin's pkg directory.
file_path should be relative to the pkg dir (e.g., 'index.js')."""
cmd_args = [file_path]
if args:
cmd_args.extend(args)
return self.execute_in_context(plugin_root, cmd_args, is_npm=False)
def init_project(self, plugin_root: str, name: str = "plugin-project") -> Dict[str, Any]:
"""Helper: Initialize a package.json in the plugin's pkg directory."""
res = self.execute_in_context(plugin_root, ['init', '-y'], is_npm=True)
if not res['success']:
return res
pkg_json_path = os.path.join(plugin_root, 'pkg', 'package.json')
if os.path.exists(pkg_json_path):
try:
with open(pkg_json_path, 'r+') as f:
data = json.load(f)
data['name'] = name
data['private'] = True
f.seek(0)
json.dump(data, f, indent=2)
f.truncate()
return {'success': True, 'message': f'Initialized project {name}'}
except Exception as e:
return {'success': False, 'error': f'Failed to update package.json: {e}'}
return res
class NodeJSAdapterPlugin:
"""Plugin-ABC-compatible wrapper for NodeJSAdapter"""
name = "nodejs-adapter"
version = "1.0.0"
description = "Stateless Node.js runtime adapter for cross-plugin execution"
def __init__(self):
self._adapter = NodeJSAdapter()
def init(self, deps=None):
pass
def start(self):
pass
def stop(self):
pass
def get_adapter(self) -> NodeJSAdapter:
return self._adapter
def __getattr__(self, name):
return getattr(self._adapter, name)
def New():
return NodeJSAdapterPlugin()