feat: 新增脚手架/开发模式/权限白名单/system-monitor插件
- 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:
561
README.md
561
README.md
@@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/NebulaShell-v2.0-6C47FF?style=for-the-badge&logo=python&logoColor=white&labelColor=1a1a2e">
|
||||
<img alt="NebulaShell" src="https://img.shields.io/badge/NebulaShell-v2.0-6C47FF?style=for-the-badge&logo=python&logoColor=white&labelColor=f0f0ff">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/NebulaShell-v1.2.0-6C47FF?style=for-the-badge&logo=python&logoColor=white&labelColor=1a1a2e">
|
||||
<img alt="NebulaShell" src="https://img.shields.io/badge/NebulaShell-v1.2.0-6C47FF?style=for-the-badge&logo=python&logoColor=white&labelColor=f0f0ff">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache--2.0-6C47FF" alt="License"></a>
|
||||
<a href=""><img src="https://img.shields.io/badge/Build-Passing-22C55E" alt="Build"></a>
|
||||
<a href=""><img src="https://img.shields.io/badge/Coverage-92%25-22C55E" alt="Coverage"></a>
|
||||
<a href=""><img src="https://img.shields.io/badge/Security-AES--256--GCM%20%7C%20Ed25519%20%7C%20RSA--4096-EF4444" alt="Security"></a>
|
||||
</p>
|
||||
|
||||
@@ -24,16 +23,13 @@
|
||||
## 目录
|
||||
|
||||
- [项目定位](#项目定位)
|
||||
- [架构总览](#架构总览)
|
||||
- [核心能力](#核心能力)
|
||||
- [快速开始](#快速开始)
|
||||
- [NBPF 包格式](#nbpf-包格式)
|
||||
- [NIR 中间表示](#nir-中间表示)
|
||||
- [CLI 工具链](#cli-工具链)
|
||||
- [插件开发](#插件开发)
|
||||
- [权限白名单](#权限白名单manifestpermissionsimports)
|
||||
- [NBPF 包格式](#nbpf-包格式)
|
||||
- [内置插件](#内置插件)
|
||||
- [安全体系](#安全体系)
|
||||
- [性能指标](#性能指标)
|
||||
- [贡献指南](#贡献指南)
|
||||
- [许可证](#许可证)
|
||||
|
||||
@@ -43,374 +39,357 @@
|
||||
|
||||
NebulaShell 是一个**以安全为基石、以插件为灵魂**的运行时框架。核心只做两件事:**加载插件**与**调度插件**,其余一切功能均由插件生态提供。
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/philosophy.svg" alt="NebulaShell Philosophy" width="600">
|
||||
</p>
|
||||
|
||||
### 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **最小核心** | 核心仅 1100+ 行,职责单一,可独立审计 |
|
||||
| **插件即产品** | 所有业务功能以插件形式交付,核心不耦合任何业务 |
|
||||
| **安全默认** | 插件分发强制签名加密,运行时隔离,防篡改防逆向 |
|
||||
| **一次编译** | NIR 中间表示确保插件跨平台运行,无需为架构适配 |
|
||||
| **零信任分发** | 每个包经过三层签名验证 + 两层加密解密才可加载 |
|
||||
|
||||
---
|
||||
|
||||
## 架构总览
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/architecture.svg" alt="NebulaShell Architecture" width="800">
|
||||
</p>
|
||||
|
||||
### 分层架构
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/layers.svg" alt="NebulaShell Layers" width="800">
|
||||
</p>
|
||||
|
||||
### 数据流
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/dataflow.svg" alt="NebulaShell Data Flow" width="800">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 核心能力
|
||||
|
||||
### 插件化架构
|
||||
|
||||
- **热插拔**:插件可在运行时动态加载/卸载,无需重启
|
||||
- **依赖注入**:通过 `use()` 获取任意已加载插件实例
|
||||
- **生命周期管理**:`init → start → stop` 三阶段标准化生命周期
|
||||
- **优先级控制**:支持 `load_priority` 标记控制加载顺序
|
||||
- **熔断降级**:插件异常自动隔离,不影响核心运行
|
||||
|
||||
### NBPF 包格式
|
||||
|
||||
- **多重签名**:Ed25519(外层)→ RSA-4096-PSS(中层)→ HMAC-SHA256(内层)
|
||||
- **多重加密**:AES-256-GCM 双层加密,密钥经 RSA-OAEP 封装
|
||||
- **代码隐藏**:混淆导入路径、常量运行时计算、反调试检测、内存擦除、花指令混淆
|
||||
- **防篡改**:任何字节级别的修改都会导致签名验证失败
|
||||
|
||||
### NIR 中间表示
|
||||
|
||||
- **一次编译,到处运行**:基于 Python `compile()` + `marshal` 序列化
|
||||
- **跨平台**:code object 是 Python 虚拟机原生格式,与 CPU 架构无关
|
||||
- **目标版本**:Python 3.10+
|
||||
- **代码保护**:编译产物不可读,增加逆向难度
|
||||
|
||||
### CLI 工具链
|
||||
|
||||
- **`nebula nbpf`**:完整的包管理命令组
|
||||
- **密钥生成**:一键生成 Ed25519 + RSA-4096 密钥对
|
||||
- **打包/解包**:插件目录 ↔ .nbpf 文件双向转换
|
||||
- **验证/签名**:独立验证工具 + 重新签名能力
|
||||
|:----|:-----|
|
||||
| 🧩 **一切皆插件** | 框架本身只提供加载和调度能力,所有功能都来自插件 |
|
||||
| 🔒 **安全默认** | 沙箱执行、签名验证、权限声明、完整性校验,层层防护 |
|
||||
| 📦 **一次编译到处运行** | NIR 中间表示使插件可在任何 Python 3.10+ 平台运行 |
|
||||
| 🎯 **最小权限** | 插件必须显式声明所需权限,未授权操作被拒绝 |
|
||||
| 🔧 **开发者体验** | 脚手架生成、热重载、详细日志,让开发尽可能愉快 |
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/Starlight-apk/NebulaShell.git
|
||||
cd NebulaShell
|
||||
### 一键启动(推荐)
|
||||
|
||||
```bash
|
||||
# 非交互式启动(适合 Docker / CI / 后台服务)
|
||||
python headless.py
|
||||
|
||||
# 自动信任所有 NBPF 插件
|
||||
python headless.py --trust-all
|
||||
|
||||
# 仅检查运行环境
|
||||
python headless.py --dry-run
|
||||
```
|
||||
|
||||
### 手动启动
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动 NebulaShell
|
||||
python main.py
|
||||
# 开发模式(热重载,推荐开发时使用)
|
||||
python main.py dev
|
||||
|
||||
# 生产模式(交互式 REPL)
|
||||
python main.py serve
|
||||
|
||||
# 生产模式(非交互,适合 Docker)
|
||||
python main.py serve --headless
|
||||
```
|
||||
|
||||
启动后访问 [http://localhost:8080](http://localhost:8080) 进入管理控制台。
|
||||
|
||||
### 生成密钥并打包一个插件
|
||||
### 快速创建第一个模组
|
||||
|
||||
```bash
|
||||
# 1. 生成密钥对
|
||||
nebula nbpf keygen --output ./nbpf-keys
|
||||
# 一行命令创建模组脚手架
|
||||
python main.py create mod hello-world -a "我" -d "我的第一个模组"
|
||||
|
||||
# 2. 打包插件为 .nbpf
|
||||
nebula nbpf pack ./store/NebulaShell/my-plugin -o my-plugin.nbpf \
|
||||
--ed25519-key ./nbpf-keys/private/ed25519.pem \
|
||||
--rsa-key ./nbpf-keys/private/rsa.pem
|
||||
# 编辑功能
|
||||
cd hello-world
|
||||
# vim main.py # 实现你的功能
|
||||
|
||||
# 3. 验证包完整性
|
||||
nebula nbpf verify my-plugin.nbpf
|
||||
# 生成签名密钥
|
||||
python main.py create key -o ./keys --name mykey
|
||||
|
||||
# 4. 将密钥放入信任目录
|
||||
cp ./nbpf-keys/trusted/* ./data/nbpf-keys/trusted/
|
||||
cp ./nbpf-keys/rsa/* ./data/nbpf-keys/rsa/
|
||||
# 打包为 .nbpf
|
||||
python main.py nbpf pack ./hello-world -o mods/hello-world.nbpf \
|
||||
--ed25519-key ./keys/mykey_ed25519.pem \
|
||||
--rsa-key ./keys/mykey_rsa.pem \
|
||||
--rsa-pub ./keys/mykey_rsa.pub.pem \
|
||||
--signer "我"
|
||||
|
||||
# 5. 重启 NebulaShell,插件自动加载
|
||||
# 启动开发模式测试
|
||||
python main.py dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NBPF 包格式
|
||||
|
||||
### 包结构
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/package-structure.svg" alt="NBPF Package Structure" width="500">
|
||||
</p>
|
||||
|
||||
### 加密层级
|
||||
|
||||
| 层级 | 算法 | 密钥来源 | 保护范围 |
|
||||
|------|------|----------|----------|
|
||||
| 外层加密 | AES-256-GCM | key1(RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 |
|
||||
| 中层加密 | AES-256-GCM | key2(RSA-OAEP 封装) | NIR 数据内容 |
|
||||
| 外层签名 | Ed25519 | 开发者私钥 | 加密层完整性 |
|
||||
| 中层签名 | RSA-4096-PSS | 作者私钥 | 模块内容完整性 |
|
||||
| 内层签名 | HMAC-SHA256 | 派生密钥(key1+key2) | 单个模块完整性 |
|
||||
|
||||
### 安全流程
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/security-flow.svg" alt="NBPF Security Flow" width="700">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## NIR 中间表示
|
||||
|
||||
NIR(Nebula Intermediate Representation)是 NebulaShell 的跨平台编译方案。
|
||||
|
||||
### 技术原理
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/nir-flow.svg" alt="NIR Compilation Flow" width="600">
|
||||
</p>
|
||||
|
||||
### 代码保护
|
||||
|
||||
| 技术 | 说明 |
|
||||
|------|------|
|
||||
| 混淆导入路径 | 动态 `__import__()` + 字符串拼接,隐藏依赖关系 |
|
||||
| 常量运行时计算 | 关键字符串在运行时拼接,避免静态分析 |
|
||||
| 反调试检测 | `sys.gettrace()` 检测调试器附加 |
|
||||
| 内存擦除 | `bytearray` 覆盖清零,防止内存 dump |
|
||||
| 花指令混淆 | 向 `co_consts` 插入无害垃圾常量,干扰分析 |
|
||||
> 默认 HTTP 服务端口:**10086**(可在 `data/config.json` 或环境变量 `HTTP_API_PORT` 中修改)
|
||||
|
||||
---
|
||||
|
||||
## CLI 工具链
|
||||
|
||||
| 命令 | 说明 |
|
||||
|:----|:------|
|
||||
| `serve` | 启动 NebulaShell 服务端 |
|
||||
| `serve --headless` | 🆕 非交互模式(不启动 REPL,适合 Docker/后台) |
|
||||
| `dev` | 🆕 **开发模式** — 监听文件变化 + 自动热重载 |
|
||||
| `version` | 显示版本信息 |
|
||||
| `info` | 显示系统信息 |
|
||||
| `create mod` | 🆕 **模组脚手架** — 交互式生成插件骨架 |
|
||||
| `create key` | 🆕 快速生成 Ed25519 + RSA 签名密钥对 |
|
||||
| `create list-templates` | 🆕 查看可用模板 |
|
||||
| `nbpf pack` | 打包插件目录为 .nbpf 文件 |
|
||||
| `nbpf unpack` | 解包 .nbpf 文件到目录 |
|
||||
| `nbpf verify` | 验证 .nbpf 签名 |
|
||||
| `nbpf sign` | 为 .nbpf 重新签名 |
|
||||
| `nbpf keygen` | 生成 Ed25519 + RSA-4096 密钥对 |
|
||||
|
||||
### 典型用法
|
||||
|
||||
```bash
|
||||
# 密钥管理
|
||||
nebula nbpf keygen # 生成 Ed25519 + RSA-4096 密钥对
|
||||
nebula nbpf keygen --output ./keys # 指定输出目录
|
||||
# 🚀 快速创建模组(脚手架)
|
||||
nebula create mod my-plugin -a "我" -d "插件描述"
|
||||
nebula create mod my-service --type service --with-keys
|
||||
|
||||
# 打包
|
||||
nebula nbpf pack ./plugin-dir # 打包为 .nbpf
|
||||
nebula nbpf pack ./plugin-dir -o out.nbpf --keys-dir ./keys
|
||||
# 🔑 生成签名密钥
|
||||
nebula create key -o ./keys --name mykey
|
||||
|
||||
# 解包
|
||||
nebula nbpf unpack package.nbpf # 解包到目录
|
||||
nebula nbpf unpack package.nbpf -o ./out
|
||||
# 💻 开发模式(热重载)
|
||||
nebula dev # 监听当前目录
|
||||
nebula dev ./my-plugin --port 10086 # 监听指定目录
|
||||
nebula dev --skip-sign # 跳过签名验证(调试用)
|
||||
|
||||
# 验证
|
||||
nebula nbpf verify package.nbpf # 验证完整签名链
|
||||
# 🚀 启动服务
|
||||
nebula serve
|
||||
nebula serve --headless # 非交互模式
|
||||
|
||||
# 重新签名
|
||||
nebula nbpf sign package.nbpf # 使用新密钥重新签名
|
||||
nebula nbpf sign package.nbpf --ed25519-key ./key.pem --rsa-key ./rsa.pem
|
||||
# 📦 打包插件
|
||||
nebula nbpf pack ./my-plugin -o my-plugin.nbpf \
|
||||
--ed25519-key ./keys/ed25519.pem \
|
||||
--rsa-key ./keys/rsa.pem \
|
||||
--rsa-pub ./keys/rsa.pub.pem \
|
||||
--signer "作者名"
|
||||
|
||||
# ℹ️ 系统信息
|
||||
nebula info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 插件开发
|
||||
|
||||
### 最小插件
|
||||
### 模组结构(目录模式)
|
||||
|
||||
```python
|
||||
from oss.plugin.types import Plugin
|
||||
|
||||
class HelloPlugin(Plugin):
|
||||
def init(self, deps=None):
|
||||
self.name = "hello"
|
||||
|
||||
def start(self):
|
||||
print(f"{self.name} started")
|
||||
|
||||
def stop(self):
|
||||
print(f"{self.name} stopped")
|
||||
|
||||
def New():
|
||||
return HelloPlugin()
|
||||
```
|
||||
my-plugin/
|
||||
├── manifest.json ← 【必填】模组身份证
|
||||
├── main.py ← 【必填】模组代码(类 + New() 工厂函数)
|
||||
└── README.md ← 【推荐】说明文档
|
||||
```
|
||||
|
||||
### 清单文件
|
||||
### main.py 规范
|
||||
|
||||
插件支持两种写法,**推荐使用类 + New() 工厂函数**以兼容目录和 nbpf 两种加载方式:
|
||||
|
||||
```python
|
||||
class MyPlugin:
|
||||
name = "my-plugin"
|
||||
version = "1.0.0"
|
||||
description = "描述"
|
||||
|
||||
def init(self, deps=None): # 初始化,可获取依赖
|
||||
pass
|
||||
def start(self): # 启动
|
||||
pass
|
||||
def stop(self): # 停止,释放资源
|
||||
pass
|
||||
def reload(self, config): # 热重载(可选)
|
||||
pass
|
||||
def health(self) -> dict: # 健康检查(可选)
|
||||
return {"status": "ok"}
|
||||
|
||||
def New():
|
||||
"""目录插件工厂函数(必需)"""
|
||||
return MyPlugin()
|
||||
```
|
||||
|
||||
### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "hello-plugin",
|
||||
"name": "@作者/模组名",
|
||||
"version": "1.0.0",
|
||||
"description": "示例插件",
|
||||
"author": "developer"
|
||||
},
|
||||
"config": {
|
||||
"description": "描述",
|
||||
"author": "作者",
|
||||
"type": "example",
|
||||
"main": "main.py",
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
"priority": 999,
|
||||
"runtime": {
|
||||
"language": "python",
|
||||
"entry_point": "main.py",
|
||||
"requirements": []
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": ["storage:read"]
|
||||
"permissions": {
|
||||
"imports": [] ← 声明的系统模块导入权限
|
||||
},
|
||||
"services": {
|
||||
"provides": [],
|
||||
"consumes": []
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用其他插件
|
||||
|
||||
```python
|
||||
from store.NebulaShell.plugin_bridge.main import use
|
||||
|
||||
# 获取 HTTP API 插件实例
|
||||
http_api = use("http-api")
|
||||
|
||||
# 注册路由
|
||||
http_api.add_route("/hello", lambda: {"message": "world"})
|
||||
```
|
||||
|
||||
### 打包分发
|
||||
### 开发流程
|
||||
|
||||
```bash
|
||||
# 开发阶段:源码直接放入 store/ 目录
|
||||
# 分发阶段:打包为 .nbpf
|
||||
nebula nbpf pack ./store/NebulaShell/hello-plugin -o hello-plugin.nbpf \
|
||||
--ed25519-key ./nbpf-keys/private/ed25519.pem \
|
||||
--rsa-key ./nbpf-keys/private/rsa.pem
|
||||
# 1. 使用脚手架创建模组(推荐)
|
||||
nebula create mod my-plugin -a "我" -d "描述"
|
||||
|
||||
# 2. 编辑 main.py 实现功能
|
||||
cd my-plugin
|
||||
# vim main.py ...
|
||||
|
||||
# 3. 开发模式:边改边测,自动热重载
|
||||
nebula dev ./my-plugin
|
||||
|
||||
# 4. 满意后生成签名密钥
|
||||
nebula create key -o ./keys --name mykey
|
||||
|
||||
# 5. 打包为 .nbpf 发布
|
||||
nebula nbpf pack ./my-plugin -o mods/my-plugin.nbpf \\
|
||||
--ed25519-key ./keys/mykey_ed25519.pem \\
|
||||
--rsa-key ./keys/mykey_rsa.pem \\
|
||||
--rsa-pub ./keys/mykey_rsa.pub.pem \\
|
||||
--signer "我"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 权限白名单(manifest.permissions.imports)
|
||||
|
||||
NBPF 安全沙箱默认禁止导入 `os`、`threading`、`socket` 等系统模块。
|
||||
如果插件确实需要,**必须在 manifest.json 的 `permissions.imports` 中显式声明**。
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"imports": ["os", "threading", "json", "http"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 声明方式 | 效果 |
|
||||
|:---------|:-----|
|
||||
| 不声明 `permissions` | 仅限纯 Python 计算,不可导入任何系统模块 |
|
||||
| `"imports": ["os", "json"]` | 只允许导入 `os` 和 `json` |
|
||||
| 使用未声明的模块 | **编译时报错**,拒绝打包 |
|
||||
| 运行时突破白名单 | **安全沙箱拦截**,抛出 `ImportError` |
|
||||
|
||||
> **安全原则**:最小权限。只声明你真正需要的模块,不要图省事全写上。
|
||||
|
||||
---
|
||||
|
||||
## NBPF 包格式
|
||||
|
||||
`.nbpf` 是 NebulaShell 的插件分发格式,本质上是一个多层加密签名的 ZIP 包。
|
||||
|
||||
### 包结构
|
||||
|
||||
```
|
||||
my-plugin.nbpf
|
||||
├── META-INF/
|
||||
│ ├── SIGNATURE ← 外层 Ed25519 签名
|
||||
│ ├── SIGNER.PEM ← 签名者 Ed25519 公钥
|
||||
│ ├── ENCRYPTION ← 外层加密信息(RSA 加密 AES 密钥)
|
||||
│ ├── INNER_SIGNATURE ← 中层 RSA-4096 签名
|
||||
│ ├── INNER_ENCRYPTION ← 中层加密信息
|
||||
│ ├── MODULE_SIGS ← 内层 HMAC 模块签名
|
||||
│ └── PLUGIN.MF ← 插件清单
|
||||
├── NIR/ ← 编译后的 NIR 中间表示
|
||||
│ └── main ← 主模块序列化 code object
|
||||
└── RES/ ← 资源文件(README、图片等)
|
||||
```
|
||||
|
||||
### 安全层级
|
||||
|
||||
```
|
||||
外层: Ed25519 签名 → 验证包完整性 & 作者身份
|
||||
↓
|
||||
中层: RSA-4096 签名 → 验证 NIR 数据完整性
|
||||
↓
|
||||
内层: HMAC 签名 → 验证每个模块未被篡改
|
||||
↓
|
||||
执行: 安全沙箱 + 权限白名单 → 限制运行时能力
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 内置插件
|
||||
|
||||
NebulaShell 内置 26+ 个插件,覆盖 Web 服务、系统管理、安全防护、协议适配等场景。
|
||||
|
||||
### Web 与 API
|
||||
NebulaShell 内置了以下官方插件(位于 `store/NebulaShell/`):
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `http-api` | RESTful API 服务,支持路由注册、中间件 |
|
||||
| `ws-api` | WebSocket 实时通信服务 |
|
||||
| `webui` | 管理控制台 Web 界面 |
|
||||
| `dashboard` | 系统仪表盘,实时监控 |
|
||||
|
||||
### 系统管理
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `plugin-loader` | 插件加载核心,manifest 解析 |
|
||||
| `plugin-loader-pro` | 熔断、降级、容错机制 |
|
||||
| `pkg-manager` | 插件包管理器,在线安装/更新 |
|
||||
| `lifecycle` | 插件生命周期管理 |
|
||||
| `hot-reload` | 插件热重载,开发模式自动刷新 |
|
||||
| `dependency` | 依赖关系解析与冲突检测 |
|
||||
|
||||
### 安全防护
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `signature-verifier` | 运行时签名验证 |
|
||||
| `code-reviewer` | 插件代码安全审查 |
|
||||
| `firewall` | 网络防火墙规则引擎 |
|
||||
|
||||
### 通信与协议
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `plugin-bridge` | 插件间通信(事件总线 / RPC / use()) |
|
||||
| `http-tcp` | TCP 协议适配 |
|
||||
|:----|:------|
|
||||
| `plugin-bridge` | 事件总线 + 服务注册 + 跨插件 RPC |
|
||||
| `plugin-storage` | 插件持久化 KV 存储 |
|
||||
| `ws-api` | WebSocket 实时推送 |
|
||||
| `i18n` | 国际化多语言支持 |
|
||||
| `nodejs-adapter` | Node.js 运行时适配 |
|
||||
| `frp-proxy` | 内网穿透代理 |
|
||||
| `ftp-server` | 文件服务 |
|
||||
| `system-monitor` 🆕 | **系统监控仪表盘** — CPU/内存/磁盘/网络/进程TOP |
|
||||
| | 提供 HTML 仪表盘 + REST API,默认端口 **10087** |
|
||||
|
||||
### 工具与增强
|
||||
### system-monitor API
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `plugin-storage` | 插件持久化存储 |
|
||||
| `i18n` | 国际化支持 |
|
||||
| `auto-dependency` | 系统依赖自动安装 |
|
||||
| `performance-optimizer` | 性能优化引擎 |
|
||||
| `json-codec` | JSON 编解码 |
|
||||
| `log-terminal` | 日志查看与终端 |
|
||||
| `polyglot-deploy` | 多语言部署支持 |
|
||||
```
|
||||
http://localhost:10087/ → 📊 仪表盘 HTML
|
||||
http://localhost:10087/health → 💚 {"status": "ok"}
|
||||
http://localhost:10087/stats → 📄 系统状态 JSON
|
||||
http://localhost:10087/stats/cpu → 🧠 CPU 详情
|
||||
http://localhost:10087/stats/memory → 💾 内存详情
|
||||
http://localhost:10087/stats/disk → 💿 磁盘详情
|
||||
http://localhost:10087/stats/network → 🌐 网络详情
|
||||
http://localhost:10087/stats/processes → ⚡ TOP 进程
|
||||
http://localhost:10087/stats/history → 📈 历史数据(最近60条)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全体系
|
||||
|
||||
### 全链路安全
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/security-chain.svg" alt="Full Chain Security" width="800">
|
||||
</p>
|
||||
|
||||
### 加密标准
|
||||
|
||||
| 组件 | 标准 | 密钥长度 |
|
||||
|------|------|----------|
|
||||
| 对称加密 | AES-256-GCM | 256 位 |
|
||||
| 非对称加密 | RSA-OAEP | 4096 位 |
|
||||
| 外层签名 | Ed25519 | 256 位 |
|
||||
| 中层签名 | RSA-PSS | 4096 位 |
|
||||
| 内层签名 | HMAC-SHA256 | 256 位 |
|
||||
|
||||
### 密钥管理
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/key-structure.svg" alt="Key Management Structure" width="500">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 性能指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 核心代码行数 | ~1,100 行 |
|
||||
| 内置插件数量 | 26+ |
|
||||
| 测试覆盖率 | ~92% |
|
||||
| 语法检查通过率 | 100% |
|
||||
| Python 版本要求 | 3.10+ |
|
||||
| 依赖库数量 | 精简(核心仅依赖 cryptography) |
|
||||
| 层级 | 措施 | 说明 |
|
||||
|:----|:-----|:------|
|
||||
| 🛡 **分发安全** | 三层签名(Ed25519 + RSA-4096 + HMAC) | 防止包被篡改或伪造 |
|
||||
| 🔐 **传输安全** | AES-256-GCM 双层加密 | 插件代码在传输和存储中均加密 |
|
||||
| 🏖 **执行沙箱** | NIR 安全沙箱 | 限制危险模块和内置函数 |
|
||||
| 📋 **权限控制** | manifest 权限白名单 | 插件必须声明所需模块导入权限 |
|
||||
| 🔍 **完整性检查** | SHA-256 文件 hash 监控 | 运行时定期校验插件文件完整性 |
|
||||
| 🧠 **内存防护** | MemoryGuard 冻结核心属性 | 防止插件修改框架内部状态 |
|
||||
| 📝 **行为审计** | AuditLogger | 记录所有插件操作行为 |
|
||||
| 👀 **防篡改监控** | TamperMonitor 后台线程 | 自动检测篡改并停止被篡改插件 |
|
||||
| 🔄 **降级恢复** | FallbackManager 自动重试 | 插件崩溃时自动重启(最多3次) |
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
### 开发流程
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 1. Fork 仓库
|
||||
# 2. 创建特性分支
|
||||
git checkout -b feat/my-feature
|
||||
# 克隆仓库
|
||||
git clone https://git.starlight-apk.cn/starlight-apk/NebulaShell.git
|
||||
cd NebulaShell
|
||||
|
||||
# 3. 安装开发依赖
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 4. 确保语法检查通过
|
||||
find . -name "*.py" -not -path "./venv/*" -not -path "./.git/*" | \
|
||||
xargs -I{} python3 -m py_compile {}
|
||||
# 启动开发模式
|
||||
python main.py dev
|
||||
|
||||
# 5. 运行测试
|
||||
# 运行测试
|
||||
python -m pytest tests/
|
||||
|
||||
# 6. 提交 PR
|
||||
```
|
||||
|
||||
### 贡献流程
|
||||
|
||||
1. Fork 项目并创建特性分支
|
||||
2. 编写代码,确保语法检查零错误
|
||||
3. 添加或更新测试
|
||||
4. 更新文档(README、注释等)
|
||||
5. 提交 Pull Request
|
||||
|
||||
### 代码规范
|
||||
|
||||
- 遵循 PEP 8 编码规范
|
||||
- 所有插件必须实现 `init()`、`start()`、`stop()` 方法
|
||||
- 插件清单必须包含完整的元数据和权限声明
|
||||
- 提交前确保语法检查零错误
|
||||
- 插件必须实现 `init()`、`start()`、`stop()` 方法
|
||||
- 插件必须包含 `New()` 工厂函数(兼容目录 + nbpf 两种加载方式)
|
||||
- 插件必须声明完整的 `permissions.imports` 权限白名单
|
||||
- 提交前确保所有测试通过
|
||||
|
||||
---
|
||||
|
||||
|
||||
302
oss/cli.py
302
oss/cli.py
@@ -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"):
|
||||
|
||||
@@ -16,8 +16,8 @@ class Config:
|
||||
|
||||
DEFAULTS = {
|
||||
# 服务器配置
|
||||
"HTTP_API_PORT": 8080,
|
||||
"HTTP_TCP_PORT": 8082,
|
||||
"HTTP_API_PORT": 10086,
|
||||
"HTTP_TCP_PORT": 10086,
|
||||
"HOST": "127.0.0.1",
|
||||
|
||||
# 数据目录
|
||||
|
||||
@@ -29,7 +29,7 @@ class HttpServer:
|
||||
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.port = port or config.get("HTTP_API_PORT", 10086)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self._server = None
|
||||
|
||||
@@ -154,11 +154,16 @@ class PluginManager:
|
||||
name = kf.stem
|
||||
trusted_rsa[name] = kf.read_bytes()
|
||||
|
||||
# 加载 RSA 私钥
|
||||
# 加载 RSA 私钥(只匹配名称包含 rsa 的文件,避免误读 Ed25519 私钥)
|
||||
rsa_private = None
|
||||
private_dir = Path(config.get("NBPF_KEYS_DIR", "./data/nbpf-keys")) / "private"
|
||||
if private_dir.exists():
|
||||
pk_files = [f for f in private_dir.glob("*.pem") if "rsa" in f.name.lower()]
|
||||
if not pk_files:
|
||||
# 回退:匹配任意私钥(警告日志)
|
||||
pk_files = list(private_dir.glob("*.pem"))
|
||||
if pk_files:
|
||||
Log.warn("Core", "未找到名称包含 'rsa' 的私钥文件,尝试加载第一个 .pem 文件(可能导致类型错误)")
|
||||
if pk_files:
|
||||
rsa_private = pk_files[0].read_bytes()
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class NIRCompiler:
|
||||
|
||||
# ── 编译 ──
|
||||
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>") -> bytes:
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>", allowed_imports: list[str] = None) -> bytes:
|
||||
"""将 Python 源码编译为序列化的 code object
|
||||
|
||||
Args:
|
||||
@@ -61,7 +61,7 @@ class NIRCompiler:
|
||||
"""
|
||||
try:
|
||||
# 静态安全检查
|
||||
self._static_check(source, filename)
|
||||
self._static_check(source, filename, allowed_imports or [])
|
||||
|
||||
# 编译为 code object
|
||||
code = compile(source, filename, 'exec')
|
||||
@@ -79,11 +79,12 @@ class NIRCompiler:
|
||||
except Exception as e:
|
||||
raise NIRCompileError(f"编译失败: {type(e).__name__}: {e}") from e
|
||||
|
||||
def compile_plugin(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||
def compile_plugin(self, plugin_dir: Path, allowed_imports: list[str] = None) -> dict[str, bytes]:
|
||||
"""编译整个插件目录为 NIR
|
||||
|
||||
Args:
|
||||
plugin_dir: 插件目录路径
|
||||
allowed_imports: 允许导入的系统模块白名单(来自 manifest permissions.imports)
|
||||
|
||||
Returns:
|
||||
{module_name: nir_bytes} 字典
|
||||
@@ -105,7 +106,7 @@ class NIRCompiler:
|
||||
module_name = rel_path.replace(".py", "").replace("/", ".")
|
||||
if module_name.endswith(".__init__"):
|
||||
module_name = module_name[:-9] # 去掉 .__init__
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path))
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path), allowed_imports)
|
||||
|
||||
return nir_data
|
||||
|
||||
@@ -163,7 +164,7 @@ class NIRCompiler:
|
||||
|
||||
# ── 静态安全检查 ──
|
||||
|
||||
def _static_check(self, source: str, filename: str):
|
||||
def _static_check(self, source: str, filename: str, allowed_imports: list[str] = None):
|
||||
"""静态源码安全检查"""
|
||||
try:
|
||||
tree = ast.parse(source, filename=filename)
|
||||
@@ -174,12 +175,12 @@ class NIRCompiler:
|
||||
# 检查 import 语句
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
self._check_module(alias.name, node.lineno)
|
||||
self._check_module(alias.name, node.lineno, allowed_imports)
|
||||
|
||||
# 检查 from ... import 语句
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
self._check_module(node.module, node.lineno)
|
||||
self._check_module(node.module, node.lineno, allowed_imports)
|
||||
|
||||
# 检查 __import__ 调用
|
||||
elif isinstance(node, ast.Call):
|
||||
@@ -196,12 +197,16 @@ class NIRCompiler:
|
||||
f"{filename}:{node.lineno} - 禁止使用 {node.func.id}()"
|
||||
)
|
||||
|
||||
def _check_module(self, module_name: str, lineno: int):
|
||||
"""检查模块是否被禁止"""
|
||||
def _check_module(self, module_name: str, lineno: int, allowed_imports: list[str] = None):
|
||||
"""检查模块是否被禁止(支持白名单豁免)"""
|
||||
base = module_name.split(".")[0]
|
||||
if base in self.FORBIDDEN_MODULES:
|
||||
# 检查是否在白名单中
|
||||
if allowed_imports and base in allowed_imports:
|
||||
return # 白名单放行
|
||||
raise NIRCompileError(
|
||||
f"第 {lineno} 行 - 禁止导入系统模块: '{module_name}'"
|
||||
f"(如需使用请在 manifest.json 的 permissions.imports 中声明)"
|
||||
)
|
||||
|
||||
def _reject_c_extensions(self, plugin_dir: Path):
|
||||
|
||||
@@ -114,9 +114,16 @@ class NBPFPacker:
|
||||
# 1. 读取 manifest
|
||||
manifest = self._read_manifest(plugin_dir)
|
||||
|
||||
# 2. 编译所有 .py 文件为 NIR
|
||||
# 2. 编译所有 .py 文件为 NIR(传入 manifest 权限白名单)
|
||||
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir)
|
||||
perms = manifest.get("permissions", {})
|
||||
if isinstance(perms, dict):
|
||||
allowed_imports = perms.get("imports", [])
|
||||
else:
|
||||
allowed_imports = [] # 旧的数组格式,不开放系统模块
|
||||
if allowed_imports:
|
||||
Log.info("NBPF", f"已授权导入: {allowed_imports}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir, allowed_imports=allowed_imports)
|
||||
|
||||
# 3. 收集资源文件
|
||||
res_files = self._collect_resources(plugin_dir)
|
||||
|
||||
@@ -55,6 +55,7 @@ class NBPFLoader:
|
||||
self.trusted_ed25519_keys = trusted_ed25519_keys or {}
|
||||
self.trusted_rsa_keys = trusted_rsa_keys or {}
|
||||
self.rsa_private_key = rsa_private_key
|
||||
self._current_allowed_imports: list[str] = []
|
||||
|
||||
def load(
|
||||
self,
|
||||
@@ -112,7 +113,12 @@ class NBPFLoader:
|
||||
meta = manifest.get("metadata", {})
|
||||
name = plugin_name or meta.get("name", nbpf_path.stem)
|
||||
|
||||
# 9. 反序列化并执行
|
||||
# 9. 反序列化并执行(传入 imports 白名单)
|
||||
perms = manifest.get("permissions", {})
|
||||
if isinstance(perms, dict):
|
||||
self._current_allowed_imports = perms.get("imports", [])
|
||||
else:
|
||||
self._current_allowed_imports = []
|
||||
instance, module = self._deserialize_and_exec(nir_data, name)
|
||||
|
||||
# 10. 构建插件信息
|
||||
@@ -326,7 +332,7 @@ class NBPFLoader:
|
||||
"""反序列化 NIR 并执行,返回 (instance, module)"""
|
||||
|
||||
# 构建安全的全局命名空间
|
||||
safe_globals = self._build_safe_globals(plugin_name)
|
||||
safe_globals = self._build_safe_globals(plugin_name, self._current_allowed_imports)
|
||||
|
||||
# 按依赖顺序执行模块
|
||||
main_module = None
|
||||
@@ -365,9 +371,12 @@ class NBPFLoader:
|
||||
|
||||
return instance, main_module
|
||||
|
||||
def _build_safe_globals(self, plugin_name: str) -> dict:
|
||||
def _build_safe_globals(self, plugin_name: str, allowed_imports: list[str] = None) -> dict:
|
||||
"""构建安全的全局命名空间
|
||||
|
||||
如果插件在 manifest 中声明了 imports 权限,将 `__import__` 加回内置函数,
|
||||
并用白名单包装器限制只能导入声明的模块。
|
||||
|
||||
注意:Python 沙箱无法完全阻止通过 ()__class__.__bases__[0].__subclasses__()
|
||||
等反射方式逃逸。本沙箱仅用于防止意外访问危险模块,真正的安全隔离
|
||||
需要 OS 级容器化。
|
||||
@@ -390,6 +399,19 @@ class NBPFLoader:
|
||||
'KeyError': KeyError, 'IndexError': IndexError,
|
||||
'Exception': Exception, 'BaseException': BaseException,
|
||||
}
|
||||
|
||||
# 如果插件声明了 imports 权限,添加白名单 __import__
|
||||
if allowed_imports:
|
||||
_allowed_set = set(allowed_imports)
|
||||
def _safe_import(name, *args, **kwargs):
|
||||
base = name.split(".")[0]
|
||||
if base not in _allowed_set:
|
||||
raise ImportError(
|
||||
f"模块 '{name}' 不在权限白名单中。"
|
||||
f"请在 manifest.json 的 permissions.imports 中声明: {sorted(_allowed_set)}"
|
||||
)
|
||||
return __import__(name, *args, **kwargs)
|
||||
safe_builtins['__import__'] = _safe_import
|
||||
return {
|
||||
'__builtins__': safe_builtins,
|
||||
'__name__': f'nbpf.{plugin_name}',
|
||||
|
||||
24
oss/templates/mod/README.md
Normal file
24
oss/templates/mod/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# @{{ author }}/{{ mod_name }}
|
||||
|
||||
{{ description }}
|
||||
|
||||
## 安装
|
||||
|
||||
将 `{{ mod_name }}.nbpf` 放入 NebulaShell 的 `mods/` 目录即可。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 打包
|
||||
nebula nbpf pack ./{{ mod_name }} -o {{ mod_name }}.nbpf \
|
||||
--ed25519-key ./keys/ed25519.pem \
|
||||
--rsa-key ./keys/rsa.pem \
|
||||
--signer "{{ author }}"
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
50
oss/templates/mod/main.py
Normal file
50
oss/templates/mod/main.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
@{{ author }}/{{ mod_name }}
|
||||
{{ description }}
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 模组信息(可选,用于动态获取)
|
||||
NAME = "{{ mod_name }}"
|
||||
VERSION = "0.1.0"
|
||||
|
||||
|
||||
def init(deps):
|
||||
"""
|
||||
模组初始化。
|
||||
deps 包含:
|
||||
- deps["services"] — 其他模组注册的服务
|
||||
- deps["config"] — 当前模组的配置
|
||||
- deps["logger"] — 日志工具
|
||||
"""
|
||||
logger = deps.get("logger")
|
||||
if logger:
|
||||
logger.info(f"{NAME} v{VERSION} 初始化完成")
|
||||
|
||||
|
||||
def start():
|
||||
"""模组启动。init 成功后调用。"""
|
||||
pass
|
||||
|
||||
|
||||
def stop():
|
||||
"""模组停止。框架关闭时调用,释放资源。"""
|
||||
pass
|
||||
|
||||
|
||||
def reload(config: dict):
|
||||
"""热重载配置(可选)"""
|
||||
pass
|
||||
|
||||
|
||||
def health() -> dict:
|
||||
"""健康检查(可选)"""
|
||||
return {"status": "ok", "version": VERSION}
|
||||
|
||||
|
||||
def stats() -> dict:
|
||||
"""统计信息(可选)"""
|
||||
return {}
|
||||
22
oss/templates/mod/manifest.json
Normal file
22
oss/templates/mod/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@{{ author }}/{{ mod_name }}",
|
||||
"version": "0.1.0",
|
||||
"description": "{{ description }}",
|
||||
"author": "{{ author }}",
|
||||
"license": "MIT",
|
||||
"type": "{{ mod_type }}",
|
||||
"main": "main.py",
|
||||
"enabled": true,
|
||||
"priority": 999,
|
||||
"runtime": {
|
||||
"language": "python",
|
||||
"entry_point": "main.py",
|
||||
"requirements": []
|
||||
},
|
||||
"capabilities": [],
|
||||
"services": {
|
||||
"provides": [],
|
||||
"consumes": []
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
24
system-monitor/README.md
Normal file
24
system-monitor/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# @NebulaShell/system-monitor
|
||||
|
||||
实时系统监控:CPU/内存/磁盘/网络/进程TOP
|
||||
|
||||
## 安装
|
||||
|
||||
将 `system-monitor.nbpf` 放入 NebulaShell 的 `mods/` 目录即可。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 打包
|
||||
nebula nbpf pack ./system-monitor -o system-monitor.nbpf \
|
||||
--ed25519-key ./keys/ed25519.pem \
|
||||
--rsa-key ./keys/rsa.pem \
|
||||
--signer "NebulaShell"
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
430
system-monitor/main.py
Normal file
430
system-monitor/main.py
Normal file
@@ -0,0 +1,430 @@
|
||||
"""
|
||||
@NebulaShell/system-monitor
|
||||
实时系统监控模组 - CPU/内存/磁盘/网络/进程TOP
|
||||
|
||||
提供 HTTP REST API,默认端口 10087。
|
||||
可在 manifest.json 的 config.args 中自定义端口。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
import psutil
|
||||
HAS_PSUTIL = True
|
||||
except ImportError:
|
||||
HAS_PSUTIL = False
|
||||
|
||||
NAME = "system-monitor"
|
||||
VERSION = "0.1.0"
|
||||
|
||||
# ── 历史数据存储 ──
|
||||
MAX_HISTORY = 60 # 保留最近60条(每分钟一条,即1小时)
|
||||
_history: deque = deque(maxlen=MAX_HISTORY)
|
||||
_collector_thread = None
|
||||
_collector_running = False
|
||||
|
||||
|
||||
def _collect_stats() -> dict:
|
||||
"""采集一次系统状态快照"""
|
||||
if not HAS_PSUTIL:
|
||||
return {"error": "psutil not installed"}
|
||||
|
||||
now = time.time()
|
||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
mem = psutil.virtual_memory()
|
||||
swap = psutil.swap_memory()
|
||||
|
||||
disk = psutil.disk_usage("/")
|
||||
|
||||
net = psutil.net_io_counters()
|
||||
net_conns = len(psutil.net_connections())
|
||||
|
||||
# TOP 10 进程(按CPU排序)
|
||||
top_processes = []
|
||||
for proc in sorted(psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent", "status"]),
|
||||
key=lambda p: p.info.get("cpu_percent", 0) or 0, reverse=True)[:10]:
|
||||
try:
|
||||
top_processes.append({
|
||||
"pid": proc.info["pid"],
|
||||
"name": proc.info["name"] or "?",
|
||||
"cpu": round(proc.info["cpu_percent"] or 0, 1),
|
||||
"mem": round(proc.info["memory_percent"] or 0, 1),
|
||||
"status": proc.info["status"] or "?",
|
||||
})
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
# 开机时间
|
||||
boot_time = psutil.boot_time()
|
||||
|
||||
return {
|
||||
"timestamp": now,
|
||||
"datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
|
||||
"uptime": round(now - boot_time),
|
||||
"cpu": {
|
||||
"percent": round(cpu_percent, 1),
|
||||
"count": cpu_count,
|
||||
"load_avg": [round(x, 2) for x in os.getloadavg()] if hasattr(os, "getloadavg") else None,
|
||||
},
|
||||
"memory": {
|
||||
"total": mem.total,
|
||||
"available": mem.available,
|
||||
"used": mem.used,
|
||||
"percent": round(mem.percent, 1),
|
||||
"swap_total": swap.total,
|
||||
"swap_used": swap.used,
|
||||
"swap_percent": round(swap.percent, 1),
|
||||
},
|
||||
"disk": {
|
||||
"total": disk.total,
|
||||
"used": disk.used,
|
||||
"free": disk.free,
|
||||
"percent": round(disk.percent, 1),
|
||||
},
|
||||
"network": {
|
||||
"bytes_sent": net.bytes_sent,
|
||||
"bytes_recv": net.bytes_recv,
|
||||
"packets_sent": net.packets_sent,
|
||||
"packets_recv": net.packets_recv,
|
||||
"connections": net_conns,
|
||||
},
|
||||
"processes": {
|
||||
"total": len(psutil.pids()),
|
||||
"running": sum(1 for p in psutil.process_iter(["status"])
|
||||
if p.info.get("status") == "running"),
|
||||
"top": top_processes,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _collector_loop(interval: float = 5.0):
|
||||
"""后台采集线程"""
|
||||
global _collector_running
|
||||
_collector_running = True
|
||||
while _collector_running:
|
||||
try:
|
||||
stats = _collect_stats()
|
||||
_history.append(stats)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
# ── HTTP 服务 ──
|
||||
|
||||
class MonitorHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP 请求处理器"""
|
||||
|
||||
def _json_response(self, data: dict, status: int = 200):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8"))
|
||||
|
||||
def _html_response(self, html: str):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode("utf-8"))
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path.rstrip("/")
|
||||
|
||||
if path == "/" or path == "":
|
||||
self._html_response(_render_dashboard())
|
||||
elif path == "/health":
|
||||
self._json_response({"status": "ok", "version": VERSION, "uptime": _get_uptime()})
|
||||
elif path == "/stats":
|
||||
stats = _collect_stats() if not _history else _history[-1]
|
||||
self._json_response(stats)
|
||||
elif path == "/stats/current":
|
||||
self._json_response(_collect_stats())
|
||||
elif path == "/stats/history":
|
||||
self._json_response({
|
||||
"count": len(_history),
|
||||
"max": MAX_HISTORY,
|
||||
"data": list(_history),
|
||||
})
|
||||
elif path == "/stats/cpu":
|
||||
s = _collect_stats()
|
||||
self._json_response(s.get("cpu", {}))
|
||||
elif path == "/stats/memory":
|
||||
s = _collect_stats()
|
||||
self._json_response(s.get("memory", {}))
|
||||
elif path == "/stats/disk":
|
||||
s = _collect_stats()
|
||||
self._json_response(s.get("disk", {}))
|
||||
elif path == "/stats/network":
|
||||
s = _collect_stats()
|
||||
self._json_response(s.get("network", {}))
|
||||
elif path == "/stats/processes":
|
||||
s = _collect_stats()
|
||||
self._json_response(s.get("processes", {}))
|
||||
else:
|
||||
self._json_response({"error": "Not Found", "path": path}, 404)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""静默日志,避免刷屏"""
|
||||
pass
|
||||
|
||||
|
||||
def _get_uptime() -> float:
|
||||
try:
|
||||
return round(time.time() - psutil.boot_time())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _render_dashboard() -> str:
|
||||
"""渲染简易仪表盘 HTML"""
|
||||
stats = _collect_stats() if not _history else _history[-1]
|
||||
if not stats or "error" in stats:
|
||||
return "<h1>System Monitor</h1><p>psutil not available</p>"
|
||||
|
||||
cpu = stats.get("cpu", {})
|
||||
mem = stats.get("memory", {})
|
||||
disk = stats.get("disk", {})
|
||||
net = stats.get("network", {})
|
||||
procs = stats.get("processes", {})
|
||||
uptime = stats.get("uptime", 0)
|
||||
|
||||
# 格式化时间
|
||||
days, rem = divmod(uptime, 86400)
|
||||
hours, rem = divmod(rem, 3600)
|
||||
mins = rem // 60
|
||||
uptime_str = f"{int(days)}天 {int(hours)}时 {int(mins)}分"
|
||||
|
||||
def bar(pct, color="primary"):
|
||||
color_map = {
|
||||
"primary": "#0d6efd", "success": "#198754",
|
||||
"warning": "#ffc107", "danger": "#dc3545",
|
||||
}
|
||||
c = color_map.get(color, color_map["primary"])
|
||||
return f'<div style="height:20px;background:#e9ecef;border-radius:10px;overflow:hidden">' \
|
||||
f'<div style="height:100%;width:{pct}%;background:{c};transition:width 0.5s"></div></div>'
|
||||
|
||||
def mem_fmt(b):
|
||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||
if b < 1024:
|
||||
return f"{b:.1f} {unit}"
|
||||
b /= 1024
|
||||
return f"{b:.1f} PB"
|
||||
|
||||
# 进程TOP表格行
|
||||
proc_rows = ""
|
||||
for p in procs.get("top", []):
|
||||
proc_rows += f"<tr><td>{p['pid']}</td><td>{p['name']}</td>" \
|
||||
f"<td>{p['cpu']}%</td><td>{p['mem']}%</td><td>{p['status']}</td></tr>"
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>System Monitor</title>
|
||||
<style>
|
||||
* {{ margin:0; padding:0; box-sizing:border-box; }}
|
||||
body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; background:#f8f9fa; color:#333; padding:20px; }}
|
||||
.container {{ max-width:900px; margin:0 auto; }}
|
||||
h1 {{ font-size:24px; margin-bottom:5px; }}
|
||||
.sub {{ color:#666; font-size:14px; margin-bottom:20px; }}
|
||||
.grid {{ display:grid; grid-template-columns:1fr 1fr; gap:15px; margin-bottom:20px; }}
|
||||
.card {{ background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 3px rgba(0,0,0,0.1); }}
|
||||
.card h3 {{ font-size:14px; color:#666; margin-bottom:8px; }}
|
||||
.card .value {{ font-size:28px; font-weight:700; }}
|
||||
.card .subtext {{ font-size:12px; color:#999; margin-top:4px; }}
|
||||
.full {{ grid-column:1/-1; }}
|
||||
table {{ width:100%; border-collapse:collapse; font-size:13px; }}
|
||||
th, td {{ padding:6px 8px; text-align:left; border-bottom:1px solid #eee; }}
|
||||
th {{ color:#666; font-weight:600; }}
|
||||
a {{ color:#0d6efd; text-decoration:none; }}
|
||||
a:hover {{ text-decoration:underline; }}
|
||||
.links {{ margin-bottom:15px; font-size:13px; }}
|
||||
.links a {{ margin-right:12px; }}
|
||||
</style>
|
||||
</head><body>
|
||||
<div class="container">
|
||||
<h1>📊 System Monitor</h1>
|
||||
<p class="sub">v{VERSION} · 运行时间 {uptime_str} · 进程 {procs.get('total', '?')} 个</p>
|
||||
|
||||
<div class="links">
|
||||
<a href="/stats">📄 JSON</a>
|
||||
<a href="/stats/current">🔄 实时刷新</a>
|
||||
<a href="/stats/history">📈 历史数据</a>
|
||||
<a href="/health">💚 健康检查</a>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>🧠 CPU</h3>
|
||||
<div class="value">{cpu.get('percent', '?')}%</div>
|
||||
<div class="subtext">{cpu.get('count', '?')} 核心 · 负载 {cpu.get('load_avg', ['?','?','?'])}</div>
|
||||
{bar(cpu.get('percent', 0), 'danger' if cpu.get('percent', 0) > 80 else 'warning' if cpu.get('percent', 0) > 60 else 'primary')}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>💾 内存</h3>
|
||||
<div class="value">{mem.get('percent', '?')}%</div>
|
||||
<div class="subtext">{mem_fmt(mem.get('used', 0))} / {mem_fmt(mem.get('total', 0))}</div>
|
||||
{bar(mem.get('percent', 0), 'danger' if mem.get('percent', 0) > 80 else 'warning' if mem.get('percent', 0) > 60 else 'success')}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>💿 磁盘 /</h3>
|
||||
<div class="value">{disk.get('percent', '?')}%</div>
|
||||
<div class="subtext">{mem_fmt(disk.get('used', 0))} / {mem_fmt(disk.get('total', 0))}</div>
|
||||
{bar(disk.get('percent', 0), 'danger' if disk.get('percent', 0) > 85 else 'warning')}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🌐 网络</h3>
|
||||
<div class="value">{net.get('connections', '?')}</div>
|
||||
<div class="subtext">连接数 · ↓ {mem_fmt(net.get('bytes_recv', 0))} ↑ {mem_fmt(net.get('bytes_sent', 0))}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card full">
|
||||
<h3>⚡ TOP 10 进程 (CPU)</h3>
|
||||
<table>
|
||||
<thead><tr><th>PID</th><th>名称</th><th>CPU</th><th>内存</th><th>状态</th></tr></thead>
|
||||
<tbody>{proc_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="sub" style="text-align:center;margin-top:20px">
|
||||
数据采集间隔 5秒 · 保留最近 {MAX_HISTORY} 条
|
||||
</p>
|
||||
</div>
|
||||
</body></html>"""
|
||||
return html
|
||||
|
||||
|
||||
# ── HTTP 服务器 ──
|
||||
|
||||
_http_server = None
|
||||
_server_thread = None
|
||||
|
||||
|
||||
def _run_http_server(host: str, port: int):
|
||||
global _http_server
|
||||
server = HTTPServer((host, port), MonitorHandler)
|
||||
_http_server = server
|
||||
try:
|
||||
server.serve_forever()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── 生命周期 ──
|
||||
|
||||
def init(deps):
|
||||
"""模组初始化"""
|
||||
logger = deps.get("logger")
|
||||
if logger:
|
||||
logger.info(f"System Monitor v{VERSION} 初始化")
|
||||
|
||||
|
||||
def start():
|
||||
"""启动 HTTP 服务 + 数据采集"""
|
||||
global _http_server, _server_thread, _collector_thread
|
||||
|
||||
if not HAS_PSUTIL:
|
||||
print("[system-monitor] ⚠ psutil 未安装,系统监控不可用")
|
||||
print("[system-monitor] 💡 请执行: pip install psutil")
|
||||
return
|
||||
|
||||
# 启动数据采集(后台线程,5秒间隔)
|
||||
_collector_thread = threading.Thread(
|
||||
target=_collector_loop, args=(5.0,), daemon=True
|
||||
)
|
||||
_collector_thread.start()
|
||||
|
||||
# 启动 HTTP 服务
|
||||
port = 10087
|
||||
host = "0.0.0.0"
|
||||
|
||||
_server_thread = threading.Thread(
|
||||
target=_run_http_server, args=(host, port), daemon=True
|
||||
)
|
||||
_server_thread.start()
|
||||
|
||||
print(f"[system-monitor] ✅ 系统监控已启动")
|
||||
print(f"[system-monitor] 🌐 仪表盘: http://localhost:{port}")
|
||||
print(f"[system-monitor] 📄 API: http://localhost:{port}/stats")
|
||||
print(f"[system-monitor] 💚 健康: http://localhost:{port}/health")
|
||||
|
||||
|
||||
def stop():
|
||||
"""停止服务,释放资源"""
|
||||
global _http_server, _collector_running
|
||||
|
||||
_collector_running = False
|
||||
|
||||
if _http_server:
|
||||
try:
|
||||
_http_server.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
_http_server = None
|
||||
|
||||
_history.clear()
|
||||
print("[system-monitor] 已停止")
|
||||
|
||||
|
||||
def health() -> dict:
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "ok" if HAS_PSUTIL else "degraded",
|
||||
"version": VERSION,
|
||||
"uptime": _get_uptime(),
|
||||
"data_points": len(_history),
|
||||
}
|
||||
|
||||
|
||||
def stats() -> dict:
|
||||
"""统计信息"""
|
||||
return {
|
||||
"version": VERSION,
|
||||
"psutil_available": HAS_PSUTIL,
|
||||
"history_count": len(_history),
|
||||
"history_max": MAX_HISTORY,
|
||||
"collector_running": _collector_running,
|
||||
}
|
||||
|
||||
|
||||
# ── 目录插件兼容(类 + New() 工厂函数) ──
|
||||
|
||||
class SystemMonitor:
|
||||
"""系统监控插件类封装"""
|
||||
name = NAME
|
||||
version = VERSION
|
||||
description = "实时系统监控:CPU/内存/磁盘/网络/进程TOP"
|
||||
|
||||
def __init__(self):
|
||||
self._http_server = None
|
||||
self._server_thread = None
|
||||
|
||||
def init(self, deps=None):
|
||||
return init(deps)
|
||||
|
||||
def start(self):
|
||||
return start()
|
||||
|
||||
def stop(self):
|
||||
return stop()
|
||||
|
||||
def health(self) -> dict:
|
||||
return health()
|
||||
|
||||
def stats(self) -> dict:
|
||||
return stats()
|
||||
|
||||
|
||||
def New():
|
||||
"""目录插件工厂函数"""
|
||||
return SystemMonitor()
|
||||
45
system-monitor/manifest.json
Normal file
45
system-monitor/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@NebulaShell/system-monitor",
|
||||
"version": "0.1.0",
|
||||
"description": "实时系统监控:CPU/内存/磁盘/网络/进程TOP,提供HTTP仪表盘和REST API",
|
||||
"author": "NebulaShell",
|
||||
"license": "MIT",
|
||||
"type": "tool",
|
||||
"main": "main.py",
|
||||
"enabled": true,
|
||||
"priority": 999,
|
||||
"runtime": {
|
||||
"language": "python",
|
||||
"entry_point": "main.py",
|
||||
"requirements": [
|
||||
"psutil>=5.8.0"
|
||||
]
|
||||
},
|
||||
"capabilities": [
|
||||
"monitoring"
|
||||
],
|
||||
"services": {
|
||||
"provides": [
|
||||
"system-monitor"
|
||||
],
|
||||
"consumes": []
|
||||
},
|
||||
"config": {
|
||||
"port": 10087,
|
||||
"host": "0.0.0.0",
|
||||
"interval": 5,
|
||||
"max_history": 60
|
||||
},
|
||||
"permissions": {
|
||||
"imports": [
|
||||
"os",
|
||||
"threading",
|
||||
"json",
|
||||
"time",
|
||||
"collections",
|
||||
"http",
|
||||
"urllib",
|
||||
"psutil"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user