Files
NebulaShell/oss/cli.py
Falck 3a096f59a9 重构:核心迁移至 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)
2026-05-05 07:29:43 +08:00

390 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CLI 入口"""
import click
import signal
import os
import sys
import random
from pathlib import Path
from oss import __version__
from oss.logger.logger import Log
from oss.plugin.manager import PluginManager
from oss.config import init_config, get_config
# 深度隐藏的成就系统导入
try:
from oss.core.achievements import init_achievements, get_validator, _cmd_echo, _cmd_help_internal, _cmd_list_all, _cmd_stats, _cmd_reset_progress, _cmd_export, _cmd_import, _cmd_verify, _cmd_debug, _cmd_info
_ACHIEVEMENTS_ENABLED = True
except ImportError:
_ACHIEVEMENTS_ENABLED = False
def _handle_hidden_command():
"""处理 !! 前缀的隐藏命令"""
if len(sys.argv) <= 1 or not sys.argv[1].startswith("!!"):
return False
if not _ACHIEVEMENTS_ENABLED:
print("成就系统未启用")
return True
cmd = sys.argv[1][2:]
args = sys.argv[2:]
cmd_map = {
"echo": _cmd_echo,
"help": _cmd_help_internal,
"list": _cmd_list_all,
"stats": _cmd_stats,
"reset": _cmd_reset_progress,
"export": _cmd_export,
"import": _cmd_import,
"verify": _cmd_verify,
"debug": _cmd_debug,
"info": _cmd_info,
}
if cmd in cmd_map:
validator = get_validator()
validator.use_hidden_command(cmd)
cmd_map[cmd](args)
else:
print(f"未知命令:!!{cmd}")
return True
@click.group()
@click.option('--config', '-c', type=str, help='配置文件路径')
@click.pass_context
def cli(ctx, config):
"""NebulaShell - 一切皆为插件"""
ctx.ensure_object(dict)
ctx.obj['config'] = init_config(config)
if _ACHIEVEMENTS_ENABLED:
try:
init_achievements()
except Exception:
pass
@cli.command()
@click.option('--host', type=str, default=None, help='监听地址')
@click.option('--port', type=int, default=None, help='HTTP API 端口')
@click.option('--tcp-port', type=int, default=None, help='HTTP TCP 端口')
@click.pass_context
def serve(ctx, host, port, tcp_port):
"""启动 NebulaShell 服务端"""
config = ctx.obj.get('config', get_config())
if host:
config.set('HOST', host)
if port:
config.set('HTTP_API_PORT', port)
if tcp_port:
config.set('HTTP_TCP_PORT', tcp_port)
Log.info("NebulaShell", f"NebulaShell {__version__} 启动")
Log.info("NebulaShell", f"监听地址:{config.host}:{config.http_api_port}")
Log.info("NebulaShell", f"数据目录:{config.data_dir.absolute()}")
Log.info("NebulaShell", f"插件仓库:{config.store_dir.absolute()}")
plugin_mgr = PluginManager()
plugin_mgr.load()
plugin_mgr.start()
Log.info("NebulaShell", "就绪")
def shutdown(sig, frame):
Log.info("NebulaShell", "停止中...")
plugin_mgr.stop()
Log.info("NebulaShell", "已停止")
raise SystemExit(0)
signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)
# 启动 REPL 交互(由 Core 内部提供)
try:
if hasattr(plugin_mgr, 'core') and plugin_mgr.core:
plugin_mgr.core.start_repl()
else:
Log.error("NebulaShell", "Core 未加载,无法启动 REPL")
signal.pause()
except Exception as e:
Log.error("NebulaShell", f"REPL 启动失败: {e}")
signal.pause()
@cli.command()
def version():
"""显示版本"""
click.echo(f"NebulaShell {__version__}")
@cli.command()
@click.pass_context
def info(ctx):
"""显示系统信息"""
config = ctx.obj.get('config', get_config())
click.echo(f"NebulaShell {__version__}")
click.echo(f"配置文件:{config._config_file or ''}")
click.echo(f"HTTP API 端口:{config.http_api_port}")
click.echo(f"HTTP TCP 端口:{config.http_tcp_port}")
click.echo(f"主机地址:{config.host}")
click.echo(f"数据目录:{config.data_dir.absolute()}")
click.echo(f"插件仓库:{config.store_dir.absolute()}")
click.echo(f"日志级别:{config.log_level}")
click.echo(f"权限检查:{'启用' if config.permission_check else '禁用'}")
# 彩蛋提示
click.echo("")
if random.random() < 0.1:
click.echo("✨ 奇怪的提示:试试在命令前加两个感叹号会怎样?比如 !!help")
elif random.random() < 0.05:
click.echo("🤔 听说有人用 !! 开头的命令发现了不得了的东西...")
@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')
def cli_command(connect_host, connect_port):
"""启动 TUI 前端(前后端分离,连接已有后端)"""
click.echo("NebulaShell TUI 客户端(待实现)")
click.echo(f"目标后端:{connect_host}:{connect_port}")
# ═══════════════════════════════════════════════════════════════
# NBPF 命令组
# ═══════════════════════════════════════════════════════════════
@cli.group()
def nbpf():
"""管理 .nbpf 插件包(打包/解包/签名/验证/密钥生成)"""
pass
@nbpf.command()
@click.argument('plugin_dir', type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.argument('output', type=click.Path(), default=None, required=False)
@click.option('--ed25519-key', type=click.Path(exists=True), help='Ed25519 私钥路径')
@click.option('--rsa-key', type=click.Path(exists=True), help='RSA 私钥路径')
@click.option('--rsa-pub', type=click.Path(exists=True), help='RSA 公钥路径')
@click.option('--signer', default='unknown', help='签名者名称')
@click.pass_context
def pack(ctx, plugin_dir, output, ed25519_key, rsa_key, rsa_pub, signer):
"""打包插件目录为 .nbpf 文件"""
from oss.core.nbpf import NBPFPacker
plugin_path = Path(plugin_dir)
if not output:
output = f"{plugin_path.name}.nbpf"
# 读取密钥
ed25519_private = Path(ed25519_key).read_bytes() if ed25519_key else None
rsa_private_pem = Path(rsa_key).read_bytes() if rsa_key else None
rsa_public_pem = Path(rsa_pub).read_bytes() if rsa_pub else None
if not ed25519_private:
click.echo("错误: 需要 Ed25519 私钥 (--ed25519-key)", err=True)
raise click.Abort()
if not rsa_private_pem:
click.echo("错误: 需要 RSA 私钥 (--rsa-key)", err=True)
raise click.Abort()
if not rsa_public_pem:
click.echo("错误: 需要 RSA 公钥 (--rsa-pub)", err=True)
raise click.Abort()
click.echo(f"打包插件: {plugin_path}")
click.echo(f"输出文件: {output}")
click.echo(f"签名者: {signer}")
try:
packer = NBPFPacker()
result = packer.pack(
plugin_dir=plugin_path,
output_path=Path(output),
ed25519_private_key=ed25519_private,
rsa_private_key_pem=rsa_private_pem,
rsa_public_key_pem=rsa_public_pem,
signer_name=signer,
)
click.echo(f"打包成功: {result}")
except Exception as e:
click.echo(f"打包失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command()
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
@click.argument('output_dir', type=click.Path(), default=None, required=False)
def unpack(nbpf_file, output_dir):
"""解包 .nbpf 文件到目录"""
from oss.core.nbpf import NBPFUnpacker
nbpf_path = Path(nbpf_file)
if not output_dir:
output_dir = nbpf_path.stem
click.echo(f"解包: {nbpf_path}")
click.echo(f"输出目录: {output_dir}")
try:
unpacker = NBPFUnpacker()
result = unpacker.unpack(nbpf_path, Path(output_dir))
click.echo(f"解包成功: {result}")
except Exception as e:
click.echo(f"解包失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command()
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
@click.option('--trusted-keys-dir', type=click.Path(exists=True), help='信任的 Ed25519 公钥目录')
def verify(nbpf_file, trusted_keys_dir):
"""验证 .nbpf 文件签名"""
from oss.core.nbpf import NBPFUnpacker
nbpf_path = Path(nbpf_file)
# 加载信任密钥
trusted_keys = {}
if trusted_keys_dir:
keys_path = Path(trusted_keys_dir)
for kf in keys_path.glob("*.pem"):
trusted_keys[kf.stem] = kf.read_bytes()
else:
# 尝试从默认目录加载
default_dir = Path("./data/nbpf-keys/trusted")
if default_dir.exists():
for kf in default_dir.glob("*.pem"):
trusted_keys[kf.stem] = kf.read_bytes()
if not trusted_keys:
click.echo("警告: 未加载任何信任密钥,将尝试提取 manifest 信息", err=True)
click.echo(f"验证: {nbpf_path}")
click.echo(f"信任密钥: {len(trusted_keys)}")
try:
unpacker = NBPFUnpacker()
manifest = unpacker.extract_manifest(nbpf_path)
click.echo(f"插件名称: {manifest.get('metadata', {}).get('name', '未知')}")
click.echo(f"版本: {manifest.get('metadata', {}).get('version', '未知')}")
click.echo(f"作者: {manifest.get('metadata', {}).get('author', '未知')}")
if trusted_keys:
valid, msg = unpacker.verify_signature(nbpf_path, trusted_keys)
if valid:
click.echo(f"签名验证: 通过 ({msg})")
else:
click.echo(f"签名验证: 失败 ({msg})", err=True)
raise click.Abort()
except Exception as e:
click.echo(f"验证失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command()
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
@click.option('--ed25519-key', type=click.Path(exists=True), help='Ed25519 私钥路径')
@click.option('--signer', default=None, help='签名者名称')
def sign(nbpf_file, ed25519_key, signer):
"""为 .nbpf 文件重新签名"""
from oss.core.nbpf import NBPFPacker, NBPFUnpacker
nbpf_path = Path(nbpf_file)
if not ed25519_key:
click.echo("错误: 需要 Ed25519 私钥 (--ed25519-key)", err=True)
raise click.Abort()
ed25519_private = Path(ed25519_key).read_bytes()
click.echo(f"重新签名: {nbpf_path}")
try:
# 解包
temp_dir = nbpf_path.parent / f".{nbpf_path.stem}_tmp"
if temp_dir.exists():
import shutil
shutil.rmtree(temp_dir)
NBPFUnpacker().unpack(nbpf_path, temp_dir)
# 重新打包
packer = NBPFPacker()
result = packer.pack(
plugin_dir=temp_dir,
output_path=nbpf_path,
ed25519_private_key=ed25519_private,
rsa_private_key_pem=None,
rsa_public_key_pem=None,
signer_name=signer or "resign",
)
click.echo(f"重新签名成功: {result}")
# 清理临时目录
import shutil
shutil.rmtree(temp_dir)
except Exception as e:
click.echo(f"重新签名失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command(name="keygen")
@click.option('--output-dir', type=click.Path(), default='./data/nbpf-keys', help='密钥输出目录')
@click.option('--name', default='default', help='密钥名称')
def keygen(output_dir, name):
"""生成 Ed25519 + RSA 密钥对"""
from oss.core.nbpf import NBPCrypto
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# 创建子目录
trusted_dir = output_path / "trusted"
rsa_dir = output_path / "rsa"
private_dir = output_path / "private"
trusted_dir.mkdir(exist_ok=True)
rsa_dir.mkdir(exist_ok=True)
private_dir.mkdir(exist_ok=True)
click.echo(f"生成密钥对到: {output_path}")
# 生成 Ed25519 密钥对
click.echo("生成 Ed25519 密钥对...")
ed25519_private, ed25519_public = NBPCrypto.generate_ed25519_keypair()
(trusted_dir / f"{name}.pem").write_bytes(ed25519_public)
(private_dir / f"{name}_ed25519.pem").write_bytes(ed25519_private)
click.echo(f" Ed25519 公钥: {trusted_dir / f'{name}.pem'}")
click.echo(f" Ed25519 私钥: {private_dir / f'{name}_ed25519.pem'}")
# 生成 RSA 密钥对
click.echo("生成 RSA-4096 密钥对(可能需要几秒钟)...")
rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=4096)
(rsa_dir / f"{name}.pem").write_bytes(rsa_public)
(private_dir / f"{name}_rsa.pem").write_bytes(rsa_private)
click.echo(f" RSA 公钥: {rsa_dir / f'{name}.pem'}")
click.echo(f" RSA 私钥: {private_dir / f'{name}_rsa.pem'}")
click.echo("密钥生成完成!")
click.echo("")
click.echo("使用示例:")
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}")
def main():
cmd_name = os.path.basename(sys.argv[0])
if cmd_name in ("oss", "oss.exe"):
Log.warn("NebulaShell", "oss 命令已弃用,请使用 nebula 替代")
sys.exit(1)
if _handle_hidden_command():
return
cli()
if __name__ == "__main__":
main()