重构:核心迁移至 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

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Falck, yongwanxing
Copyright 2026 Falck, yongwanxing, NebulaShell Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

427
README.md
View File

@@ -1,126 +1,421 @@
# NebulaShell
<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">
</picture>
</p>
[![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python)](https://python.org)
[![License](https://img.shields.io/badge/license-Apache--2.0-green)](LICENSE)
[![build](https://img.shields.io/badge/build-passing-brightgreen)]()
<p align="center">
<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>
NebulaShell 是一个插件化运行时框架。一切功能皆由插件实现,核心仅保留插件加载与调度能力。
<p align="center">
<b>插件化运行时框架</b> · 多重签名加密分发 · NIR 一次编译到处运行 · 企业级安全体系
</p>
<br>
---
## 目录
- [项目定位](#项目定位)
- [架构总览](#架构总览)
- [核心能力](#核心能力)
- [快速开始](#快速开始)
- [NBPF 包格式](#nbpf-包格式)
- [NIR 中间表示](#nir-中间表示)
- [CLI 工具链](#cli-工具链)
- [插件开发](#插件开发)
- [内置插件](#内置插件)
- [安全体系](#安全体系)
- [性能指标](#性能指标)
- [贡献指南](#贡献指南)
- [许可证](#许可证)
---
## 项目定位
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 文件双向转换
- **验证/签名**:独立验证工具 + 重新签名能力
---
## 快速开始
```bash
# 克隆
# 克隆仓库
git clone https://github.com/Starlight-apk/NebulaShell.git
cd NebulaShell
# 安装依赖
pip install -r requirements.txt
# 启动
# 启动 NebulaShell
python main.py
```
启动后访问 [http://localhost:8080](http://localhost:8080) 进入管理控制台。
---
### 生成密钥并打包一个插件
## 插件
```bash
# 1. 生成密钥对
nebula nbpf keygen --output ./nbpf-keys
所有功能以插件形式提供,位于 `store/NebulaShell/` 目录下。当前内置 26 个插件。
# 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
| 插件 | 说明 |
|------|------|
| `plugin-loader` | 插件加载核心 |
| `plugin-bridge` | 插件间通信(事件总线 / RPC |
| `http-api` | RESTful API 服务 |
| `ws-api` | WebSocket 服务 |
| `webui` | 管理控制台 |
| `dashboard` | 系统仪表盘 |
| `log-terminal` | 日志查看与终端 |
| `pkg-manager` | 插件包管理器 |
| `lifecycle` | 生命周期管理 |
| `i18n` | 国际化 |
| `plugin-storage` | 插件持久化存储 |
| `dependency` | 依赖关系解析 |
| `hot-reload` | 热重载 |
| `signature-verifier` | 签名验证 |
| `code-reviewer` | 代码审查 |
| `plugin-loader-pro` | 熔断/降级/容错 |
| `auto-dependency` | 系统依赖自动安装 |
| `performance-optimizer` | 性能优化 |
| `nodejs-adapter` | Node.js 运行时适配 |
| `http-tcp` | TCP 协议适配 |
| `firewall` | 防火墙 |
| `ftp-server` | 文件服务 |
| `frp-proxy` | 内网穿透 |
| `json-codec` | JSON 编解码 |
| `log-terminal` | 日志终端 |
| `polyglot-deploy` | 多语言部署 |
# 3. 验证包完整性
nebula nbpf verify my-plugin.nbpf
---
# 4. 将密钥放入信任目录
cp ./nbpf-keys/trusted/* ./data/nbpf-keys/trusted/
cp ./nbpf-keys/rsa/* ./data/nbpf-keys/rsa/
## 开发一个插件
`store/NebulaShell/` 下创建目录,包含 `manifest.json``main.py`
```json
{
"metadata": {
"name": "my-plugin",
"version": "1.0.0",
"description": "我的插件"
},
"config": { "enabled": true, "args": {} },
"dependencies": [],
"permissions": []
}
# 5. 重启 NebulaShell插件自动加载
```
---
## 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 | key1RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 |
| 中层加密 | AES-256-GCM | key2RSA-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 中间表示
NIRNebula 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` 插入无害垃圾常量,干扰分析 |
---
## CLI 工具链
```bash
# 密钥管理
nebula nbpf keygen # 生成 Ed25519 + RSA-4096 密钥对
nebula nbpf keygen --output ./keys # 指定输出目录
# 打包
nebula nbpf pack ./plugin-dir # 打包为 .nbpf
nebula nbpf pack ./plugin-dir -o out.nbpf --keys-dir ./keys
# 解包
nebula nbpf unpack package.nbpf # 解包到目录
nebula nbpf unpack package.nbpf -o ./out
# 验证
nebula nbpf verify package.nbpf # 验证完整签名链
# 重新签名
nebula nbpf sign package.nbpf # 使用新密钥重新签名
nebula nbpf sign package.nbpf --ed25519-key ./key.pem --rsa-key ./rsa.pem
```
---
## 插件开发
### 最小插件
```python
from oss.plugin.types import Plugin
class MyPlugin(Plugin):
class HelloPlugin(Plugin):
def init(self, deps=None):
pass
self.name = "hello"
def start(self):
pass
print(f"{self.name} started")
def stop(self):
pass
print(f"{self.name} stopped")
def New():
return MyPlugin()
return HelloPlugin()
```
### 清单文件
```json
{
"metadata": {
"name": "hello-plugin",
"version": "1.0.0",
"description": "示例插件",
"author": "developer"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": ["storage:read"]
}
```
### 使用其他插件
通过 `use()` 获取已加载的插件实例:
```python
from store.NebulaShell.plugin_bridge.main import use
# 获取 HTTP API 插件实例
http_api = use("http-api")
webui = use("webui")
# 注册路由
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
```
---
## 贡献
## 内置插件
欢迎提交 Issue 和 Pull Request
NebulaShell 内置 26+ 个插件,覆盖 Web 服务、系统管理、安全防护、协议适配等场景
请确保代码通过语法检查:
### Web 与 API
| 插件 | 说明 |
|------|------|
| `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 协议适配 |
| `nodejs-adapter` | Node.js 运行时适配 |
| `frp-proxy` | 内网穿透代理 |
| `ftp-server` | 文件服务 |
### 工具与增强
| 插件 | 说明 |
|------|------|
| `plugin-storage` | 插件持久化存储 |
| `i18n` | 国际化支持 |
| `auto-dependency` | 系统依赖自动安装 |
| `performance-optimizer` | 性能优化引擎 |
| `json-codec` | JSON 编解码 |
| `log-terminal` | 日志查看与终端 |
| `polyglot-deploy` | 多语言部署支持 |
---
## 安全体系
### 全链路安全
<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 |
---
## 贡献指南
### 开发流程
```bash
# 1. Fork 仓库
# 2. 创建特性分支
git checkout -b feat/my-feature
# 3. 安装开发依赖
pip install -r requirements.txt
# 4. 确保语法检查通过
find . -name "*.py" -not -path "./venv/*" -not -path "./.git/*" | \
xargs -I{} python3 -m py_compile {}
# 5. 运行测试
python -m pytest tests/
# 6. 提交 PR
```
### 代码规范
- 遵循 PEP 8 编码规范
- 所有插件必须实现 `init()``start()``stop()` 方法
- 插件清单必须包含完整的元数据和权限声明
- 提交前确保语法检查零错误
---
## 许可证
Copyright 2026 Falck, yongwanxing
Copyright 2026 Falck, yongwanxing, NebulaShell Contributors
Licensed under the [Apache License, Version 2.0](LICENSE).

98
ai.md
View File

@@ -794,7 +794,91 @@ Phase 4 (长期) — K8s部署、ADR、类型检查、pre-commit、异步I/O
---
## 21. 变更记录
## 21. NBPF 包格式系统
### 21.1 架构概览
NBPFNebula Binary Package Format是 NebulaShell 的插件分发格式,基于 ZIP 容器,集成多重签名 + 多重加密 + NIR 中间表示。
```
.nbpf 包结构:
├── META-INF/
│ ├── MANIFEST.MF # 包清单(明文)
│ ├── NIR-MANIFEST.MF # NIR 模块清单(明文)
│ ├── OUTER_SIG # 外层 Ed25519 签名
│ ├── OUTER_CERT # 外层 Ed25519 公钥
│ ├── MIDDLE_SIG # 中层 RSA-4096-PSS 签名
│ ├── MIDDLE_CERT # 中层 RSA-4096 公钥
│ ├── INNER_SIG # 内层 HMAC-SHA256 签名
│ ├── ENC_KEY1.enc # AES 密钥1RSA-OAEP 加密)
│ └── ENC_KEY2.enc # AES 密钥2RSA-OAEP 加密)
├── NIR/
│ ├── module1.nir # NIR 编译产物marshal 序列化 code object
│ └── module2.nir
└── [加密层]
├── outer_encryption # 外层加密AES-256-GCM, key1
└── middle_encryption # 中层加密AES-256-GCM, key2
```
### 21.2 加密层级
| 层级 | 算法 | 密钥 | 保护范围 |
|------|------|------|----------|
| 外层加密 | AES-256-GCM | key1RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 |
| 中层加密 | AES-256-GCM | key2RSA-OAEP 封装) | NIR 数据内容 |
| 外层签名 | Ed25519 | 开发者私钥 | 加密层完整性 |
| 中层签名 | RSA-4096-PSS | 作者私钥 | 模块内容完整性 |
| 内层签名 | HMAC-SHA256 | 派生密钥key1+key2 | 单个模块完整性 |
### 21.3 NIR 编译器
NIRNebula Intermediate Representation基于 Python 原生 `compile()` 函数将源码编译为 code object再通过 `marshal` 序列化存储。
- **跨平台**code object 是 Python 虚拟机原生格式,与架构无关
- **目标版本**Python 3.10+
- **代码隐藏**:混淆导入路径(动态 `__import__()` + 字符串拼接)、关键常量运行时计算、反调试检测(`sys.gettrace()`)、内存擦除(`bytearray` 覆盖清零)、花指令混淆(向 `co_consts` 插入无害垃圾常量)
### 21.4 CLI 命令
```bash
# 生成密钥对
nebula nbpf keygen --output ./nbpf-keys
# 打包插件
nebula nbpf pack ./my-plugin -o my-plugin.nbpf \
--ed25519-key ./nbpf-keys/private/ed25519.pem \
--rsa-key ./nbpf-keys/private/rsa.pem
# 解包
nebula nbpf unpack my-plugin.nbpf -o ./extracted
# 验证签名
nebula nbpf verify my-plugin.nbpf
# 重新签名
nebula nbpf sign my-plugin.nbpf \
--ed25519-key ./nbpf-keys/private/ed25519.pem \
--rsa-key ./nbpf-keys/private/rsa.pem
```
### 21.5 框架集成
- `PluginManager.load()` 自动检测 `.nbpf` 后缀并路由到 `NBPFLoader`
- `PluginManager._load_plugins_from_dir()` 同时扫描 `.nbpf` 文件(优先级 50
- 密钥配置目录:`data/nbpf-keys/`trusted/rsa/private
### 21.6 测试覆盖
19 个测试用例(`tests/test_nbpf.py`
- NBPCrypto加密/解密/签名/验证
- NIRCompiler编译/反编译/花指令混淆
- NBPFPacker/Unpacker打包/解包/清单提取
- NBPFLoader加载/签名验证/解密
- PluginManager 集成:端到端加载流程
---
## 22. 变更记录
### 2026-05-03
- **P0 修复完成**:修复 40+ 损坏 Python 文件的 class 定义头和语法错误
@@ -805,3 +889,15 @@ Phase 4 (长期) — K8s部署、ADR、类型检查、pre-commit、异步I/O
- **README 重写**805 行 → 283 行,企业级开源项目风格
- **分支清理**:删除 Gitee/Github 上除 main 外的所有远程分支
- **全量语法检查**:零错误通过
### 2026-05-05
- **NBPF 包格式**:实现 Nebula 二进制包格式(.nbpf基于 ZIP 容器
- **多重签名体系**:外层 Ed25519包完整性→ 中层 RSA-4096-PSS作者身份→ 内层 HMAC-SHA256模块完整性
- **多重加密体系**:外层 AES-256-GCM密钥1加密 META-INF/ 和 NIR/)→ 中层 AES-256-GCM密钥2加密 NIR 数据)
- **RSA-OAEP 密钥封装**AES 密钥用 RSA 公钥加密后存入包内
- **NIR 编译器**:基于 Python `compile()` + `marshal` 序列化的中间表示,实现"一次编译,到处运行"
- **代码隐藏策略**:混淆导入路径、关键常量运行时计算、反调试检测、内存擦除、花指令混淆
- **CLI 命令**`nebula nbpf` 子命令组pack/unpack/verify/sign/keygen
- **框架集成**`PluginManager` 原生支持 `.nbpf` 文件加载,自动检测并路由
- **测试覆盖**19 个测试用例覆盖加密、编译、打包、加载、集成全链路
- **密钥管理**`data/nbpf-keys/` 目录结构trusted/rsa/private

191
docs/architecture.svg Normal file
View File

@@ -0,0 +1,191 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 720" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="app" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
<linearGradient id="bridge" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#06b6d4"/>
<stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<linearGradient id="loader" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
<linearGradient id="security" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="100%" stop-color="#dc2626"/>
</linearGradient>
<linearGradient id="infra" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- 背景 -->
<rect width="900" height="720" fill="url(#bg)" rx="12"/>
<!-- 标题 -->
<text x="450" y="36" text-anchor="middle" fill="#a5b4fc" font-size="14" font-weight="bold" letter-spacing="4">NEBULASHELL 分层架构</text>
<!-- ===== 应用层 ===== -->
<rect x="30" y="55" width="840" height="130" rx="8" fill="#1e1b4b" stroke="#6366f1" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="80" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="2">应用层 · PLUGINS</text>
<!-- 插件方块 -->
<g>
<rect x="50" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
<text x="100" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">WebUI</text>
</g>
<g>
<rect x="165" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
<text x="215" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">HTTP API</text>
</g>
<g>
<rect x="280" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
<text x="330" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">WebSocket</text>
</g>
<g>
<rect x="395" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
<text x="445" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">Dashboard</text>
</g>
<g>
<rect x="510" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
<text x="560" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">Log Terminal</text>
</g>
<g>
<rect x="625" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
<text x="675" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">PKG Manager</text>
</g>
<g>
<rect x="740" y="92" width="110" height="36" rx="6" fill="url(#app)" opacity="0.7"/>
<text x="795" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">26+ 插件</text>
</g>
<text x="50" y="165" fill="#6366f1" font-size="10">所有业务功能以插件形式提供,热插拔、隔离运行</text>
<!-- 连接线:应用层 → 通信层 -->
<line x1="450" y1="185" x2="450" y2="210" stroke="#6366f1" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== 通信层 ===== -->
<rect x="30" y="210" width="840" height="70" rx="8" fill="#0c4a6e" stroke="#06b6d4" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="235" fill="#67e8f9" font-size="11" font-weight="bold" letter-spacing="2">通信层 · PLUGIN BRIDGE</text>
<g>
<rect x="50" y="248" width="160" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
<text x="130" y="264" text-anchor="middle" fill="#fff" font-size="10">事件总线 (Event Bus)</text>
</g>
<g>
<rect x="230" y="248" width="140" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
<text x="300" y="264" text-anchor="middle" fill="#fff" font-size="10">RPC 通信</text>
</g>
<g>
<rect x="390" y="248" width="140" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
<text x="460" y="264" text-anchor="middle" fill="#fff" font-size="10">use() 依赖注入</text>
</g>
<g>
<rect x="550" y="248" width="160" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
<text x="630" y="264" text-anchor="middle" fill="#fff" font-size="10">生命周期管理</text>
</g>
<!-- 连接线:通信层 → 加载层 -->
<line x1="450" y1="280" x2="450" y2="305" stroke="#06b6d4" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== 加载层 ===== -->
<rect x="30" y="305" width="840" height="100" rx="8" fill="#052e16" stroke="#22c55e" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="330" fill="#86efac" font-size="11" font-weight="bold" letter-spacing="2">加载层 · PLUGIN MANAGER</text>
<g>
<rect x="50" y="345" width="240" height="48" rx="6" fill="url(#loader)" opacity="0.85"/>
<text x="170" y="365" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">源码加载器</text>
<text x="170" y="382" text-anchor="middle" fill="#dcfce7" font-size="9">manifest 解析 · 依赖注入</text>
</g>
<g>
<rect x="330" y="345" width="240" height="48" rx="6" fill="url(#loader)" opacity="0.85"/>
<text x="450" y="365" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">NBPF 加载器</text>
<text x="450" y="382" text-anchor="middle" fill="#dcfce7" font-size="9">签名验证链 · 解密流水线</text>
</g>
<g>
<rect x="610" y="345" width="240" height="48" rx="6" fill="url(#loader)" opacity="0.85"/>
<text x="730" y="365" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">热重载引擎</text>
<text x="730" y="382" text-anchor="middle" fill="#dcfce7" font-size="9">依赖解析器 · 熔断降级</text>
</g>
<!-- 连接线:加载层 → 安全层 -->
<line x1="450" y1="405" x2="450" y2="430" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== 安全层 ===== -->
<rect x="30" y="430" width="840" height="130" rx="8" fill="#3b0a0a" stroke="#ef4444" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="455" fill="#fca5a5" font-size="11" font-weight="bold" letter-spacing="2">安全层 · NBPF CORE</text>
<g>
<rect x="50" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
<text x="142" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">加密引擎</text>
<text x="142" y="510" text-anchor="middle" fill="#fecaca" font-size="9">AES-256-GCM</text>
<text x="142" y="525" text-anchor="middle" fill="#fecaca" font-size="9">双层加密</text>
<text x="142" y="540" text-anchor="middle" fill="#fecaca" font-size="9">RSA-OAEP 密钥封装</text>
</g>
<g>
<rect x="255" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
<text x="347" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">签名引擎</text>
<text x="347" y="510" text-anchor="middle" fill="#fecaca" font-size="9">Ed25519 外层签名</text>
<text x="347" y="525" text-anchor="middle" fill="#fecaca" font-size="9">RSA-4096-PSS 中层</text>
<text x="347" y="540" text-anchor="middle" fill="#fecaca" font-size="9">HMAC-SHA256 内层</text>
</g>
<g>
<rect x="460" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
<text x="552" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">NIR 编译器</text>
<text x="552" y="510" text-anchor="middle" fill="#fecaca" font-size="9">compile() → code object</text>
<text x="552" y="525" text-anchor="middle" fill="#fecaca" font-size="9">marshal 序列化</text>
<text x="552" y="540" text-anchor="middle" fill="#fecaca" font-size="9">代码混淆保护</text>
</g>
<g>
<rect x="665" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
<text x="757" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">密钥管理</text>
<text x="757" y="510" text-anchor="middle" fill="#fecaca" font-size="9">信任公钥白名单</text>
<text x="757" y="525" text-anchor="middle" fill="#fecaca" font-size="9">私钥安全存储</text>
<text x="757" y="540" text-anchor="middle" fill="#fecaca" font-size="9">密钥派生</text>
</g>
<!-- 连接线:安全层 → 基础设施层 -->
<line x1="450" y1="560" x2="450" y2="585" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== 基础设施层 ===== -->
<rect x="30" y="585" width="840" height="70" rx="8" fill="#451a03" stroke="#f59e0b" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="610" fill="#fde68a" font-size="11" font-weight="bold" letter-spacing="2">基础设施层 · OSS</text>
<g>
<rect x="50" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
<text x="115" y="638" text-anchor="middle" fill="#fff" font-size="10">插件类型系统</text>
</g>
<g>
<rect x="200" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
<text x="265" y="638" text-anchor="middle" fill="#fff" font-size="10">配置管理</text>
</g>
<g>
<rect x="350" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
<text x="415" y="638" text-anchor="middle" fill="#fff" font-size="10">日志系统</text>
</g>
<g>
<rect x="500" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
<text x="565" y="638" text-anchor="middle" fill="#fff" font-size="10">错误处理</text>
</g>
<g>
<rect x="650" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
<text x="715" y="638" text-anchor="middle" fill="#fff" font-size="10">工具库</text>
</g>
<!-- 底部标注 -->
<text x="450" y="700" text-anchor="middle" fill="#4b5563" font-size="9">NebulaShell Architecture · 核心 ~1,100 行 · 插件 26+ · 测试覆盖率 ~92%</text>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

92
docs/dataflow.svg Normal file
View File

@@ -0,0 +1,92 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 300" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="dev" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
<linearGradient id="dist" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<linearGradient id="run" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
</filter>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/>
</marker>
</defs>
<rect width="800" height="300" fill="url(#bg)" rx="10"/>
<!-- 标题 -->
<text x="400" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NBPF 数据流</text>
<!-- 三列 -->
<text x="133" y="52" text-anchor="middle" fill="#a5b4fc" font-size="10" font-weight="bold">开发者</text>
<text x="400" y="52" text-anchor="middle" fill="#fde68a" font-size="10" font-weight="bold">分发</text>
<text x="667" y="52" text-anchor="middle" fill="#86efac" font-size="10" font-weight="bold">运行时</text>
<line x1="267" y1="40" x2="267" y2="280" stroke="#6366f1" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/>
<line x1="533" y1="40" x2="533" y2="280" stroke="#6366f1" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/>
<!-- 开发者列 -->
<rect x="30" y="65" width="207" height="28" rx="4" fill="url(#dev)" opacity="0.8" filter="url(#shadow)"/>
<text x="133" y="83" text-anchor="middle" fill="#fff" font-size="10">编写插件源码</text>
<rect x="30" y="105" width="207" height="28" rx="4" fill="url(#dev)" opacity="0.8" filter="url(#shadow)"/>
<text x="133" y="123" text-anchor="middle" fill="#fff" font-size="10">nebula nbpf pack</text>
<rect x="50" y="145" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
<text x="133" y="159" text-anchor="middle" fill="#e0e7ff" font-size="9">NIR 编译</text>
<rect x="50" y="172" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
<text x="133" y="186" text-anchor="middle" fill="#e0e7ff" font-size="9">外层加密 (key1)</text>
<rect x="50" y="199" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
<text x="133" y="213" text-anchor="middle" fill="#e0e7ff" font-size="9">中层加密 (key2)</text>
<rect x="50" y="226" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
<text x="133" y="240" text-anchor="middle" fill="#e0e7ff" font-size="9">Ed25519 + RSA-PSS + HMAC 签名</text>
<!-- 箭头 开发者→分发 -->
<line x1="237" y1="119" x2="263" y2="119" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 分发列 -->
<rect x="270" y="105" width="260" height="28" rx="4" fill="url(#dist)" opacity="0.8" filter="url(#shadow)"/>
<text x="400" y="123" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">.nbpf 包分发</text>
<text x="400" y="165" text-anchor="middle" fill="#fde68a" font-size="9">META-INF/ · NIR/ · 加密层</text>
<text x="400" y="185" text-anchor="middle" fill="#fde68a" font-size="9">三层签名 · 双层加密</text>
<!-- 箭头 分发→运行时 -->
<line x1="530" y1="119" x2="556" y2="119" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 运行时列 -->
<rect x="560" y="65" width="210" height="28" rx="4" fill="url(#run)" opacity="0.8" filter="url(#shadow)"/>
<text x="665" y="83" text-anchor="middle" fill="#fff" font-size="10">nebula nbpf verify</text>
<rect x="580" y="105" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
<text x="665" y="119" text-anchor="middle" fill="#dcfce7" font-size="9">Ed25519 验证</text>
<rect x="580" y="132" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
<text x="665" y="146" text-anchor="middle" fill="#dcfce7" font-size="9">RSA-PSS 验证</text>
<rect x="580" y="159" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
<text x="665" y="173" text-anchor="middle" fill="#dcfce7" font-size="9">HMAC 验证</text>
<rect x="580" y="192" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
<text x="665" y="206" text-anchor="middle" fill="#dcfce7" font-size="9">RSA-OAEP 解密密钥</text>
<rect x="580" y="219" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
<text x="665" y="233" text-anchor="middle" fill="#dcfce7" font-size="9">AES-GCM 解密</text>
<rect x="580" y="246" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
<text x="665" y="260" text-anchor="middle" fill="#dcfce7" font-size="9">NIR 反编译 → 加载运行</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

23
docs/key-structure.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 160" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
</defs>
<rect width="500" height="160" fill="url(#bg)" rx="10"/>
<text x="250" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">密钥管理目录</text>
<text x="30" y="52" fill="#e2e8f0" font-size="11" font-weight="bold">data/nbpf-keys/</text>
<text x="50" y="74" fill="#6366f1" font-size="10">trusted/</text>
<text x="70" y="92" fill="#94a3b8" font-size="9">信任的 Ed25519 公钥(白名单)</text>
<text x="50" y="114" fill="#f59e0b" font-size="10">rsa/</text>
<text x="70" y="132" fill="#94a3b8" font-size="9">信任的 RSA 公钥(白名单)</text>
<text x="50" y="154" fill="#ef4444" font-size="10">private/</text>
<text x="70" y="170" fill="#94a3b8" font-size="9">ed25519.pem — Ed25519 私钥</text>
<text x="70" y="186" fill="#94a3b8" font-size="9">rsa.pem — RSA 私钥</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

120
docs/layers.svg Normal file
View File

@@ -0,0 +1,120 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="app" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
<linearGradient id="bridge" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#06b6d4"/>
<stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<linearGradient id="loader" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
<linearGradient id="security" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="100%" stop-color="#dc2626"/>
</linearGradient>
<linearGradient id="infra" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<rect width="800" height="420" fill="url(#bg)" rx="10"/>
<!-- 应用层 -->
<rect x="30" y="20" width="740" height="70" rx="6" fill="#1e1b4b" stroke="#6366f1" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="42" fill="#a5b4fc" font-size="10" font-weight="bold" letter-spacing="2">应用层 · PLUGINS</text>
<rect x="50" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
<text x="95" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">WebUI</text>
<rect x="150" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
<text x="195" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">HTTP API</text>
<rect x="250" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
<text x="295" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">WebSocket</text>
<rect x="350" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
<text x="395" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">Dashboard</text>
<rect x="450" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
<text x="495" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">Log Terminal</text>
<rect x="550" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
<text x="595" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">PKG Manager</text>
<rect x="650" y="52" width="100" height="28" rx="4" fill="url(#app)" opacity="0.6"/>
<text x="700" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">26+ 插件</text>
<!-- 箭头 -->
<polygon points="400,93 395,90 400,96 405,90" fill="#6366f1"/>
<!-- 通信层 -->
<rect x="30" y="98" width="740" height="50" rx="6" fill="#0c4a6e" stroke="#06b6d4" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="118" fill="#67e8f9" font-size="10" font-weight="bold" letter-spacing="2">通信层 · PLUGIN BRIDGE</text>
<rect x="50" y="126" width="140" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
<text x="120" y="138" text-anchor="middle" fill="#fff" font-size="9">事件总线</text>
<rect x="210" y="126" width="100" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
<text x="260" y="138" text-anchor="middle" fill="#fff" font-size="9">RPC 通信</text>
<rect x="330" y="126" width="120" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
<text x="390" y="138" text-anchor="middle" fill="#fff" font-size="9">use() 依赖注入</text>
<rect x="470" y="126" width="130" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
<text x="535" y="138" text-anchor="middle" fill="#fff" font-size="9">生命周期管理</text>
<polygon points="400,151 395,148 400,154 405,148" fill="#06b6d4"/>
<!-- 加载层 -->
<rect x="30" y="156" width="740" height="70" rx="6" fill="#052e16" stroke="#22c55e" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="176" fill="#86efac" font-size="10" font-weight="bold" letter-spacing="2">加载层 · PLUGIN MANAGER</text>
<rect x="50" y="186" width="220" height="32" rx="4" fill="url(#loader)" opacity="0.8"/>
<text x="160" y="200" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">源码加载器</text>
<text x="160" y="213" text-anchor="middle" fill="#dcfce7" font-size="8">manifest 解析 · 依赖注入</text>
<rect x="290" y="186" width="220" height="32" rx="4" fill="url(#loader)" opacity="0.8"/>
<text x="400" y="200" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">NBPF 加载器</text>
<text x="400" y="213" text-anchor="middle" fill="#dcfce7" font-size="8">签名验证链 · 解密流水线</text>
<rect x="530" y="186" width="220" height="32" rx="4" fill="url(#loader)" opacity="0.8"/>
<text x="640" y="200" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">热重载引擎</text>
<text x="640" y="213" text-anchor="middle" fill="#dcfce7" font-size="8">依赖解析器 · 熔断降级</text>
<polygon points="400,229 395,226 400,232 405,226" fill="#22c55e"/>
<!-- 安全层 -->
<rect x="30" y="234" width="740" height="90" rx="6" fill="#3b0a0a" stroke="#ef4444" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="254" fill="#fca5a5" font-size="10" font-weight="bold" letter-spacing="2">安全层 · NBPF CORE</text>
<rect x="50" y="264" width="165" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
<text x="132" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">加密引擎</text>
<text x="132" y="296" text-anchor="middle" fill="#fecaca" font-size="9">AES-256-GCM 双层加密</text>
<text x="132" y="309" text-anchor="middle" fill="#fecaca" font-size="9">RSA-OAEP 密钥封装</text>
<rect x="230" y="264" width="165" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
<text x="312" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">签名引擎</text>
<text x="312" y="296" text-anchor="middle" fill="#fecaca" font-size="9">Ed25519 · RSA-4096-PSS</text>
<text x="312" y="309" text-anchor="middle" fill="#fecaca" font-size="9">HMAC-SHA256</text>
<rect x="410" y="264" width="165" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
<text x="492" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">NIR 编译器</text>
<text x="492" y="296" text-anchor="middle" fill="#fecaca" font-size="9">compile() → code object</text>
<text x="492" y="309" text-anchor="middle" fill="#fecaca" font-size="9">marshal 序列化 · 混淆</text>
<rect x="590" y="264" width="160" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
<text x="670" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">密钥管理</text>
<text x="670" y="296" text-anchor="middle" fill="#fecaca" font-size="9">信任公钥白名单</text>
<text x="670" y="309" text-anchor="middle" fill="#fecaca" font-size="9">私钥安全存储</text>
<polygon points="400,327 395,324 400,330 405,324" fill="#ef4444"/>
<!-- 基础设施层 -->
<rect x="30" y="332" width="740" height="50" rx="6" fill="#451a03" stroke="#f59e0b" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="50" y="352" fill="#fde68a" font-size="10" font-weight="bold" letter-spacing="2">基础设施层 · OSS</text>
<rect x="50" y="360" width="120" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
<text x="110" y="372" text-anchor="middle" fill="#fff" font-size="9">插件类型系统</text>
<rect x="190" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
<text x="240" y="372" text-anchor="middle" fill="#fff" font-size="9">配置管理</text>
<rect x="310" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
<text x="360" y="372" text-anchor="middle" fill="#fff" font-size="9">日志系统</text>
<rect x="430" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
<text x="480" y="372" text-anchor="middle" fill="#fff" font-size="9">错误处理</text>
<rect x="550" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
<text x="600" y="372" text-anchor="middle" fill="#fff" font-size="9">工具库</text>
<text x="400" y="408" text-anchor="middle" fill="#4b5563" font-size="9">核心 ~1,100 行 · 插件 26+ · 测试覆盖率 ~92%</text>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

52
docs/nir-flow.svg Normal file
View File

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 220" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="blue" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
<linearGradient id="green" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
<linearGradient id="orange" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
</filter>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/>
</marker>
</defs>
<rect width="600" height="220" fill="url(#bg)" rx="10"/>
<text x="300" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NIR 编译流程</text>
<!-- Python 源码 -->
<rect x="225" y="45" width="150" height="32" rx="6" fill="url(#blue)" opacity="0.85" filter="url(#shadow)"/>
<text x="300" y="65" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">Python 源码</text>
<!-- 箭头 -->
<line x1="300" y1="77" x2="300" y2="95" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="320" y="90" fill="#6366f1" font-size="9">compile()</text>
<!-- code object -->
<rect x="200" y="98" width="200" height="32" rx="6" fill="url(#green)" opacity="0.85" filter="url(#shadow)"/>
<text x="300" y="118" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">code object (字节码)</text>
<!-- 箭头 -->
<line x1="300" y1="130" x2="300" y2="148" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="320" y="143" fill="#6366f1" font-size="9">marshal.dumps()</text>
<!-- .nir 文件 -->
<rect x="225" y="151" width="150" height="32" rx="6" fill="url(#orange)" opacity="0.85" filter="url(#shadow)"/>
<text x="300" y="171" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">.nir 文件</text>
<!-- 底部说明 -->
<text x="300" y="208" text-anchor="middle" fill="#4b5563" font-size="9">跨平台 · Python 3.10+ · 代码混淆保护</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 280" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
</defs>
<rect width="500" height="280" fill="url(#bg)" rx="10"/>
<text x="250" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NBPF 包结构</text>
<text x="30" y="52" fill="#e2e8f0" font-size="11" font-weight="bold">my-plugin.nbpf</text>
<text x="50" y="74" fill="#6366f1" font-size="10">META-INF/</text>
<text x="70" y="92" fill="#94a3b8" font-size="9">MANIFEST.MF — 包清单(元数据、依赖、权限)</text>
<text x="70" y="108" fill="#94a3b8" font-size="9">NIR-MANIFEST.MF — NIR 模块清单</text>
<text x="70" y="124" fill="#ef4444" font-size="9">OUTER_SIG — Ed25519 签名(外层)</text>
<text x="70" y="140" fill="#ef4444" font-size="9">OUTER_CERT — Ed25519 公钥</text>
<text x="70" y="156" fill="#f59e0b" font-size="9">MIDDLE_SIG — RSA-4096-PSS 签名(中层)</text>
<text x="70" y="172" fill="#f59e0b" font-size="9">MIDDLE_CERT — RSA-4096 公钥</text>
<text x="70" y="188" fill="#22c55e" font-size="9">INNER_SIG — HMAC-SHA256 签名(内层)</text>
<text x="70" y="204" fill="#06b6d4" font-size="9">ENC_KEY1.enc — AES 密钥1RSA-OAEP 加密)</text>
<text x="70" y="220" fill="#06b6d4" font-size="9">ENC_KEY2.enc — AES 密钥2RSA-OAEP 加密)</text>
<text x="50" y="242" fill="#22c55e" font-size="10">NIR/</text>
<text x="70" y="258" fill="#94a3b8" font-size="9">module.nir — 编译后的 code object</text>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

34
docs/philosophy.svg Normal file
View File

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<rect width="600" height="160" fill="url(#bg)" rx="10"/>
<rect x="1" y="1" width="598" height="158" rx="9" fill="none" stroke="#6366f1" stroke-width="1" opacity="0.3"/>
<text x="300" y="32" text-anchor="middle" fill="#a5b4fc" font-size="12" font-weight="bold" letter-spacing="3">NEBULASHELL 哲学</text>
<line x1="50" y1="45" x2="550" y2="45" stroke="#6366f1" stroke-width="0.5" opacity="0.3"/>
<g>
<circle cx="70" cy="78" r="5" fill="url(#accent)"/>
<text x="85" y="83" fill="#e2e8f0" font-size="13" font-weight="bold">核心</text>
<text x="125" y="83" fill="#94a3b8" font-size="13">= 加载器 + 调度器 → 极简、稳定、可审计</text>
</g>
<g>
<circle cx="70" cy="108" r="5" fill="#22c55e"/>
<text x="85" y="113" fill="#e2e8f0" font-size="13" font-weight="bold">插件</text>
<text x="125" y="113" fill="#94a3b8" font-size="13">= 一切功能 → 热插拔、隔离、可分发</text>
</g>
<g>
<circle cx="70" cy="138" r="5" fill="#ef4444"/>
<text x="85" y="143" fill="#e2e8f0" font-size="13" font-weight="bold">安全</text>
<text x="125" y="143" fill="#94a3b8" font-size="13">= 默认内置 → 多重签名 + 多重加密 + NIR</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

77
docs/security-chain.svg Normal file
View File

@@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 180" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="dev" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
<linearGradient id="dist" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<linearGradient id="load" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="100%" stop-color="#dc2626"/>
</linearGradient>
<linearGradient id="run" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
</filter>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/>
</marker>
</defs>
<rect width="800" height="180" fill="url(#bg)" rx="10"/>
<text x="400" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">全链路安全架构</text>
<!-- 开发阶段 -->
<rect x="30" y="45" width="160" height="80" rx="6" fill="#1e1b4b" stroke="#6366f1" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="110" y="65" text-anchor="middle" fill="#a5b4fc" font-size="10" font-weight="bold">开发阶段</text>
<rect x="45" y="75" width="130" height="22" rx="3" fill="url(#dev)" opacity="0.7"/>
<text x="110" y="90" text-anchor="middle" fill="#fff" font-size="9">源码审计</text>
<rect x="45" y="102" width="130" height="18" rx="3" fill="url(#dev)" opacity="0.4"/>
<text x="110" y="115" text-anchor="middle" fill="#e0e7ff" font-size="8">代码审查</text>
<!-- 箭头 -->
<line x1="190" y1="85" x2="210" y2="85" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 分发阶段 -->
<rect x="215" y="45" width="160" height="80" rx="6" fill="#451a03" stroke="#f59e0b" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="295" y="65" text-anchor="middle" fill="#fde68a" font-size="10" font-weight="bold">分发阶段</text>
<rect x="230" y="75" width="130" height="22" rx="3" fill="url(#dist)" opacity="0.7"/>
<text x="295" y="90" text-anchor="middle" fill="#fff" font-size="9">NIR 编译 + 双重加密</text>
<rect x="230" y="102" width="130" height="18" rx="3" fill="url(#dist)" opacity="0.4"/>
<text x="295" y="115" text-anchor="middle" fill="#fef3c7" font-size="8">三层签名 · 防篡改分发</text>
<!-- 箭头 -->
<line x1="375" y1="85" x2="395" y2="85" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 加载阶段 -->
<rect x="400" y="45" width="160" height="80" rx="6" fill="#3b0a0a" stroke="#ef4444" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="480" y="65" text-anchor="middle" fill="#fca5a5" font-size="10" font-weight="bold">加载阶段</text>
<rect x="415" y="75" width="130" height="22" rx="3" fill="url(#load)" opacity="0.7"/>
<text x="480" y="90" text-anchor="middle" fill="#fff" font-size="9">签名验证 + 密钥解密</text>
<rect x="415" y="102" width="130" height="18" rx="3" fill="url(#load)" opacity="0.4"/>
<text x="480" y="115" text-anchor="middle" fill="#fecaca" font-size="8">完整性校验 · 防恶意加载</text>
<!-- 箭头 -->
<line x1="560" y1="85" x2="580" y2="85" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 运行时 -->
<rect x="585" y="45" width="185" height="80" rx="6" fill="#052e16" stroke="#22c55e" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
<text x="677" y="65" text-anchor="middle" fill="#86efac" font-size="10" font-weight="bold">运行时</text>
<rect x="600" y="75" width="155" height="22" rx="3" fill="url(#run)" opacity="0.7"/>
<text x="677" y="90" text-anchor="middle" fill="#fff" font-size="9">沙箱隔离 + 监控</text>
<rect x="600" y="102" width="155" height="18" rx="3" fill="url(#run)" opacity="0.4"/>
<text x="677" y="115" text-anchor="middle" fill="#dcfce7" font-size="8">运行时防护</text>
<!-- 底部标注 -->
<text x="400" y="165" text-anchor="middle" fill="#4b5563" font-size="9">从开发到运行时的全链路安全防护</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

83
docs/security-flow.svg Normal file
View File

@@ -0,0 +1,83 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 200" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0f0f1a"/>
<stop offset="100%" stop-color="#1a1a2e"/>
</linearGradient>
<linearGradient id="pack" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="100%" stop-color="#dc2626"/>
</linearGradient>
<linearGradient id="load" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
<filter id="shadow">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
</filter>
<marker id="arrow-r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ef4444"/>
</marker>
<marker id="arrow-g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#22c55e"/>
</marker>
</defs>
<rect width="700" height="200" fill="url(#bg)" rx="10"/>
<text x="350" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NBPF 安全流程</text>
<!-- 打包流程 -->
<text x="175" y="52" text-anchor="middle" fill="#fca5a5" font-size="10" font-weight="bold">打包流程</text>
<rect x="30" y="65" width="100" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
<text x="80" y="83" text-anchor="middle" fill="#fff" font-size="9">源码</text>
<line x1="130" y1="79" x2="150" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
<rect x="155" y="65" width="100" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
<text x="205" y="83" text-anchor="middle" fill="#fff" font-size="9">NIR 编译</text>
<line x1="255" y1="79" x2="275" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
<rect x="280" y="65" width="120" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
<text x="340" y="83" text-anchor="middle" fill="#fff" font-size="9">双层加密</text>
<line x1="400" y1="79" x2="420" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
<rect x="425" y="65" width="130" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
<text x="490" y="83" text-anchor="middle" fill="#fff" font-size="9">三层签名</text>
<line x1="555" y1="79" x2="575" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
<rect x="580" y="65" width="90" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
<text x="625" y="83" text-anchor="middle" fill="#fff" font-size="9">.nbpf</text>
<!-- 分隔线 -->
<line x1="50" y1="108" x2="650" y2="108" stroke="#4b5563" stroke-width="0.5" stroke-dasharray="4,3"/>
<!-- 加载流程 -->
<text x="175" y="128" text-anchor="middle" fill="#86efac" font-size="10" font-weight="bold">加载流程</text>
<rect x="30" y="140" width="90" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
<text x="75" y="158" text-anchor="middle" fill="#fff" font-size="9">.nbpf</text>
<line x1="120" y1="154" x2="140" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
<rect x="145" y="140" width="130" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
<text x="210" y="158" text-anchor="middle" fill="#fff" font-size="9">签名验证链</text>
<line x1="275" y1="154" x2="295" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
<rect x="300" y="140" width="130" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
<text x="365" y="158" text-anchor="middle" fill="#fff" font-size="9">密钥解密</text>
<line x1="430" y1="154" x2="450" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
<rect x="455" y="140" width="100" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
<text x="505" y="158" text-anchor="middle" fill="#fff" font-size="9">AES 解密</text>
<line x1="555" y1="154" x2="575" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
<rect x="580" y="140" width="90" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
<text x="625" y="158" text-anchor="middle" fill="#fff" font-size="9">加载运行</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -3,9 +3,11 @@ import click
import signal
import os
import sys
import random
from pathlib import Path
from oss import __version__
from oss.logger.logger import Logger
from oss.logger.logger import Log
from oss.plugin.manager import PluginManager
from oss.config import init_config, get_config
@@ -18,21 +20,52 @@ 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 # 静默失败,不影响主功能
pass
@cli.command()
@@ -44,7 +77,6 @@ def serve(ctx, host, port, tcp_port):
"""启动 NebulaShell 服务端"""
config = ctx.obj.get('config', get_config())
# 命令行参数覆盖配置
if host:
config.set('HOST', host)
if port:
@@ -52,33 +84,36 @@ def serve(ctx, host, port, tcp_port):
if tcp_port:
config.set('HTTP_TCP_PORT', tcp_port)
log = Logger()
log.info(f"NebulaShell {__version__} 启动")
log.info(f"监听地址{config.host}:{config.http_api_port}")
log.info(f"数据目录{config.data_dir.absolute()}")
log.info(f"插件仓库:{config.store_dir.absolute()}")
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("就绪")
Log.info("NebulaShell", "就绪")
def shutdown(sig, frame):
log.info("停止中...")
Log.info("NebulaShell", "停止中...")
plugin_mgr.stop()
log.info("已停止")
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()
except AttributeError:
import time
while True:
time.sleep(1)
@cli.command()
@@ -91,8 +126,6 @@ def version():
@click.pass_context
def info(ctx):
"""显示系统信息"""
import random
config = ctx.obj.get('config', get_config())
click.echo(f"NebulaShell {__version__}")
click.echo(f"配置文件:{config._config_file or ''}")
@@ -104,12 +137,11 @@ def info(ctx):
click.echo(f"日志级别:{config.log_level}")
click.echo(f"权限检查:{'启用' if config.permission_check else '禁用'}")
# 小提示:有些功能需要特殊方式访问 (4675747572654f53)
# 随机显示隐藏提示(约 10% 概率)
# 彩蛋提示
click.echo("")
if random.random() < 0.1: # 10% 概率显示彩蛋提示
if random.random() < 0.1:
click.echo("✨ 奇怪的提示:试试在命令前加两个感叹号会怎样?比如 !!help")
elif random.random() < 0.05: # 额外 5% 概率显示另一种提示
elif random.random() < 0.05:
click.echo("🤔 听说有人用 !! 开头的命令发现了不得了的东西...")
@@ -122,49 +154,233 @@ def cli_command(connect_host, connect_port):
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():
# 检测是否通过已弃用的 oss 命令调用
cmd_name = os.path.basename(sys.argv[0])
if cmd_name in ("oss", "oss.exe"):
print("╔══════════════════════════════════════════╗")
print("║ ⚠ oss 命令已弃用,请使用 nebula 替代 ║")
print("║ 例如: nebula serve ║")
print("║ nebula info ║")
print("║ nebula version ║")
print("╚══════════════════════════════════════════╝")
Log.warn("NebulaShell", "oss 命令已弃用,请使用 nebula 替代")
sys.exit(1)
# 检查隐藏命令前缀
if len(sys.argv) > 1 and sys.argv[1].startswith("!!"):
if _ACHIEVEMENTS_ENABLED:
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)
return
else:
print(f"未知命令:!!{cmd}")
return
else:
print("成就系统未启用")
return
if _handle_hidden_command():
return
cli()

View File

@@ -49,6 +49,13 @@ class Config:
# 性能配置
"MAX_WORKERS": 4,
"ENABLE_ASYNC": False,
# NBPF 配置
"NBPF_KEYS_DIR": "./data/nbpf-keys",
"NBPF_TRUSTED_KEYS_DIR": "./data/nbpf-keys/trusted",
"NBPF_RSA_KEYS_DIR": "./data/nbpf-keys/rsa",
"NBPF_ENCRYPTION_ENABLED": True,
"NBPF_SIGNATURE_REQUIRED": True,
}
def __init__(self, config_file: Optional[str] = None):
@@ -74,7 +81,7 @@ class Config:
self._config[key] = value
# 隐藏成就:配置黑客 - 记录配置修改
if _ACHIEVEMENTS_ENABLED:
if Config._ACHIEVEMENTS_ENABLED:
try:
from oss.core.achievements import get_validator
validator = get_validator()

1687
oss/core/engine.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
"""中间件链 - 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
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 ""
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("Core", 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("Core", f"{req.method} {req.path}")
return next_fn()
class MiddlewareChain:
"""中间件链"""
def __init__(self):
self.middlewares: list[Middleware] = []
self.add(CorsMiddleware())
self.add(AuthMiddleware())
self.add(LoggerMiddleware())
self.add(RateLimitMiddleware())
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

@@ -0,0 +1,138 @@
"""
限流工具 - 令牌桶限流器
"""
import time
import threading
from typing import Dict, Callable, Optional
from collections import defaultdict, deque
from oss.config import get_config
from oss.core.http_api.server import Request, Response
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
class RateLimitMiddleware:
"""限流中间件 - 防止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
},
}
# 全局限流配置
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 = request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", ""))
if not ip:
ip = request.headers.get("Remote-Addr", "unknown")
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()

View File

@@ -3,6 +3,7 @@ import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Any
from oss.config import get_config
from oss.logger.logger import Log
class Request:
@@ -40,13 +41,13 @@ class HttpServer:
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}")
Log.info("Core", f"HTTP 服务器启动: {self.host}:{self.port}")
def stop(self):
"""停止服务器"""
if self._server:
self._server.shutdown()
print("[http-api] 服务器已停止")
Log.info("Core", "HTTP 服务器已停止")
def _create_handler(self):
"""创建请求处理器"""
@@ -118,6 +119,6 @@ class HttpServer:
pass # 忽略客户端断开
def log_message(self, format, *args):
pass
Log.debug("Core", format % args)
return Handler

18
oss/core/nbpf/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
"""Nebula Plugin File (.nbpf) — 插件打包与加密系统
提供:
- 多重签名 + 多重加密Ed25519 + RSA-4096 + AES-256-GCM + HMAC-SHA256
- NIR (Nebula Intermediate Representation) 编译
- .nbpf 文件打包/解包/加载
"""
from .crypto import NBPCrypto, NBPCryptoError
from .compiler import NIRCompiler, NIRCompileError
from .format import NBPFFormatter, NBPFPacker, NBPFUnpacker, NBPFFormatError
from .loader import NBPFLoader, NBPFLoadError
__all__ = [
"NBPCrypto", "NBPCryptoError",
"NIRCompiler", "NIRCompileError",
"NBPFFormatter", "NBPFPacker", "NBPFUnpacker", "NBPFFormatError",
"NBPFLoader", "NBPFLoadError",
]

271
oss/core/nbpf/compiler.py Normal file
View File

@@ -0,0 +1,271 @@
"""NIR (Nebula Intermediate Representation) 编译器
将 Python 插件源码编译为序列化 code object实现"一次编译,到处运行"
NIR 基于 Python 原生 code object + marshal 序列化:
- 任何 Python 3.10+ 平台均可执行
- 不依赖特定 CPU 架构或操作系统
- 编译时拒绝 C 扩展,保证纯 Python 可移植性
"""
import ast
import marshal
import types
import sys
import random
from pathlib import Path
from typing import Optional
class NIRCompileError(Exception):
"""NIR 编译错误"""
pass
class NIRCompiler:
"""NIR 编译器 — Python 源码 ↔ 序列化 code object"""
# 允许的 Python 字节码版本范围
MIN_PY_VERSION = (3, 10)
MAX_PY_VERSION = (3, 13)
# 禁止导入的 C 扩展模块
FORBIDDEN_C_EXTENSIONS = {
".so", ".pyd", ".dll", ".dylib",
}
# 禁止导入的危险模块
FORBIDDEN_MODULES = {
"os", "sys", "subprocess", "shutil", "socket",
"ctypes", "cffi", "multiprocessing", "threading",
"signal", "fcntl", "termios", "ptty", "grp", "pwd",
"resource", "syslog", "crypt",
}
def __init__(self, obfuscate: bool = True):
self.obfuscate = obfuscate
# ── 编译 ──
def compile_source(self, source: str, filename: str = "<nbpf>") -> bytes:
"""将 Python 源码编译为序列化的 code object
Args:
source: Python 源码
filename: 文件名(用于错误报告)
Returns:
序列化的 code object (bytes)
Raises:
NIRCompileError: 编译失败
"""
try:
# 静态安全检查
self._static_check(source, filename)
# 编译为 code object
code = compile(source, filename, 'exec')
# 可选:插入花指令混淆
if self.obfuscate:
code = self._obfuscate_code(code)
# 序列化
return marshal.dumps(code)
except SyntaxError as e:
raise NIRCompileError(f"语法错误: {e}") from e
except NIRCompileError:
raise
except Exception as e:
raise NIRCompileError(f"编译失败: {type(e).__name__}: {e}") from e
def compile_plugin(self, plugin_dir: Path) -> dict[str, bytes]:
"""编译整个插件目录为 NIR
Args:
plugin_dir: 插件目录路径
Returns:
{module_name: nir_bytes} 字典
"""
if not plugin_dir.exists():
raise NIRCompileError(f"插件目录不存在: {plugin_dir}")
# 拒绝 C 扩展
self._reject_c_extensions(plugin_dir)
# 收集所有 .py 文件
sources = self._collect_sources(plugin_dir)
if not sources:
raise NIRCompileError(f"插件目录中没有 .py 文件: {plugin_dir}")
# 编译每个文件
nir_data = {}
for rel_path, source in sources.items():
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))
return nir_data
def _collect_sources(self, plugin_dir: Path) -> dict[str, str]:
"""收集插件目录下所有 .py 文件源码
Returns:
{相对路径: 源码} 字典
"""
sources = {}
for file_path in sorted(plugin_dir.rglob("*.py")):
# 跳过 __pycache__
if "__pycache__" in file_path.parts:
continue
rel_path = str(file_path.relative_to(plugin_dir))
try:
source = file_path.read_text(encoding="utf-8")
sources[rel_path] = source
except Exception as e:
raise NIRCompileError(f"读取文件失败 {rel_path}: {e}") from e
return sources
# ── 反序列化 ──
@staticmethod
def deserialize_nir(nir_data: bytes) -> types.CodeType:
"""反序列化 NIR 数据为 code object
Args:
nir_data: 序列化的 code object (bytes)
Returns:
code object
"""
try:
code = marshal.loads(nir_data)
if not isinstance(code, types.CodeType):
raise NIRCompileError("反序列化结果不是 code object")
return code
except Exception as e:
raise NIRCompileError(f"NIR 反序列化失败: {e}") from e
@staticmethod
def create_function(code: types.CodeType, globals_dict: dict) -> types.FunctionType:
"""从 code object 创建可调用函数
Args:
code: code object
globals_dict: 全局命名空间
Returns:
可调用的函数对象
"""
return types.FunctionType(code, globals_dict)
# ── 静态安全检查 ──
def _static_check(self, source: str, filename: str):
"""静态源码安全检查"""
try:
tree = ast.parse(source, filename=filename)
except SyntaxError:
raise
for node in ast.walk(tree):
# 检查 import 语句
if isinstance(node, ast.Import):
for alias in node.names:
self._check_module(alias.name, node.lineno)
# 检查 from ... import 语句
elif isinstance(node, ast.ImportFrom):
if node.module:
self._check_module(node.module, node.lineno)
# 检查 __import__ 调用
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id == "__import__":
raise NIRCompileError(
f"{filename}:{node.lineno} - 禁止使用 __import__()"
)
# 检查 exec/eval/compile 调用
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id in ("exec", "eval", "compile"):
raise NIRCompileError(
f"{filename}:{node.lineno} - 禁止使用 {node.func.id}()"
)
def _check_module(self, module_name: str, lineno: int):
"""检查模块是否被禁止"""
base = module_name.split(".")[0]
if base in self.FORBIDDEN_MODULES:
raise NIRCompileError(
f"{lineno} 行 - 禁止导入系统模块: '{module_name}'"
)
def _reject_c_extensions(self, plugin_dir: Path):
"""拒绝 C 扩展"""
for ext in self.FORBIDDEN_C_EXTENSIONS:
for f in plugin_dir.rglob(f"*{ext}"):
raise NIRCompileError(
f"插件包含 C 扩展,拒绝编译: {f.relative_to(plugin_dir)}"
)
# ── 花指令混淆 ──
def _obfuscate_code(self, code: types.CodeType) -> types.CodeType:
"""向 code object 中插入无害垃圾代码(花指令)
通过修改 code object 的 co_consts 插入无意义的常量,
增加逆向分析难度。
"""
# 只对非空代码进行混淆
if not code.co_code or len(code.co_consts) == 0:
return code
# 生成无害的垃圾常量
junk_consts = [
None,
42,
"NebulaShell",
True,
False,
]
# 随机选择垃圾常量插入
junk = random.choice(junk_consts)
# 修改 co_consts在末尾添加垃圾常量
# 注意:这不会影响代码执行,因为 co_consts 中的额外条目不会被引用
new_consts = list(code.co_consts) + [junk]
# 递归混淆子 code object
new_child_consts = []
for child in code.co_consts:
if isinstance(child, types.CodeType):
new_child_consts.append(self._obfuscate_code(child))
else:
new_child_consts.append(child)
# 重建 code object
try:
new_code = code.replace(
co_consts=tuple(new_child_consts + [junk]),
)
return new_code
except AttributeError:
# Python 3.7 及以下不支持 replace
return code
# ── 工具方法 ──
@staticmethod
def check_python_version() -> bool:
"""检查 Python 版本是否支持 NIR"""
ver = sys.version_info[:2]
if ver < NIRCompiler.MIN_PY_VERSION:
return False
if ver > NIRCompiler.MAX_PY_VERSION:
return False
return True

591
oss/core/nbpf/crypto.py Normal file
View File

@@ -0,0 +1,591 @@
"""多重签名 + 多重加密工具
加密层级(从外到内):
1. Ed25519 外层签名 — 验证包完整性
2. AES-256-GCM 外层加密 — 加密 META-INF/ 和 NIR/
3. RSA-4096-PSS 中层签名 — 验证插件作者身份
4. AES-256-GCM 中层加密 — 加密 NIR 数据
5. HMAC-SHA256 内层签名 — 验证每个模块
代码隐藏策略:
- 关键常量运行时计算
- 导入路径动态拼接
- 解密函数分散
- 反调试检测
- 内存擦除
"""
import os
import sys
import json
import hmac
import hashlib
import base64
import threading
from typing import Optional, Tuple
class NBPCryptoError(Exception):
"""NBPF 加密/解密错误"""
pass
class NBPCrypto:
"""多重签名 + 多重加密工具"""
# 关键常量通过运行时计算得出,不直接出现在源码中
@staticmethod
def _aes_key_len() -> int:
"""AES-256 密钥长度(运行时计算)"""
return 32 # 256 bits
@staticmethod
def _aes_nonce_len() -> int:
"""AES-GCM nonce 长度"""
return 12 # 96 bits
@staticmethod
def _aes_tag_len() -> int:
"""AES-GCM 认证标签长度"""
return 16 # 128 bits
@staticmethod
def _hmac_key_len() -> int:
"""HMAC 密钥派生长度"""
return 32
@staticmethod
def _rsa_key_size() -> int:
"""RSA 密钥大小"""
return 4096
# ── 混淆导入 ──
@staticmethod
def _imp_crypto() -> object:
"""混淆导入 cryptography.hazmat 模块"""
# 动态拼接导入路径,防止静态分析
_a = "cryptography"
_b = "hazmat"
_c = "primitives"
_d = "ciphers"
_e = "aead"
_f = "asymmetric"
_g = "serialization"
_h = "hashes"
_i = "padding"
_j = "backends"
_k = "ed25519"
_l = "rsa"
_m = "exceptions"
_n = "utils"
# 使用 __import__ 动态导入
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["AESGCM"])
@staticmethod
def _imp_ed25519() -> object:
"""混淆导入 Ed25519"""
_a = "cryptography"
_b = "hazmat"
_c = "primitives"
_d = "asymmetric"
_e = "ed25519"
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["Ed25519PrivateKey"])
@staticmethod
def _imp_rsa() -> object:
"""混淆导入 RSA"""
_a = "cryptography"
_b = "hazmat"
_c = "primitives"
_d = "asymmetric"
_e = "rsa"
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["generate_private_key"])
@staticmethod
def _imp_serialization() -> object:
"""混淆导入 serialization"""
_a = "cryptography"
_b = "hazmat"
_c = "primitives"
_d = "serialization"
return __import__(f"{_a}.{_b}.{_c}.{_d}", fromlist=["Encoding"])
@staticmethod
def _imp_hashes() -> object:
"""混淆导入 hashes"""
_a = "cryptography"
_b = "hazmat"
_c = "primitives"
_d = "hashes"
return __import__(f"{_a}.{_b}.{_c}.{_d}", fromlist=["SHA256"])
@staticmethod
def _imp_padding() -> object:
"""混淆导入 padding"""
_a = "cryptography"
_b = "hazmat"
_c = "primitives"
_d = "asymmetric"
_e = "padding"
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["OAEP"])
@staticmethod
def _imp_backends() -> object:
"""混淆导入 backends"""
_a = "cryptography"
_b = "hazmat"
_c = "backends"
return __import__(f"{_a}.{_b}.{_c}", fromlist=["default_backend"])
# ── 反调试检测 ──
@staticmethod
def _anti_debug_check() -> bool:
"""检测是否被调试,被调试时返回 True"""
try:
# Python 调试器会设置 sys.gettrace()
if sys.gettrace() is not None:
return True
# 检查常见的调试环境变量
debug_envs = ["PYTHONDEBUG", "PYTHONVERBOSE", "NEBULA_DEBUG"]
for env in debug_envs:
if os.environ.get(env, "").lower() in ("1", "true", "yes"):
return True
except Exception:
pass
return False
# ── 安全内存擦除 ──
@staticmethod
def _secure_wipe(data: bytearray):
"""安全擦除内存中的敏感数据"""
try:
length = len(data)
for i in range(length):
data[i] = 0
# 二次擦除,防止编译器优化
for i in range(length):
data[i] = 0xff
for i in range(length):
data[i] = 0
except Exception:
pass
# ── 密钥生成 ──
@staticmethod
def generate_aes_key() -> bytes:
"""生成 256 位 AES 密钥"""
return os.urandom(NBPCrypto._aes_key_len())
@staticmethod
def generate_ed25519_keypair() -> Tuple[bytes, bytes]:
"""生成 Ed25519 密钥对,返回 (private_key_bytes, public_key_bytes)"""
ed25519 = NBPCrypto._imp_ed25519()
serialization = NBPCrypto._imp_serialization()
private_key = ed25519.Ed25519PrivateKey.generate()
private_bytes = private_key.private_bytes(
serialization.Encoding.Raw,
serialization.PrivateFormat.Raw,
serialization.NoEncryption()
)
public_bytes = private_key.public_key().public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw
)
return private_bytes, public_bytes
@staticmethod
def generate_rsa_keypair(key_size: int = None) -> Tuple[bytes, bytes]:
"""生成 RSA 密钥对,返回 (private_key_pem, public_key_pem)"""
if key_size is None:
key_size = NBPCrypto._rsa_key_size()
rsa = NBPCrypto._imp_rsa()
serialization = NBPCrypto._imp_serialization()
backends = NBPCrypto._imp_backends()
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=backends.default_backend()
)
private_pem = private_key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()
)
public_pem = private_key.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo
)
return private_pem, public_pem
# ── 密钥派生 ──
@staticmethod
def derive_hmac_key(key1: bytes, key2: bytes) -> bytes:
"""从两个 AES 密钥派生 HMAC 密钥"""
# 使用 HKDF-like 派生
dig = hashlib.sha256()
dig.update(key1)
dig.update(key2)
dig.update(b"NebulaHMACv1")
return dig.digest()
# ── AES-256-GCM 加密/解密 ──
@staticmethod
def _aes_encrypt(data: bytes, key: bytes) -> Tuple[bytes, bytes, bytes]:
"""AES-256-GCM 加密,返回 (nonce, ciphertext, tag)"""
aead_mod = NBPCrypto._imp_crypto()
aesgcm = aead_mod.AESGCM(key)
nonce = os.urandom(NBPCrypto._aes_nonce_len())
ciphertext = aesgcm.encrypt(nonce, data, None)
# AESGCM.encrypt 返回 nonce || ciphertext || tag
# 但我们需要分开,所以手动构造
tag = ciphertext[-NBPCrypto._aes_tag_len():]
ct = ciphertext[:-NBPCrypto._aes_tag_len()]
return nonce, ct, tag
@staticmethod
def _aes_decrypt(ciphertext: bytes, key: bytes, nonce: bytes, tag: bytes) -> bytes:
"""AES-256-GCM 解密"""
aead_mod = NBPCrypto._imp_crypto()
aesgcm = aead_mod.AESGCM(key)
# AESGCM.decrypt 期望 (nonce, ciphertext || tag, aad)
combined = ciphertext + tag
return aesgcm.decrypt(nonce, combined, None)
# ── 外层加密/解密 ──
@staticmethod
def outer_encrypt(data: bytes, key: bytes) -> dict:
"""外层 AES-256-GCM 加密,返回加密信息字典"""
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
return {
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ct).decode(),
"tag": base64.b64encode(tag).decode(),
}
@staticmethod
def outer_decrypt(enc_info: dict, key: bytes) -> bytes:
"""外层 AES-256-GCM 解密"""
nonce = base64.b64decode(enc_info["nonce"])
ct = base64.b64decode(enc_info["ciphertext"])
tag = base64.b64decode(enc_info["tag"])
return NBPCrypto._aes_decrypt(ct, key, nonce, tag)
# ── 中层加密/解密 ──
@staticmethod
def inner_encrypt(data: bytes, key: bytes) -> dict:
"""中层 AES-256-GCM 加密"""
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
return {
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ct).decode(),
"tag": base64.b64encode(tag).decode(),
}
@staticmethod
def inner_decrypt(enc_info: dict, key: bytes) -> bytes:
"""中层 AES-256-GCM 解密"""
nonce = base64.b64decode(enc_info["nonce"])
ct = base64.b64decode(enc_info["ciphertext"])
tag = base64.b64decode(enc_info["tag"])
return NBPCrypto._aes_decrypt(ct, key, nonce, tag)
# ── Ed25519 外层签名/验签 ──
@staticmethod
def outer_sign(data: bytes, private_key: bytes) -> bytes:
"""Ed25519 签名"""
ed25519 = NBPCrypto._imp_ed25519()
key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
return key.sign(data)
@staticmethod
def outer_verify(data: bytes, signature: bytes, public_key: bytes) -> bool:
"""Ed25519 验签"""
try:
ed25519 = NBPCrypto._imp_ed25519()
key = ed25519.Ed25519PublicKey.from_public_bytes(public_key)
key.verify(signature, data)
return True
except Exception:
return False
# ── RSA-4096-PSS 中层签名/验签 ──
@staticmethod
def inner_sign(data: bytes, private_key_pem: bytes) -> bytes:
"""RSA-4096-PSS 签名"""
serialization = NBPCrypto._imp_serialization()
hashes_mod = NBPCrypto._imp_hashes()
padding_mod = NBPCrypto._imp_padding()
backends = NBPCrypto._imp_backends()
private_key = serialization.load_pem_private_key(
private_key_pem, password=None, backend=backends.default_backend()
)
signature = private_key.sign(
data,
padding_mod.PSS(
mgf=padding_mod.MGF1(hashes_mod.SHA256()),
salt_length=padding_mod.PSS.MAX_LENGTH
),
hashes_mod.SHA256()
)
return signature
@staticmethod
def inner_verify(data: bytes, signature: bytes, public_key_pem: bytes) -> bool:
"""RSA-4096-PSS 验签"""
try:
serialization = NBPCrypto._imp_serialization()
hashes_mod = NBPCrypto._imp_hashes()
padding_mod = NBPCrypto._imp_padding()
backends = NBPCrypto._imp_backends()
public_key = serialization.load_pem_public_key(
public_key_pem, backend=backends.default_backend()
)
public_key.verify(
signature, data,
padding_mod.PSS(
mgf=padding_mod.MGF1(hashes_mod.SHA256()),
salt_length=padding_mod.PSS.MAX_LENGTH
),
hashes_mod.SHA256()
)
return True
except Exception:
return False
# ── HMAC-SHA256 内层模块签名/验签 ──
@staticmethod
def module_sign(data: bytes, hmac_key: bytes) -> str:
"""HMAC-SHA256 模块签名"""
h = hmac.new(hmac_key, data, hashlib.sha256)
return base64.b64encode(h.digest()).decode()
@staticmethod
def module_verify(data: bytes, signature: str, hmac_key: bytes) -> bool:
"""HMAC-SHA256 模块验签"""
expected = NBPCrypto.module_sign(data, hmac_key)
return hmac.compare_digest(expected, signature)
# ── RSA-OAEP 密钥封装 ──
@staticmethod
def encrypt_key(aes_key: bytes, rsa_public_key_pem: bytes) -> str:
"""RSA-OAEP 加密 AES 密钥"""
serialization = NBPCrypto._imp_serialization()
hashes_mod = NBPCrypto._imp_hashes()
padding_mod = NBPCrypto._imp_padding()
backends = NBPCrypto._imp_backends()
public_key = serialization.load_pem_public_key(
rsa_public_key_pem, backend=backends.default_backend()
)
encrypted = public_key.encrypt(
aes_key,
padding_mod.OAEP(
mgf=padding_mod.MGF1(algorithm=hashes_mod.SHA256()),
algorithm=hashes_mod.SHA256(),
label=None
)
)
return base64.b64encode(encrypted).decode()
@staticmethod
def decrypt_key(encrypted_key: str, rsa_private_key_pem: bytes) -> bytes:
"""RSA-OAEP 解密 AES 密钥"""
serialization = NBPCrypto._imp_serialization()
hashes_mod = NBPCrypto._imp_hashes()
padding_mod = NBPCrypto._imp_padding()
backends = NBPCrypto._imp_backends()
private_key = serialization.load_pem_private_key(
rsa_private_key_pem, password=None, backend=backends.default_backend()
)
encrypted = base64.b64decode(encrypted_key)
aes_key = private_key.decrypt(
encrypted,
padding_mod.OAEP(
mgf=padding_mod.MGF1(algorithm=hashes_mod.SHA256()),
algorithm=hashes_mod.SHA256(),
label=None
)
)
return aes_key
# ── 密钥文件读写 ──
@staticmethod
def save_key_to_pem(key_bytes: bytes, path: str, is_private: bool = False):
"""保存密钥到 PEM 文件"""
import os as _os
dir_path = _os.path.dirname(path)
if dir_path:
_os.makedirs(dir_path, exist_ok=True)
with open(path, "wb") as f:
f.write(key_bytes)
@staticmethod
def load_key_from_pem(path: str) -> bytes:
"""从 PEM 文件加载密钥"""
with open(path, "rb") as f:
return f.read()
# ── 完整加密流程(打包时使用) ──
@staticmethod
def full_encrypt_package(
nir_data: dict[str, bytes],
manifest: dict,
ed25519_private_key: bytes,
rsa_private_key_pem: bytes,
rsa_public_key_pem: bytes,
) -> dict:
"""完整加密打包流程
返回包含所有加密/签名信息的字典,供 NBPFPacker 使用
"""
# 1. 生成两个 AES 密钥
key1 = NBPCrypto.generate_aes_key()
key2 = NBPCrypto.generate_aes_key()
# 2. 派生 HMAC 密钥
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
# 3. 中层加密:用 key2 加密每个 NIR 模块
inner_encrypted = {}
for mod_name, mod_data in nir_data.items():
inner_encrypted[mod_name] = NBPCrypto.inner_encrypt(mod_data, key2)
# 4. 中层签名:用 RSA 签名 NIR 数据摘要
nir_digest = hashlib.sha256()
for mod_name in sorted(inner_encrypted.keys()):
nir_digest.update(mod_name.encode())
nir_digest.update(inner_encrypted[mod_name]["ciphertext"].encode())
inner_signature = NBPCrypto.inner_sign(nir_digest.digest(), rsa_private_key_pem)
# 5. 内层签名:用 HMAC 签名每个模块
module_sigs = {}
for mod_name, mod_data in nir_data.items():
module_sigs[mod_name] = NBPCrypto.module_sign(mod_data, hmac_key)
# 6. 构建 META-INF 数据(用于外层加密)
meta_inf = {
"manifest": manifest,
"inner_signature": base64.b64encode(inner_signature).decode(),
"inner_encryption": {
"algorithm": "AES-256-GCM",
"encrypted_key": NBPCrypto.encrypt_key(key2, rsa_public_key_pem),
},
"module_signatures": module_sigs,
}
# 7. 外层加密:用 key1 加密 META-INF 数据
meta_inf_bytes = json.dumps(meta_inf).encode("utf-8")
outer_encrypted = NBPCrypto.outer_encrypt(meta_inf_bytes, key1)
# 8. 外层签名:用 Ed25519 签名整个包摘要
package_digest = hashlib.sha256()
package_digest.update(json.dumps(outer_encrypted).encode())
for mod_name in sorted(inner_encrypted.keys()):
package_digest.update(mod_name.encode())
package_digest.update(inner_encrypted[mod_name]["ciphertext"].encode())
outer_signature = NBPCrypto.outer_sign(package_digest.digest(), ed25519_private_key)
# 9. 返回结果
return {
"outer_encryption": {
"algorithm": "AES-256-GCM",
"encrypted_key": NBPCrypto.encrypt_key(key1, rsa_public_key_pem),
"data": outer_encrypted,
},
"outer_signature": base64.b64encode(outer_signature).decode(),
"inner_encrypted": inner_encrypted,
"inner_signature": base64.b64encode(inner_signature).decode(),
"inner_encryption": meta_inf["inner_encryption"],
"module_signatures": module_sigs,
"hmac_key_derivation": "SHA256(key1+key2+NebulaHMACv1)",
}
# ── 完整解密流程(加载时使用) ──
@staticmethod
def full_decrypt_package(
package_info: dict,
ed25519_public_key: bytes,
rsa_private_key_pem: bytes,
) -> dict[str, bytes]:
"""完整解密流程,返回 NIR 数据字典 {module_name: nir_bytes}"""
# 反调试检测
if NBPCrypto._anti_debug_check():
raise NBPCryptoError("调试器检测到,拒绝解密")
# 1. 外层验签
outer_sig = base64.b64decode(package_info["outer_signature"])
package_digest = hashlib.sha256()
package_digest.update(json.dumps(package_info["outer_encryption"]["data"]).encode())
for mod_name in sorted(package_info["inner_encrypted"].keys()):
package_digest.update(mod_name.encode())
package_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
if not NBPCrypto.outer_verify(package_digest.digest(), outer_sig, ed25519_public_key):
raise NBPCryptoError("外层签名验证失败,包可能被篡改")
# 2. 外层解密:用 RSA 私钥解密 key1
key1_encrypted = package_info["outer_encryption"]["encrypted_key"]
key1 = NBPCrypto.decrypt_key(key1_encrypted, rsa_private_key_pem)
key1_buf = bytearray(key1)
# 3. 解密 META-INF 数据
meta_inf_bytes = NBPCrypto.outer_decrypt(
package_info["outer_encryption"]["data"], key1
)
NBPCrypto._secure_wipe(key1_buf)
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
# 4. 中层验签
inner_sig = base64.b64decode(meta_inf["inner_signature"])
nir_digest = hashlib.sha256()
for mod_name in sorted(package_info["inner_encrypted"].keys()):
nir_digest.update(mod_name.encode())
nir_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
# 需要 RSA 公钥来验签,从 meta_inf 中获取
# 实际使用时RSA 公钥应该从信任的密钥目录加载
# 这里假设调用者已经验证过 RSA 公钥
# 5. 中层解密:用 RSA 私钥解密 key2
key2_encrypted = meta_inf["inner_encryption"]["encrypted_key"]
key2 = NBPCrypto.decrypt_key(key2_encrypted, rsa_private_key_pem)
key2_buf = bytearray(key2)
# 6. 派生 HMAC 密钥
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
# key1 已经擦除key2 即将擦除
NBPCrypto._secure_wipe(bytearray(key2))
# 7. 解密 NIR 数据
nir_result = {}
for mod_name, enc_info in package_info["inner_encrypted"].items():
mod_data = NBPCrypto.inner_decrypt(enc_info, key2)
nir_result[mod_name] = mod_data
# 8. 内层验签
module_sigs = meta_inf.get("module_signatures", {})
for mod_name, mod_data in nir_result.items():
expected_sig = module_sigs.get(mod_name)
if expected_sig:
if not NBPCrypto.module_verify(mod_data, expected_sig, hmac_key):
raise NBPCryptoError(f"模块 '{mod_name}' HMAC 签名验证失败")
return nir_result

349
oss/core/nbpf/format.py Normal file
View File

@@ -0,0 +1,349 @@
""".nbpf 文件格式定义和打包/解包工具
.nbpf 文件结构ZIP 格式):
```
.nbpf (ZIP)
├── META-INF/
│ ├── MANIFEST.MF # 插件元数据(明文)
│ ├── SIGNATURE # 外层 Ed25519 签名(明文)
│ ├── SIGNER.PEM # 外层签名者公钥(明文)
│ ├── ENCRYPTION # 外层加密信息RSA-OAEP 加密的 AES 密钥1
│ ├── INNER_SIGNATURE # 中层 RSA-4096 签名(加密存储)
│ ├── INNER_ENCRYPTION # 中层加密信息RSA-OAEP 加密的 AES 密钥2
│ └── MODULE_SIGS # 内层 HMAC 签名列表(加密存储)
├── NIR/
│ ├── main # 主模块 NIR双重加密
│ ├── sub_module # 子模块 NIR双重加密
│ └── ...
└── RES/
├── manifest.json # 原始 manifest明文
├── config.py # 配置文件(可选,明文)
├── extensions.py # 扩展配置(可选,明文)
└── ... # 其他资源文件(明文)
```
"""
import json
import zipfile
import io
import os
import hashlib
import base64
from pathlib import Path
from typing import Optional
from oss.logger.logger import Log
from .crypto import NBPCrypto, NBPCryptoError
from .compiler import NIRCompiler, NIRCompileError
class NBPFFormatError(Exception):
""".nbpf 格式错误"""
pass
class NBPFFormatter:
""".nbpf 文件格式常量"""
MAGIC = b"NBPF"
VERSION = 1
ENTRY_POINT = "main"
# ZIP 内部路径
META_INF = "META-INF/"
NIR_DIR = "NIR/"
RES_DIR = "RES/"
# META-INF 文件
MANIFEST = META_INF + "MANIFEST.MF"
SIGNATURE = META_INF + "SIGNATURE"
SIGNER_PEM = META_INF + "SIGNER.PEM"
ENCRYPTION = META_INF + "ENCRYPTION"
INNER_SIGNATURE = META_INF + "INNER_SIGNATURE"
INNER_ENCRYPTION = META_INF + "INNER_ENCRYPTION"
MODULE_SIGS = META_INF + "MODULE_SIGS"
# 跳过列表(打包时排除的文件)
SKIP_FILES = {"__pycache__", "SIGNATURE", ".DS_Store", "Thumbs.db"}
class NBPFPacker:
""".nbpf 打包工具 — 将插件目录打包为 .nbpf 文件"""
def __init__(self, crypto: NBPCrypto = None, compiler: NIRCompiler = None):
self.crypto = crypto or NBPCrypto()
self.compiler = compiler or NIRCompiler()
def pack(
self,
plugin_dir: Path,
output_path: Path,
ed25519_private_key: bytes,
rsa_private_key_pem: bytes,
rsa_public_key_pem: bytes,
ed25519_public_key: bytes = None,
signer_name: str = "unknown",
) -> Path:
"""将插件目录打包为 .nbpf 文件
Args:
plugin_dir: 插件目录路径
output_path: 输出 .nbpf 文件路径
ed25519_private_key: Ed25519 私钥(外层签名)
rsa_private_key_pem: RSA 私钥 PEM中层签名
rsa_public_key_pem: RSA 公钥 PEM用于加密 AES 密钥)
ed25519_public_key: Ed25519 公钥存入包内None 则自动派生)
signer_name: 签名者名称
Returns:
输出文件路径
Raises:
NBPFFormatError: 打包失败
"""
if not plugin_dir.exists():
raise NBPFFormatError(f"插件目录不存在: {plugin_dir}")
# 确保输出目录存在
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
# 1. 读取 manifest
manifest = self._read_manifest(plugin_dir)
# 2. 编译所有 .py 文件为 NIR
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
nir_data = self.compiler.compile_plugin(plugin_dir)
# 3. 收集资源文件
res_files = self._collect_resources(plugin_dir)
# 4. 完整加密打包
Log.info("NBPF", "加密打包中...")
package_info = self.crypto.full_encrypt_package(
nir_data=nir_data,
manifest=manifest,
ed25519_private_key=ed25519_private_key,
rsa_private_key_pem=rsa_private_key_pem,
rsa_public_key_pem=rsa_public_key_pem,
)
# 5. 构建 ZIP 包
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
# META-INF/MANIFEST.MF
zf.writestr(NBPFFormatter.MANIFEST, json.dumps(manifest, indent=2))
# META-INF/SIGNATURE
zf.writestr(NBPFFormatter.SIGNATURE, package_info["outer_signature"])
# META-INF/SIGNER.PEM
if ed25519_public_key:
zf.writestr(NBPFFormatter.SIGNER_PEM, ed25519_public_key)
else:
# 从私钥派生公钥
ed25519_mod = NBPCrypto._imp_ed25519()
key = ed25519_mod.Ed25519PrivateKey.from_private_bytes(ed25519_private_key)
pub_bytes = key.public_key().public_bytes(
NBPCrypto._imp_serialization().Encoding.Raw,
NBPCrypto._imp_serialization().PublicFormat.Raw
)
zf.writestr(NBPFFormatter.SIGNER_PEM, pub_bytes)
# META-INF/ENCRYPTION
zf.writestr(NBPFFormatter.ENCRYPTION, json.dumps(package_info["outer_encryption"]))
# META-INF/INNER_SIGNATURE
zf.writestr(NBPFFormatter.INNER_SIGNATURE, package_info["inner_signature"])
# META-INF/INNER_ENCRYPTION
zf.writestr(NBPFFormatter.INNER_ENCRYPTION, json.dumps(package_info["inner_encryption"]))
# META-INF/MODULE_SIGS
zf.writestr(NBPFFormatter.MODULE_SIGS, json.dumps(package_info["module_signatures"]))
# NIR/ 目录
for mod_name, enc_info in package_info["inner_encrypted"].items():
nir_path = NBPFFormatter.NIR_DIR + mod_name
zf.writestr(nir_path, json.dumps(enc_info))
# RES/ 目录
for res_path, res_data in res_files.items():
zf.writestr(NBPFFormatter.RES_DIR + res_path, res_data)
Log.ok("NBPF", f"打包完成: {output_path}")
return output_path
except NIRCompileError as e:
raise NBPFFormatError(f"编译失败: {e}") from e
except NBPCryptoError as e:
raise NBPFFormatError(f"加密失败: {e}") from e
except Exception as e:
raise NBPFFormatError(f"打包失败: {type(e).__name__}: {e}") from e
def _read_manifest(self, plugin_dir: Path) -> dict:
"""读取插件 manifest.json"""
manifest_file = plugin_dir / "manifest.json"
if not manifest_file.exists():
# 生成默认 manifest
return {
"metadata": {
"name": plugin_dir.name,
"version": "1.0.0",
"author": "unknown",
"description": "",
},
"config": {"enabled": True, "args": {}},
"dependencies": [],
"permissions": [],
}
try:
return json.loads(manifest_file.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise NBPFFormatError(f"manifest.json 格式错误: {e}") from e
def _collect_resources(self, plugin_dir: Path) -> dict[str, bytes]:
"""收集资源文件(非 .py 文件)"""
resources = {}
for file_path in sorted(plugin_dir.rglob("*")):
if not file_path.is_file():
continue
rel_path = str(file_path.relative_to(plugin_dir))
# 跳过
skip = False
for skip_name in NBPFFormatter.SKIP_FILES:
if skip_name in file_path.parts:
skip = True
break
if skip:
continue
# 跳过 .py 文件(已编译为 NIR
if file_path.suffix == ".py":
continue
# 跳过 manifest.json已单独处理
if file_path.name == "manifest.json":
continue
try:
resources[rel_path] = file_path.read_bytes()
except Exception as e:
Log.warn("NBPF", f"跳过资源文件 {rel_path}: {e}")
return resources
class NBPFUnpacker:
""".nbpf 解包工具 — 解包 .nbpf 文件到目录"""
def __init__(self, crypto: NBPCrypto = None):
self.crypto = crypto or NBPCrypto()
def unpack(self, nbpf_path: Path, output_dir: Path) -> Path:
"""解包 .nbpf 到目录(用于调试/开发)
Args:
nbpf_path: .nbpf 文件路径
output_dir: 输出目录
Returns:
输出目录路径
"""
if not nbpf_path.exists():
raise NBPFFormatError(f".nbpf 文件不存在: {nbpf_path}")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(nbpf_path, 'r') as zf:
# 提取所有文件
for info in zf.infolist():
# 跳过目录
if info.filename.endswith("/"):
continue
# 计算输出路径
out_path = output_dir / info.filename
# 创建父目录
out_path.parent.mkdir(parents=True, exist_ok=True)
# 写入文件
out_path.write_bytes(zf.read(info.filename))
Log.ok("NBPF", f"解包完成: {output_dir}")
return output_dir
def extract_manifest(self, nbpf_path: Path) -> dict:
"""提取 manifest.json不解密"""
with zipfile.ZipFile(nbpf_path, 'r') as zf:
if NBPFFormatter.MANIFEST not in zf.namelist():
raise NBPFFormatError(".nbpf 文件中缺少 MANIFEST.MF")
return json.loads(zf.read(NBPFFormatter.MANIFEST).decode("utf-8"))
def verify_signature(
self,
nbpf_path: Path,
trusted_keys: dict[str, bytes],
) -> tuple[bool, str]:
"""验证 .nbpf 文件的外层 Ed25519 签名
签名计算方式与 full_encrypt_package 一致。
Args:
nbpf_path: .nbpf 文件路径
trusted_keys: {signer_name: ed25519_public_key_bytes} 信任的公钥字典
Returns:
(是否通过, 消息)
"""
try:
with zipfile.ZipFile(nbpf_path, 'r') as zf:
# 读取签名和签名者公钥
if NBPFFormatter.SIGNATURE not in zf.namelist():
return False, "缺少 SIGNATURE 文件"
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
return False, "缺少 SIGNER.PEM 文件"
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
# 查找匹配的信任公钥
matched = False
matched_name = None
for name, trusted_key in trusted_keys.items():
if trusted_key == signer_pub_key:
matched = True
matched_name = name
break
if not matched:
return False, "签名者公钥不在信任列表中"
# 计算包摘要(与 full_encrypt_package 一致)
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
digest = hashlib.sha256()
digest.update(json.dumps(encryption_data["data"]).encode())
# 按模块名排序,添加模块名和密文
nir_modules = {}
for info in zf.infolist():
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
nir_modules[mod_name] = mod_data
for mod_name in sorted(nir_modules.keys()):
digest.update(mod_name.encode())
digest.update(nir_modules[mod_name]["ciphertext"].encode())
# 验签
signature = base64.b64decode(signature_b64)
if self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
return True, f"签名验证通过 (signer: {matched_name})"
else:
return False, "签名验证失败,包可能被篡改"
except Exception as e:
return False, f"签名验证异常: {type(e).__name__}: {e}"

360
oss/core/nbpf/loader.py Normal file
View File

@@ -0,0 +1,360 @@
""".nbpf 加载器 — 加载 .nbpf 文件到运行时环境
加载流程:
1. 打开 .nbpf (ZIP) 文件
2. 外层验签:用 Ed25519 公钥验证包签名
3. 外层解密:用 RSA 私钥解密密钥1解密 META-INF/
4. 中层验签:用 RSA-4096 公钥验证 NIR 签名
5. 中层解密:用 RSA 私钥解密密钥2解密 NIR 数据
6. 内层验签:用 HMAC 验证每个模块签名
7. 反序列化 NIR 为 code object
8. 在受限沙箱中执行
9. 内存擦除所有密钥
"""
import json
import zipfile
import sys
import types
import hashlib
import base64
from pathlib import Path
from typing import Any, Optional
from oss.logger.logger import Log
from .crypto import NBPCrypto, NBPCryptoError
from .compiler import NIRCompiler, NIRCompileError
from .format import NBPFFormatter, NBPFFormatError
class NBPFLoadError(Exception):
""".nbpf 加载错误"""
pass
class NBPFLoader:
""".nbpf 加载器"""
def __init__(
self,
crypto: NBPCrypto = None,
compiler: NIRCompiler = None,
trusted_ed25519_keys: dict[str, bytes] = None,
trusted_rsa_keys: dict[str, bytes] = None,
rsa_private_key: bytes = None,
):
"""
Args:
crypto: 加密工具实例
compiler: 编译器实例
trusted_ed25519_keys: {signer_name: ed25519_public_key_bytes}
trusted_rsa_keys: {signer_name: rsa_public_key_pem}
rsa_private_key: RSA 私钥 PEM用于解密 AES 密钥)
"""
self.crypto = crypto or NBPCrypto()
self.compiler = compiler or NIRCompiler()
self.trusted_ed25519_keys = trusted_ed25519_keys or {}
self.trusted_rsa_keys = trusted_rsa_keys or {}
self.rsa_private_key = rsa_private_key
def load(
self,
nbpf_path: Path,
plugin_name: str = None,
) -> tuple[Any, dict]:
"""加载 .nbpf 插件
Args:
nbpf_path: .nbpf 文件路径
plugin_name: 插件名称(用于日志,默认从 manifest 读取)
Returns:
(plugin_instance, plugin_info_dict)
Raises:
NBPFLoadError: 加载失败
"""
if not nbpf_path.exists():
raise NBPFLoadError(f".nbpf 文件不存在: {nbpf_path}")
try:
with zipfile.ZipFile(nbpf_path, 'r') as zf:
# 1. 外层验签
signer_name = self._verify_outer_signature(zf)
Log.info("NBPF", f"外层签名验证通过 (signer: {signer_name})")
# 2. 外层解密
key1, meta_inf = self._decrypt_outer(zf)
key1_buf = bytearray(key1)
# 3. 中层验签
rsa_signer = self._verify_inner_signature(zf, meta_inf)
Log.info("NBPF", f"中层签名验证通过 (signer: {rsa_signer})")
# 4. 中层解密
key2 = self._decrypt_inner(meta_inf)
key2_buf = bytearray(key2)
# 5. 派生 HMAC 密钥
hmac_key = self.crypto.derive_hmac_key(key1, key2)
self.crypto._secure_wipe(key1_buf)
self.crypto._secure_wipe(key2_buf)
# 6. 解密 NIR 数据
nir_data = self._decrypt_nir_data(zf, key2)
# 7. 内层验签
self._verify_module_signatures(nir_data, meta_inf, hmac_key)
Log.info("NBPF", "内层模块签名验证通过")
# 8. 获取插件名称
manifest = meta_inf.get("manifest", {})
meta = manifest.get("metadata", {})
name = plugin_name or meta.get("name", nbpf_path.stem)
# 9. 反序列化并执行
instance, module = self._deserialize_and_exec(nir_data, name)
# 10. 构建插件信息
info = {
"name": name,
"version": meta.get("version", ""),
"author": meta.get("author", ""),
"description": meta.get("description", ""),
"manifest": manifest,
"nbpf_path": str(nbpf_path),
"signer": signer_name,
}
Log.ok("NBPF", f"插件 '{name}' 加载成功")
return instance, info
except (NBPFFormatError, NBPCryptoError, NIRCompileError) as e:
raise NBPFLoadError(str(e)) from e
except zipfile.BadZipFile as e:
raise NBPFLoadError(f".nbpf 文件损坏: {e}") from e
except Exception as e:
raise NBPFLoadError(f"加载失败: {type(e).__name__}: {e}") from e
# ── 外层验签 ──
def _verify_outer_signature(self, zf: zipfile.ZipFile) -> str:
"""外层 Ed25519 签名验证,返回签名者名称
签名计算方式与 full_encrypt_package 一致:
SHA256(outer_encryption_json + sorted_module_names_and_ciphertexts)
"""
if NBPFFormatter.SIGNATURE not in zf.namelist():
raise NBPFLoadError("缺少外层签名文件")
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
raise NBPFLoadError("缺少签名者公钥文件")
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
# 查找匹配的信任公钥
signer_name = None
for name, trusted_key in self.trusted_ed25519_keys.items():
if trusted_key == signer_pub_key:
signer_name = name
break
if signer_name is None:
raise NBPFLoadError("签名者公钥不在信任列表中")
# 计算包摘要(与 full_encrypt_package 一致)
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
digest = hashlib.sha256()
digest.update(json.dumps(encryption_data["data"]).encode())
# 按模块名排序,添加模块名和密文
nir_modules = {}
for info in zf.infolist():
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
nir_modules[mod_name] = mod_data
for mod_name in sorted(nir_modules.keys()):
digest.update(mod_name.encode())
digest.update(nir_modules[mod_name]["ciphertext"].encode())
# 验签
signature = base64.b64decode(signature_b64)
if not self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
raise NBPFLoadError("外层签名验证失败,包可能被篡改")
return signer_name
# ── 外层解密 ──
def _decrypt_outer(self, zf: zipfile.ZipFile) -> tuple[bytes, dict]:
"""外层解密,返回 (key1, meta_inf_dict)"""
if NBPFFormatter.ENCRYPTION not in zf.namelist():
raise NBPFLoadError("缺少外层加密信息")
encryption_info = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
# 用 RSA 私钥解密 key1
if self.rsa_private_key is None:
raise NBPFLoadError("未配置 RSA 私钥,无法解密")
key1 = self.crypto.decrypt_key(encryption_info["encrypted_key"], self.rsa_private_key)
# 解密 META-INF 数据
meta_inf_bytes = self.crypto.outer_decrypt(encryption_info["data"], key1)
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
return key1, meta_inf
# ── 中层验签 ──
def _verify_inner_signature(self, zf: zipfile.ZipFile, meta_inf: dict) -> str:
"""中层 RSA-4096 签名验证,返回签名者名称
签名计算方式与 full_encrypt_package 一致:
SHA256(sorted_module_names + inner_encrypted_ciphertexts)
"""
inner_sig_b64 = meta_inf.get("inner_signature")
if not inner_sig_b64:
raise NBPFLoadError("缺少中层签名")
# 计算 NIR 数据摘要(与 full_encrypt_package 一致)
nir_modules = {}
for info in zf.infolist():
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
nir_modules[mod_name] = mod_data
nir_digest = hashlib.sha256()
for mod_name in sorted(nir_modules.keys()):
nir_digest.update(mod_name.encode())
nir_digest.update(nir_modules[mod_name]["ciphertext"].encode())
# 查找匹配的 RSA 公钥
inner_sig = base64.b64decode(inner_sig_b64)
for name, rsa_pub_key in self.trusted_rsa_keys.items():
if self.crypto.inner_verify(nir_digest.digest(), inner_sig, rsa_pub_key):
return name
raise NBPFLoadError("中层签名验证失败,无法匹配任何信任的 RSA 公钥")
# ── 中层解密 ──
def _decrypt_inner(self, meta_inf: dict) -> bytes:
"""中层解密,返回 key2"""
inner_enc = meta_inf.get("inner_encryption", {})
encrypted_key = inner_enc.get("encrypted_key")
if not encrypted_key:
raise NBPFLoadError("缺少中层加密密钥")
if self.rsa_private_key is None:
raise NBPFLoadError("未配置 RSA 私钥,无法解密")
return self.crypto.decrypt_key(encrypted_key, self.rsa_private_key)
# ── 解密 NIR 数据 ──
def _decrypt_nir_data(self, zf: zipfile.ZipFile, key2: bytes) -> dict[str, bytes]:
"""解密 NIR 数据,返回 {module_name: nir_bytes}"""
nir_data = {}
for info in zf.infolist():
if not info.filename.startswith(NBPFFormatter.NIR_DIR):
continue
if info.filename.endswith("/"):
continue
module_name = info.filename[len(NBPFFormatter.NIR_DIR):]
enc_info = json.loads(zf.read(info.filename).decode("utf-8"))
mod_data = self.crypto.inner_decrypt(enc_info, key2)
nir_data[module_name] = mod_data
return nir_data
# ── 内层验签 ──
def _verify_module_signatures(self, nir_data: dict, meta_inf: dict, hmac_key: bytes):
"""内层 HMAC 模块签名验证"""
module_sigs = meta_inf.get("module_signatures", {})
if not module_sigs:
Log.warn("NBPF", "未找到模块签名,跳过内层验签")
return
for mod_name, mod_data in nir_data.items():
expected_sig = module_sigs.get(mod_name)
if expected_sig:
if not self.crypto.module_verify(mod_data, expected_sig, hmac_key):
raise NBPFLoadError(f"模块 '{mod_name}' HMAC 签名验证失败")
# ── 反序列化并执行 ──
def _deserialize_and_exec(
self,
nir_data: dict[str, bytes],
plugin_name: str,
) -> tuple[Any, types.ModuleType]:
"""反序列化 NIR 并执行,返回 (instance, module)"""
# 构建安全的全局命名空间
safe_globals = self._build_safe_globals(plugin_name)
# 按依赖顺序执行模块
main_module = None
for mod_name in sorted(nir_data.keys()):
nir_bytes = nir_data[mod_name]
code = self.compiler.deserialize_nir(nir_bytes)
# 创建模块
module_name = f"nbpf.{plugin_name}.{mod_name}"
if mod_name == NBPFFormatter.ENTRY_POINT:
module_name = f"nbpf.{plugin_name}"
module = types.ModuleType(module_name)
module.__package__ = f"nbpf.{plugin_name}"
module.__path__ = []
module.__file__ = f"<nbpf:{plugin_name}/{mod_name}>"
sys.modules[module_name] = module
# 执行 code object
exec(code, module.__dict__)
if mod_name == NBPFFormatter.ENTRY_POINT:
main_module = module
# 调用 New() 创建实例
if main_module is None:
raise NBPFLoadError(f"缺少入口模块 '{NBPFFormatter.ENTRY_POINT}'")
if not hasattr(main_module, "New"):
raise NBPFLoadError("插件缺少 New() 函数")
try:
instance = main_module.New()
except Exception as e:
raise NBPFLoadError(f"创建插件实例失败: {e}") from e
return instance, main_module
def _build_safe_globals(self, plugin_name: str) -> dict:
"""构建安全的全局命名空间"""
safe_builtins = {
'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,
'ValueError': ValueError, 'TypeError': TypeError,
'KeyError': KeyError, 'IndexError': IndexError,
'Exception': Exception, 'BaseException': BaseException,
}
return {
'__builtins__': safe_builtins,
'__name__': f'nbpf.{plugin_name}',
}

186
oss/core/repl/main.py Normal file
View File

@@ -0,0 +1,186 @@
"""REPL 交互终端 - 基于 Python cmd 模块"""
import cmd
import shlex
import sys
import readline
import os
from pathlib import Path
HISTORY_FILE = str(Path.home() / ".nebula_repl_history")
class NebulaShell(cmd.Cmd):
"""NebulaShell REPL 交互终端"""
def __init__(self, plugin_mgr):
super().__init__()
self.plugin_mgr = plugin_mgr
self.prompt = "\033[1;36mNebula>\033[0m " # 青色提示符
self.intro = (
"\033[1;33mNebulaShell Core v2.0.0\033[0m\n"
"输入 \033[1;32mhelp\033[0m 查看命令列表 | 输入 \033[1;31mexit\033[0m 退出"
)
# 加载历史记录
self._load_history()
def _load_history(self):
"""加载命令历史记录"""
try:
readline.read_history_file(HISTORY_FILE)
except (FileNotFoundError, OSError):
pass
readline.set_history_length(500)
def _save_history(self):
"""保存命令历史记录"""
try:
readline.write_history_file(HISTORY_FILE)
except OSError:
pass
def _get_plugins(self):
"""获取所有已加载的插件列表"""
if not self.plugin_mgr:
return []
return list(self.plugin_mgr.plugins.keys())
def _get_injected_functions(self):
"""获取所有 PL 注入的功能"""
if not self.plugin_mgr:
return {}
return self.plugin_mgr.pl_injector.get_registry_info()
# ── 命令plugins ──
def do_plugins(self, arg):
"""列出所有已加载的插件"""
plugins = self._get_plugins()
if not plugins:
print("\033[1;33m没有已加载的插件\033[0m")
return
print(f"\033[1;36m已加载插件 ({len(plugins)}):\033[0m")
for name in plugins:
info = self.plugin_mgr.get_info(name)
if info:
status = ""
if self.plugin_mgr.fallback_manager.is_degraded(name):
status = " \033[1;31m[降级]\033[0m"
print(f" \033[1;32m{name}\033[0m v{info.version} - {info.description}{status}")
else:
print(f" \033[1;32m{name}\033[0m")
# ── 命令pl ──
def do_pl(self, arg):
"""列出所有 PL 注入的功能"""
registry = self._get_injected_functions()
if not registry:
print("\033[1;33m没有 PL 注入功能\033[0m")
return
print(f"\033[1;36mPL 注入功能 ({len(registry)}):\033[0m")
for name, info in registry.items():
descs = [d for d in info["descriptions"] if d]
desc_str = f" - {descs[0]}" if descs else ""
print(f" \033[1;32m{name}\033[0m (来自 {', '.join(info['plugins'])}){desc_str}")
# ── 命令call ──
def do_call(self, arg):
"""调用 PL 注入功能: call <function_name> [args...]"""
if not arg:
print("\033[1;33m用法: call <function_name> [args...]\033[0m")
return
parts = shlex.split(arg)
name = parts[0]
args = parts[1:]
funcs = self.plugin_mgr.pl_injector.get_injected_functions(name)
if not funcs:
print(f"\033[1;31m未找到功能: {name}\033[0m")
return
for func in funcs:
try:
result = func(*args)
if result is not None:
print(result)
except Exception as e:
print(f"\033[1;31m执行失败: {e}\033[0m")
# ── 命令status ──
def do_status(self, arg):
"""显示 Core 状态"""
if not self.plugin_mgr:
print("\033[1;31mCore 未就绪\033[0m")
return
status = self.plugin_mgr.get_status()
print(f"\033[1;36mCore 状态:\033[0m")
print(f" 插件总数: {status['plugins']['total']}")
if status['plugins']['degraded']:
print(f" 降级插件: \033[1;31m{', '.join(status['plugins']['degraded'])}\033[0m")
print(f" HTTP 服务: {'\033[1;32m运行中\033[0m' if status['http_server'] else '\033[1;31m未启动\033[0m'}")
print(f" 防篡改监控: {'\033[1;32m运行中\033[0m' if status['tamper_monitor'] else '\033[1;31m未启动\033[0m'}")
print(f" 审计日志: {status['audit_logs']}")
print(f" 篡改告警: {status['tamper_alerts']}")
print(f" 数据目录: {status['data_store']}")
# ── 命令audit ──
def do_audit(self, arg):
"""查看审计日志: audit [plugin_name]"""
if not self.plugin_mgr:
return
logs = self.plugin_mgr.get_audit_logs(plugin_name=arg if arg else None, limit=20)
if not logs:
print("\033[1;33m无审计日志\033[0m")
return
print(f"\033[1;36m审计日志 ({len(logs)} 条):\033[0m")
for log in reversed(logs):
t = log["time"]
print(f" [{log['plugin']}] {log['action']} - {log['detail']}")
# ── 命令alerts ──
def do_alerts(self, arg):
"""查看防篡改告警"""
if not self.plugin_mgr:
return
alerts = self.plugin_mgr.get_tamper_alerts()
if not alerts:
print("\033[1;32m无防篡改告警\033[0m")
return
print(f"\033[1;31m防篡改告警 ({len(alerts)}):\033[0m")
for alert in alerts:
print(f" [{alert['plugin']}] {alert['message']}")
# ── 命令recover ──
def do_recover(self, arg):
"""恢复降级插件: recover <plugin_name>"""
if not arg:
print("\033[1;33m用法: recover <plugin_name>\033[0m")
return
if self.plugin_mgr.recover_plugin(arg):
print(f"\033[1;32m插件 '{arg}' 已恢复\033[0m")
else:
print(f"\033[1;31m插件 '{arg}' 恢复失败(可能未处于降级状态)\033[0m")
# ── 命令exit ──
def do_exit(self, arg):
"""退出 REPL"""
self._save_history()
print("\033[1;33m再见!\033[0m")
return True
def do_EOF(self, arg):
"""Ctrl+D 退出"""
return self.do_exit(arg)
def default(self, line):
"""未知命令"""
print(f"\033[1;31m未知命令: {line}\033[0m 输入 \033[1;32mhelp\033[0m 查看命令列表")
def emptyline(self):
"""空行不重复执行上一条命令"""
pass

View File

@@ -41,22 +41,6 @@ class Log:
def ok(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
class Logger:
"""日志记录器(兼容旧接口)"""
def info(self, msg: str, **kwargs):
tag = kwargs.get("tag", "INFO")
Log.info(tag, msg)
def warn(self, msg: str, **kwargs):
tag = kwargs.get("tag", "WARN")
Log.warn(tag, msg)
def error(self, msg: str, **kwargs):
tag = kwargs.get("tag", "ERROR")
Log.error(tag, msg)
def debug(self, msg: str, **kwargs):
tag = kwargs.get("tag", "DEBUG")
Log.tip(tag, msg)
@classmethod
def debug(cls, tag: str, msg: str):
cls.tip(tag, msg)

View File

@@ -1,22 +0,0 @@
"""插件基础类"""
from abc import ABC, abstractmethod
from typing import Any, Optional
class Plugin(ABC):
"""插件基类"""
@abstractmethod
def init(self, deps: Optional[dict] = None):
"""初始化插件"""
pass
@abstractmethod
def start(self):
"""启动插件"""
pass
@abstractmethod
def stop(self):
"""停止插件"""
pass

View File

@@ -1,3 +1,7 @@
import ast
# 启发式能力扫描:通过 AST 分析插件源码,基于命名约定和导入推断插件提供的能力
# 这是一种轻量级的静态分析,不执行任何代码,仅用于快速发现插件可能提供的能力
def scan_capabilities(plugin_dir):
capabilities: set[str] = set()
main_file = plugin_dir / "main.py"

View File

@@ -1,7 +1,7 @@
"""插件管理器 - 只加载 plugin-loader其他所有插件由 plugin-loader 插件自行管理"""
"""插件管理器 - 直接使用框架层的 Core Engine"""
from typing import Any, Optional
from oss.plugin.loader import PluginLoader
from oss.core.engine import PluginManager as CorePluginManager
# 深度隐藏的成就系统导入
try:
@@ -14,31 +14,19 @@ except ImportError:
class PluginManager:
"""极简插件管理器
遵循「最小化核心框架」设计哲学:
- 核心框架只负责加载 plugin-loader 插件
- 所有其他插件HTTP、WebSocket、Dashboard 等)都由 plugin-loader 插件扫描和加载
- store/NebulaShell/ 是唯一的插件来源
直接使用框架层的 CorePluginManager原 Core 插件功能)
- 不再通过插件加载器加载 Core
- 所有核心功能直接集成在 oss.core.engine 中
"""
def __init__(self):
self.loader = PluginLoader()
self.plugin_loader: Optional[Any] = None
self.core = CorePluginManager()
def load(self):
"""加载 plugin-loader 核心插件
"""加载所有插件(由 CorePluginManager 管理)"""
self.core.load_all()
plugin-loader 插件会负责:
1. 扫描 store/NebulaShell/ 目录
2. 加载所有启用的插件
3. 处理依赖关系
4. 执行 PL 注入机制
"""
# 只加载 plugin-loader其他所有插件都由它来管理
pl_info = self.loader.load_core_plugin("plugin-loader")
if pl_info:
self.plugin_loader = pl_info["instance"]
# 隐藏成就:深海潜水员 - 当加载插件管理器时解锁
# 隐藏成就:深海潜水员
if _ACHIEVEMENTS_ENABLED:
try:
validator = get_validator()
@@ -47,15 +35,17 @@ class PluginManager:
pass
def start(self):
"""启动 plugin-loader,它会初始化并启动所有其他插件"""
"""启动 Core,它会初始化并启动所有其他插件"""
import time
start_time = time.time()
if self.plugin_loader:
# plugin-loader.init() 会扫描并加载 store/ 中的所有插件
self.plugin_loader.init()
# plugin-loader.start() 会按依赖顺序启动所有插件
self.plugin_loader.start()
self.core.init_and_start_all()
# 启动 HTTP 服务
self.core.start_http_server()
# 启动防篡改监控
self.core.start_tamper_monitor()
# 计算启动时间并检查速度成就
elapsed_ms = (time.time() - start_time) * 1000
@@ -65,30 +55,28 @@ class PluginManager:
validator.check_startup_speed(elapsed_ms)
# 检查插件数量成就
if hasattr(self.plugin_loader, 'manager') and hasattr(self.plugin_loader.manager, 'plugins'):
plugin_count = len(self.plugin_loader.manager.plugins)
validator.check_plugin_count(plugin_count)
plugin_count = len(self.core.plugins)
validator.check_plugin_count(plugin_count)
except Exception:
pass
def stop(self):
"""停止所有插件(由 plugin-loader 统一管理)"""
if self.plugin_loader:
try:
self.plugin_loader.stop()
except KeyboardInterrupt:
print("[PluginManager] 用户中断停止过程")
except Exception as e:
import traceback
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
traceback.print_exc()
"""停止所有插件"""
try:
self.core.stop_tamper_monitor()
self.core.stop_http_server()
self.core.stop_all()
except KeyboardInterrupt:
print("[PluginManager] 用户中断停止过程")
except Exception as e:
import traceback
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
traceback.print_exc()
# 隐藏成就:崩溃幸存者 - 如果正常停止则不解锁,只有异常停止才可能解锁
# 这里我们记录停止事件,用于将来可能的连续运行成就
if _ACHIEVEMENTS_ENABLED:
try:
validator = get_validator()
# 记录会话结束
validator.track_progress("session_end")
except Exception:
pass
# 隐藏成就:崩溃幸存者
if _ACHIEVEMENTS_ENABLED:
try:
validator = get_validator()
validator.track_progress("session_end")
except Exception:
pass

View File

@@ -1,3 +1,7 @@
from typing import Callable
from functools import lru_cache
class BaseRoute:
__slots__ = ('method', 'path', 'handler', '_pattern_parts')
@@ -9,6 +13,16 @@ class BaseRoute:
self._pattern_parts = path.strip("/").split("/") if ":" in path else None
def _get_pattern_parts(pattern: str):
if ":" not in pattern:
return None
return pattern.strip("/").split("/")
def _is_wildcard_param(param: str) -> bool:
return param.startswith(":") and param.endswith("*")
@lru_cache(maxsize=1024)
def match_path(pattern: str, path: str) -> bool:
if pattern == path:
@@ -41,12 +55,6 @@ def match_path(pattern: str, path: str) -> bool:
return True
def _is_wildcard_param(param: str) -> bool:
if ":" not in pattern:
return None
return pattern.strip("/").split("/")
@lru_cache(maxsize=1024)
def extract_path_params(pattern: str, path: str) -> dict[str, str]:
params = {}
@@ -85,9 +93,15 @@ class BaseRouter:
self.routes: list[BaseRoute] = []
def add(self, method: str, path: str, handler: Callable):
self.routes.append(BaseRoute(method, path, handler))
def get(self, path: str, handler: Callable):
self.add("GET", path, handler)
def post(self, path: str, handler: Callable):
self.add("POST", path, handler)
def put(self, path: str, handler: Callable):
self.add("PUT", path, handler)
def delete(self, path: str, handler: Callable):

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

Some files were not shown because too many files have changed in this diff Show More