feat: 新增脚手架/开发模式/权限白名单/system-monitor插件
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled

- nebula create mod/key/list-templates 模组脚手架
- nebula dev 开发模式热重载
- manifest permissions.imports 权限白名单机制
- system-monitor 系统监控仪表盘插件
- 默认端口统一为 10086
- 修复 _init_nbpf 误读 Ed25519 私钥为 RSA 的 bug
- 更新 README.md 文档
This commit is contained in:
starlight-apk
2026-05-16 20:20:43 +08:00
parent bce27db4ac
commit 5fbc5cc335
14 changed files with 1225 additions and 312 deletions

View File

@@ -147,7 +147,7 @@ def info(ctx):
@cli.command(name="cli")
@click.option('--connect-host', default='127.0.0.1', help='后端地址(默认 127.0.0.1')
@click.option('--connect-port', default=8080, help='后端端口(默认 8080')
@click.option('--connect-port', default=10086, help='后端端口(默认 10086')
def cli_command(connect_host, connect_port):
"""启动 TUI 前端(前后端分离,连接已有后端)"""
click.echo("NebulaShell TUI 客户端(待实现)")
@@ -373,6 +373,306 @@ def keygen(output_dir, name):
click.echo(f" nebula nbpf pack ./my-plugin --ed25519-key {private_dir / f'{name}_ed25519.pem'} --rsa-key {private_dir / f'{name}_rsa.pem'} --rsa-pub {rsa_dir / f'{name}.pem'} --signer {name}")
# ═══════════════════════════════════════════════════════════════
# create 命令 — 模组脚手架
# ═══════════════════════════════════════════════════════════════
@cli.group()
def create():
"""创建模组/密钥等资源"""
pass
@create.command("mod")
@click.argument("name", type=str, required=False, default=None)
@click.option("--author", "-a", type=str, default=None, help="作者名")
@click.option("--description", "-d", type=str, default=None, help="模组描述")
@click.option("--type", "-t", "mod_type", type=click.Choice(["example", "adapter", "service", "security", "tool"]), default="example", help="模组类型")
@click.option("--with-keys", is_flag=True, default=False, help="同时生成签名密钥")
@click.option("--output", "-o", type=str, default=None, help="输出目录")
@click.pass_context
def create_mod(ctx, name, author, description, mod_type, with_keys, output):
"""创建新模组脚手架"""
import string as _string
from pathlib import Path as _Path
# 交互式输入(如果参数缺失)
if not name:
name = click.prompt("📛 模组名称", type=str)
if not author:
author = click.prompt("👤 作者", type=str, default="anonymous")
if not description:
description = click.prompt("📝 描述", type=str, default="")
# 校验模组名
valid_chars = set(_string.ascii_lowercase + _string.digits + "-_")
safe_name = "".join(c for c in name.lower().replace(" ", "-") if c in valid_chars)
if not safe_name:
click.echo("❌ 模组名称无效,请使用字母、数字、连字符")
raise click.Abort()
# 确定输出目录
output_dir = _Path(output or safe_name)
if output_dir.exists():
click.echo(f"❌ 目录 '{output_dir}' 已存在")
raise click.Abort()
# 渲染模板
templates_dir = _Path(__file__).parent / "templates" / "mod"
if not templates_dir.exists():
click.echo("❌ 模板目录不存在,请检查安装")
raise click.Abort()
# 替换变量
replacements = {
"{{ mod_name }}": safe_name,
"{{ author }}": author,
"{{ description }}": description,
"{{ mod_type }}": mod_type,
}
output_dir.mkdir(parents=True, exist_ok=True)
for tmpl_file in templates_dir.iterdir():
if tmpl_file.is_file():
content_tmpl = tmpl_file.read_text(encoding="utf-8")
for old, new in replacements.items():
content_tmpl = content_tmpl.replace(old, new)
out_path = output_dir / tmpl_file.name
out_path.write_text(content_tmpl, encoding="utf-8")
click.echo(f" ✅ 创建: {out_path.name}")
click.echo("")
click.echo(f"🎉 模组 '{safe_name}' 创建成功!")
click.echo(f"📂 位置: {output_dir.resolve()}")
click.echo("")
click.echo("下一步:")
click.echo(f" cd {safe_name}")
click.echo(" # 编辑 main.py 实现功能")
click.echo(" # 然后打包:")
click.echo(f' nebula nbpf pack ./{safe_name} -o mods/{safe_name}.nbpf --ed25519-key <key> --rsa-key <key> --rsa-pub <key> --signer "{author}"')
# 可选生成密钥
if with_keys:
click.echo("")
click.echo("🔑 正在生成签名密钥...")
try:
from oss.core.nbpf.crypto import NBPCrypto
keys_dir = output_dir / "keys"
keys_dir.mkdir(exist_ok=True)
ed_priv, ed_pub = NBPCrypto.generate_ed25519_keypair()
(keys_dir / "ed25519.pem").write_bytes(ed_priv)
(keys_dir / "ed25519.pub.pem").write_bytes(ed_pub)
rsa_priv, rsa_pub = NBPCrypto.generate_rsa_keypair(key_size=2048)
(keys_dir / "rsa.pem").write_bytes(rsa_priv)
(keys_dir / "rsa.pub.pem").write_bytes(rsa_pub)
click.echo(f" ✅ Ed25519 密钥: {keys_dir}/ed25519.pem")
click.echo(f" ✅ RSA 密钥: {keys_dir}/rsa.pem")
click.echo("")
click.echo("打包命令:")
click.echo(f" nebula nbpf pack ./{safe_name} -o mods/{safe_name}.nbpf")
click.echo(f" --ed25519-key {keys_dir}/ed25519.pem")
click.echo(f" --rsa-key {keys_dir}/rsa.pem")
click.echo(f" --rsa-pub {keys_dir}/rsa.pub.pem")
except Exception as e:
click.echo(f" ⚠ 密钥生成失败: {e}")
@create.command("key")
@click.option("--output", "-o", type=str, default="./keys", help="密钥输出目录")
@click.option("--name", type=str, default="default", help="密钥名称")
def create_key(output, name):
"""生成 Ed25519 + RSA 签名密钥对"""
from oss.core.nbpf.crypto import NBPCrypto
from pathlib import Path as _Path
output_path = _Path(output)
output_path.mkdir(parents=True, exist_ok=True)
click.echo(f"🔑 生成密钥对到: {output_path.resolve()}")
ed_priv, ed_pub = NBPCrypto.generate_ed25519_keypair()
(output_path / f"{name}_ed25519.pem").write_bytes(ed_priv)
(output_path / f"{name}_ed25519.pub.pem").write_bytes(ed_pub)
click.echo(f" ✅ Ed25519: {output_path / f'{name}_ed25519.pem'}")
rsa_priv, rsa_pub = NBPCrypto.generate_rsa_keypair(key_size=2048)
(output_path / f"{name}_rsa.pem").write_bytes(rsa_priv)
(output_path / f"{name}_rsa.pub.pem").write_bytes(rsa_pub)
click.echo(f" ✅ RSA: {output_path / f'{name}_rsa.pem'}")
click.echo("")
click.echo("密钥生成完成!")
@create.command("list-templates")
def list_templates():
"""列出可用的模板"""
from pathlib import Path as _Path
templates_base = _Path(__file__).parent / "templates"
if not templates_base.exists():
click.echo("没有可用的模板")
return
for tdir in templates_base.iterdir():
if tdir.is_dir():
files = [f.name for f in tdir.iterdir() if f.is_file()]
click.echo(f" 📦 {tdir.name}/")
for f in files:
click.echo(f" ├── {f}")
# ═══════════════════════════════════════════════════════════════
# dev 命令 — 开发模式热重载
# ═══════════════════════════════════════════════════════════════
@cli.command()
@click.argument("mod_dir", type=str, required=False, default=None)
@click.option("--port", "-p", type=int, default=None, help="HTTP API 端口")
@click.option("--host", type=str, default=None, help="监听地址")
@click.option("--skip-sign", is_flag=True, default=False, help="跳过签名验证(调试用)")
@click.pass_context
def dev(ctx, mod_dir, port, host, skip_sign):
"""开发模式 — 监听模组文件变化并自动热重载"""
import time as _time
import hashlib as _hashlib
from pathlib import Path as _Path
from oss.core.watcher import FileWatcher
from oss.logger.logger import Log as _Log
config = ctx.obj.get("config")
if port:
config.set("HTTP_API_PORT", port)
else:
config.set("HTTP_API_PORT", 10086)
if host:
config.set("HOST", host)
# 确定监听目录
watch_dirs = []
if mod_dir:
mod_path = _Path(mod_dir).resolve()
if not mod_path.exists():
click.echo(f"❌ 目录不存在: {mod_dir}")
raise click.Abort()
watch_dirs.append(mod_path)
click.echo(f"📁 监听目录: {mod_path}")
else:
# 默认监听 mods/ 和当前目录
watch_dirs.append(_Path.cwd())
click.echo(f"📁 监听目录: {_Path.cwd()}")
click.echo("")
# 启动 NebulaShell 服务
from oss.core.manager import PluginManager as _PluginManager
plugin_mgr = _PluginManager()
plugin_mgr.load_all()
# 同时加载 mods/ 目录下的 .nbpf 模组
from pathlib import Path as _P
mods_path = _P("mods")
if mods_path.exists():
for f in sorted(mods_path.iterdir()):
if f.suffix == ".nbpf":
plugin_mgr.load(f)
plugin_mgr.start_all()
# 启动 HTTP 服务
try:
plugin_mgr.start_http_server()
_Log.ok("Dev", f"HTTP API: http://{config.host}:{config.http_api_port}")
except Exception as e:
_Log.warn("Dev", f"HTTP 服务启动失败: {e}")
click.echo("")
click.echo("🔧 NebulaShell 开发模式已启动")
click.echo("=" * 50)
click.echo(f" HTTP: http://{config.host}:{config.http_api_port}")
click.echo(f" 监听: {', '.join(str(d) for d in watch_dirs)}")
click.echo(f" 签名验证: {'跳过' if skip_sign else '开启'}")
click.echo(f" 模组数: {len(plugin_mgr.plugins)}")
click.echo("=" * 50)
click.echo(" 按 Ctrl+C 停止")
click.echo("")
# 文件变更缓存
_file_hashes: dict[str, str] = {}
def _get_file_hash(path: _Path) -> str:
"""计算文件 hash"""
try:
return _hashlib.sha256(path.read_bytes()).hexdigest()
except Exception:
return ""
def _get_dir_hash(directory: _Path) -> dict[str, str]:
"""获取目录下所有文件的 hash"""
result = {}
for f in sorted(directory.rglob("*")):
if f.is_file() and ".nbpf" not in f.suffix and "__pycache__" not in str(f):
h = _get_file_hash(f)
if h:
result[str(f)] = h
return result
# 初始化 hash
for wd in watch_dirs:
if wd.is_dir():
_file_hashes.update(_get_dir_hash(wd))
# 主循环
try:
while True:
_time.sleep(1)
changed = False
for wd in watch_dirs:
if not wd.exists():
continue
current = _get_dir_hash(wd)
# 检查新增/修改
for fpath, h in current.items():
old_h = _file_hashes.get(fpath)
if old_h is None:
_Log.info("Dev", f"🆕 新增文件: {_Path(fpath).name}")
changed = True
elif old_h != h:
_Log.info("Dev", f"📝 文件变更: {_Path(fpath).name}")
changed = True
_file_hashes[fpath] = h
# 检查删除
for fpath in list(_file_hashes.keys()):
if fpath not in current:
_Log.info("Dev", f"🗑 文件删除: {_Path(fpath).name}")
_file_hashes.pop(fpath)
changed = True
if changed:
_Log.info("Dev", "检测到变更,尝试热重载...")
try:
# 重新加载所有模组
plugin_mgr.stop_all()
# 清空并重新加载
plugin_mgr.plugins.clear()
plugin_mgr._plugin_dirs.clear()
plugin_mgr.load_all()
from pathlib import Path as _P2
for f in sorted(_P2("mods").iterdir()):
if f.suffix == ".nbpf":
plugin_mgr.load(f)
plugin_mgr.start_all()
_Log.ok("Dev", f"热重载完成!当前模组数: {len(plugin_mgr.plugins)}")
except Exception as e:
_Log.error("Dev", f"热重载失败: {e}")
except KeyboardInterrupt:
click.echo("")
_Log.info("Dev", "正在停止开发模式...")
plugin_mgr.stop_all()
_Log.info("Dev", "开发模式已停止")
def main():
cmd_name = os.path.basename(sys.argv[0])
if cmd_name in ("oss", "oss.exe"):