From 3a096f59a930cb290aada8c190972c60857b11bc Mon Sep 17 00:00:00 2001 From: Falck Date: Tue, 5 May 2026 07:29:43 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=87=B3=20oss/core=20+=20NBPF=20=E5=A4=9A?= =?UTF-8?q?=E9=87=8D=E7=AD=BE=E5=90=8D=E5=8A=A0=E5=AF=86=20+=20NIR=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8=20+=20README=20=E5=85=A8=E9=9D=A2?= =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 核心功能从 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) --- LICENSE | 2 +- README.md | 427 ++++- ai.md | 98 +- docs/architecture.svg | 191 ++ docs/dataflow.svg | 92 + docs/key-structure.svg | 23 + docs/layers.svg | 120 ++ docs/nir-flow.svg | 52 + docs/package-structure.svg | 27 + docs/philosophy.svg | 34 + docs/security-chain.svg | 77 + docs/security-flow.svg | 83 + oss/cli.py | 348 +++- oss/config/config.py | 9 +- oss/core/engine.py | 1687 +++++++++++++++++ .../checks => oss/core/http_api}/__init__.py | 0 oss/core/http_api/middleware.py | 113 ++ oss/core/http_api/rate_limiter.py | 138 ++ .../http-api => oss/core/http_api}/router.py | 0 .../http-api => oss/core/http_api}/server.py | 7 +- oss/core/nbpf/__init__.py | 18 + oss/core/nbpf/compiler.py | 271 +++ oss/core/nbpf/crypto.py | 591 ++++++ oss/core/nbpf/format.py | 349 ++++ oss/core/nbpf/loader.py | 360 ++++ .../core => oss/core/repl}/__init__.py | 0 oss/core/repl/main.py | 186 ++ oss/logger/logger.py | 22 +- oss/plugin/base.py | 22 - oss/plugin/capabilities.py | 4 + oss/plugin/manager.py | 96 +- oss/shared/router.py | 26 +- store/NebulaShell/auto-dependency/PL/main.py | 57 - store/NebulaShell/auto-dependency/README.md | 117 -- store/NebulaShell/auto-dependency/main.py | 269 --- .../NebulaShell/auto-dependency/manifest.json | 20 - store/NebulaShell/code-reviewer/SIGNATURE | 8 - .../code-reviewer/checks/quality.py | 56 - .../code-reviewer/checks/references.py | 200 -- .../code-reviewer/checks/security.py | 35 - .../NebulaShell/code-reviewer/checks/style.py | 27 - .../code-reviewer/core/reviewer.py | 78 - store/NebulaShell/code-reviewer/main.py | 55 - store/NebulaShell/code-reviewer/manifest.json | 20 - .../code-reviewer/report/__init__.py | 0 .../code-reviewer/report/formatter.py | 43 - .../code-reviewer/utils/__init__.py | 0 store/NebulaShell/dashboard/SIGNATURE | 8 - .../dashboard/assets/css/dashboard.css | 91 - store/NebulaShell/dashboard/config.json | 28 - store/NebulaShell/dashboard/main.py | 214 --- store/NebulaShell/dashboard/manifest.json | 21 - store/NebulaShell/dependency/README.md | 39 - store/NebulaShell/dependency/SIGNATURE | 8 - store/NebulaShell/dependency/main.py | 63 - store/NebulaShell/dependency/manifest.json | 15 - .../example-with-deps/manifest.json | 16 - store/NebulaShell/firewall/manifest.json | 27 - store/NebulaShell/frp-proxy/manifest.json | 26 - store/NebulaShell/ftp-server/manifest.json | 27 - store/NebulaShell/hot-reload/README.md | 32 - store/NebulaShell/hot-reload/SIGNATURE | 8 - store/NebulaShell/hot-reload/main.py | 105 - store/NebulaShell/hot-reload/manifest.json | 18 - store/NebulaShell/http-api/README.md | 53 - store/NebulaShell/http-api/SIGNATURE | 8 - store/NebulaShell/http-api/csrf_middleware.py | 187 -- store/NebulaShell/http-api/events.py | 6 - .../NebulaShell/http-api/input_validation.py | 209 -- store/NebulaShell/http-api/main.py | 43 - store/NebulaShell/http-api/manifest.json | 25 - store/NebulaShell/http-api/middleware.py | 230 --- store/NebulaShell/http-api/rate_limiter.py | 35 - store/NebulaShell/http-tcp/README.md | 51 - store/NebulaShell/http-tcp/SIGNATURE | 8 - store/NebulaShell/http-tcp/events.py | 13 - store/NebulaShell/http-tcp/main.py | 12 - store/NebulaShell/http-tcp/manifest.json | 21 - store/NebulaShell/http-tcp/middleware.py | 24 - store/NebulaShell/http-tcp/router.py | 4 - store/NebulaShell/http-tcp/server.py | 116 -- store/NebulaShell/i18n/SIGNATURE | 8 - store/NebulaShell/i18n/__init__.py | 0 store/NebulaShell/i18n/i18n.py | 101 - store/NebulaShell/i18n/locales/en-US.json | 51 - store/NebulaShell/i18n/locales/zh-CN.json | 51 - store/NebulaShell/i18n/locales/zh-TW.json | 51 - store/NebulaShell/i18n/main.py | 132 -- store/NebulaShell/i18n/manifest.json | 24 - store/NebulaShell/i18n/middleware.py | 52 - store/NebulaShell/json-codec/README.md | 83 - store/NebulaShell/json-codec/SIGNATURE | 8 - store/NebulaShell/json-codec/main.py | 75 - store/NebulaShell/json-codec/manifest.json | 15 - store/NebulaShell/lifecycle/README.md | 30 - store/NebulaShell/lifecycle/SIGNATURE | 8 - store/NebulaShell/lifecycle/main.py | 93 - store/NebulaShell/lifecycle/manifest.json | 15 - store/NebulaShell/log-terminal/SIGNATURE | 8 - store/NebulaShell/log-terminal/config.json | 36 - store/NebulaShell/log-terminal/main.py | 459 ----- store/NebulaShell/log-terminal/manifest.json | 15 - store/NebulaShell/nodejs-adapter/README.md | 281 --- store/NebulaShell/nodejs-adapter/main.py | 402 ---- .../NebulaShell/nodejs-adapter/manifest.json | 30 - .../performance-optimizer/README.md | 155 -- .../NebulaShell/performance-optimizer/main.py | 318 ---- .../performance-optimizer/manifest.json | 18 - store/NebulaShell/pkg-manager/SIGNATURE | 8 - store/NebulaShell/pkg-manager/main.py | 276 --- store/NebulaShell/pkg-manager/manifest.json | 21 - store/NebulaShell/plugin-bridge/README.md | 77 - store/NebulaShell/plugin-bridge/SIGNATURE | 8 - store/NebulaShell/plugin-bridge/main.py | 217 --- store/NebulaShell/plugin-bridge/manifest.json | 21 - store/NebulaShell/plugin-loader-pro/SIGNATURE | 8 - .../plugin-loader-pro/circuit/__init__.py | 0 .../plugin-loader-pro/circuit/breaker.py | 28 - .../plugin-loader-pro/circuit/state.py | 4 - .../plugin-loader-pro/core/__init__.py | 0 .../plugin-loader-pro/core/config.py | 23 - .../plugin-loader-pro/core/enhancer.py | 102 - .../plugin-loader-pro/core/manager.py | 106 -- .../plugin-loader-pro/core/proxy.py | 21 - .../plugin-loader-pro/core/registry.py | 16 - .../plugin-loader-pro/fallback/__init__.py | 0 .../plugin-loader-pro/fallback/handler.py | 20 - .../plugin-loader-pro/isolation/__init__.py | 0 .../plugin-loader-pro/isolation/timeout.py | 21 - store/NebulaShell/plugin-loader-pro/main.py | 73 - .../plugin-loader-pro/manifest.json | 40 - .../plugin-loader-pro/models/__init__.py | 0 .../plugin-loader-pro/models/plugin_info.py | 25 - .../plugin-loader-pro/recovery/__init__.py | 0 .../plugin-loader-pro/recovery/auto_fix.py | 13 - .../plugin-loader-pro/recovery/health.py | 36 - .../plugin-loader-pro/retry/__init__.py | 0 .../plugin-loader-pro/retry/handler.py | 12 - .../plugin-loader-pro/utils/__init__.py | 0 .../plugin-loader-pro/utils/logger.py | 29 - store/NebulaShell/plugin-loader/PL_EXAMPLE.md | 172 -- store/NebulaShell/plugin-loader/README.md | 16 - store/NebulaShell/plugin-loader/SIGNATURE | 8 - store/NebulaShell/plugin-loader/main.py | 758 -------- store/NebulaShell/plugin-loader/manifest.json | 19 - store/NebulaShell/plugin-storage/README.md | 72 - store/NebulaShell/plugin-storage/SIGNATURE | 8 - store/NebulaShell/plugin-storage/main.py | 262 --- .../NebulaShell/plugin-storage/manifest.json | 22 - store/NebulaShell/plugin_bridge | 1 - .../NebulaShell/polyglot-deploy/manifest.json | 26 - .../NebulaShell/signature-verifier/SIGNATURE | 8 - store/NebulaShell/signature-verifier/main.py | 201 -- .../signature-verifier/manifest.json | 23 - store/NebulaShell/webui/SIGNATURE | 8 - store/NebulaShell/webui/config.json | 29 - store/NebulaShell/webui/config/database.sql | 49 - store/NebulaShell/webui/core/__init__.py | 0 store/NebulaShell/webui/core/server.py | 66 - .../webui/frontend/assets/css/main.css | 145 -- .../webui/frontend/assets/js/main.js | 36 - .../webui/frontend/views/index.html | 110 -- .../webui/frontend/views/layout.html | 33 - store/NebulaShell/webui/main.py | 67 - store/NebulaShell/webui/manifest.json | 33 - store/NebulaShell/webui/static/__init__.py | 0 store/NebulaShell/webui/static/assets.py | 8 - store/NebulaShell/webui/templates/__init__.py | 0 store/NebulaShell/webui/templates/layout.py | 42 - store/NebulaShell/webui/tui/README.md | 187 -- store/NebulaShell/webui/tui/__init__.py | 0 store/NebulaShell/webui/tui/converter.py | 481 ----- store/NebulaShell/webui/tui/index.html | 113 -- store/NebulaShell/webui/tui/main.py | 95 - store/NebulaShell/webui/tui/manifest.json | 28 - store/NebulaShell/ws-api/README.md | 50 - store/NebulaShell/ws-api/SIGNATURE | 8 - store/NebulaShell/ws-api/events.py | 15 - store/NebulaShell/ws-api/main.py | 10 - store/NebulaShell/ws-api/manifest.json | 22 - store/NebulaShell/ws-api/middleware.py | 14 - store/NebulaShell/ws-api/router.py | 15 - store/NebulaShell/ws-api/server.py | 67 - tests/test_nbpf.py | 481 +++++ 184 files changed, 5715 insertions(+), 10066 deletions(-) create mode 100644 docs/architecture.svg create mode 100644 docs/dataflow.svg create mode 100644 docs/key-structure.svg create mode 100644 docs/layers.svg create mode 100644 docs/nir-flow.svg create mode 100644 docs/package-structure.svg create mode 100644 docs/philosophy.svg create mode 100644 docs/security-chain.svg create mode 100644 docs/security-flow.svg create mode 100644 oss/core/engine.py rename {store/NebulaShell/code-reviewer/checks => oss/core/http_api}/__init__.py (100%) create mode 100644 oss/core/http_api/middleware.py create mode 100644 oss/core/http_api/rate_limiter.py rename {store/NebulaShell/http-api => oss/core/http_api}/router.py (100%) rename {store/NebulaShell/http-api => oss/core/http_api}/server.py (95%) create mode 100644 oss/core/nbpf/__init__.py create mode 100644 oss/core/nbpf/compiler.py create mode 100644 oss/core/nbpf/crypto.py create mode 100644 oss/core/nbpf/format.py create mode 100644 oss/core/nbpf/loader.py rename {store/NebulaShell/code-reviewer/core => oss/core/repl}/__init__.py (100%) create mode 100644 oss/core/repl/main.py delete mode 100644 oss/plugin/base.py delete mode 100644 store/NebulaShell/auto-dependency/PL/main.py delete mode 100644 store/NebulaShell/auto-dependency/README.md delete mode 100644 store/NebulaShell/auto-dependency/main.py delete mode 100644 store/NebulaShell/auto-dependency/manifest.json delete mode 100644 store/NebulaShell/code-reviewer/SIGNATURE delete mode 100644 store/NebulaShell/code-reviewer/checks/quality.py delete mode 100644 store/NebulaShell/code-reviewer/checks/references.py delete mode 100644 store/NebulaShell/code-reviewer/checks/security.py delete mode 100644 store/NebulaShell/code-reviewer/checks/style.py delete mode 100644 store/NebulaShell/code-reviewer/core/reviewer.py delete mode 100644 store/NebulaShell/code-reviewer/main.py delete mode 100644 store/NebulaShell/code-reviewer/manifest.json delete mode 100644 store/NebulaShell/code-reviewer/report/__init__.py delete mode 100644 store/NebulaShell/code-reviewer/report/formatter.py delete mode 100644 store/NebulaShell/code-reviewer/utils/__init__.py delete mode 100644 store/NebulaShell/dashboard/SIGNATURE delete mode 100644 store/NebulaShell/dashboard/assets/css/dashboard.css delete mode 100644 store/NebulaShell/dashboard/config.json delete mode 100644 store/NebulaShell/dashboard/main.py delete mode 100644 store/NebulaShell/dashboard/manifest.json delete mode 100644 store/NebulaShell/dependency/README.md delete mode 100644 store/NebulaShell/dependency/SIGNATURE delete mode 100644 store/NebulaShell/dependency/main.py delete mode 100644 store/NebulaShell/dependency/manifest.json delete mode 100644 store/NebulaShell/example-with-deps/manifest.json delete mode 100644 store/NebulaShell/firewall/manifest.json delete mode 100644 store/NebulaShell/frp-proxy/manifest.json delete mode 100644 store/NebulaShell/ftp-server/manifest.json delete mode 100644 store/NebulaShell/hot-reload/README.md delete mode 100644 store/NebulaShell/hot-reload/SIGNATURE delete mode 100644 store/NebulaShell/hot-reload/main.py delete mode 100644 store/NebulaShell/hot-reload/manifest.json delete mode 100644 store/NebulaShell/http-api/README.md delete mode 100644 store/NebulaShell/http-api/SIGNATURE delete mode 100644 store/NebulaShell/http-api/csrf_middleware.py delete mode 100644 store/NebulaShell/http-api/events.py delete mode 100644 store/NebulaShell/http-api/input_validation.py delete mode 100644 store/NebulaShell/http-api/main.py delete mode 100644 store/NebulaShell/http-api/manifest.json delete mode 100644 store/NebulaShell/http-api/middleware.py delete mode 100644 store/NebulaShell/http-api/rate_limiter.py delete mode 100644 store/NebulaShell/http-tcp/README.md delete mode 100644 store/NebulaShell/http-tcp/SIGNATURE delete mode 100644 store/NebulaShell/http-tcp/events.py delete mode 100644 store/NebulaShell/http-tcp/main.py delete mode 100644 store/NebulaShell/http-tcp/manifest.json delete mode 100644 store/NebulaShell/http-tcp/middleware.py delete mode 100644 store/NebulaShell/http-tcp/router.py delete mode 100644 store/NebulaShell/http-tcp/server.py delete mode 100644 store/NebulaShell/i18n/SIGNATURE delete mode 100644 store/NebulaShell/i18n/__init__.py delete mode 100644 store/NebulaShell/i18n/i18n.py delete mode 100644 store/NebulaShell/i18n/locales/en-US.json delete mode 100644 store/NebulaShell/i18n/locales/zh-CN.json delete mode 100644 store/NebulaShell/i18n/locales/zh-TW.json delete mode 100644 store/NebulaShell/i18n/main.py delete mode 100644 store/NebulaShell/i18n/manifest.json delete mode 100644 store/NebulaShell/i18n/middleware.py delete mode 100644 store/NebulaShell/json-codec/README.md delete mode 100644 store/NebulaShell/json-codec/SIGNATURE delete mode 100644 store/NebulaShell/json-codec/main.py delete mode 100644 store/NebulaShell/json-codec/manifest.json delete mode 100644 store/NebulaShell/lifecycle/README.md delete mode 100644 store/NebulaShell/lifecycle/SIGNATURE delete mode 100644 store/NebulaShell/lifecycle/main.py delete mode 100644 store/NebulaShell/lifecycle/manifest.json delete mode 100644 store/NebulaShell/log-terminal/SIGNATURE delete mode 100644 store/NebulaShell/log-terminal/config.json delete mode 100644 store/NebulaShell/log-terminal/main.py delete mode 100644 store/NebulaShell/log-terminal/manifest.json delete mode 100644 store/NebulaShell/nodejs-adapter/README.md delete mode 100644 store/NebulaShell/nodejs-adapter/main.py delete mode 100644 store/NebulaShell/nodejs-adapter/manifest.json delete mode 100644 store/NebulaShell/performance-optimizer/README.md delete mode 100644 store/NebulaShell/performance-optimizer/main.py delete mode 100644 store/NebulaShell/performance-optimizer/manifest.json delete mode 100644 store/NebulaShell/pkg-manager/SIGNATURE delete mode 100644 store/NebulaShell/pkg-manager/main.py delete mode 100644 store/NebulaShell/pkg-manager/manifest.json delete mode 100644 store/NebulaShell/plugin-bridge/README.md delete mode 100644 store/NebulaShell/plugin-bridge/SIGNATURE delete mode 100644 store/NebulaShell/plugin-bridge/main.py delete mode 100644 store/NebulaShell/plugin-bridge/manifest.json delete mode 100644 store/NebulaShell/plugin-loader-pro/SIGNATURE delete mode 100644 store/NebulaShell/plugin-loader-pro/circuit/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/circuit/breaker.py delete mode 100644 store/NebulaShell/plugin-loader-pro/circuit/state.py delete mode 100644 store/NebulaShell/plugin-loader-pro/core/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/core/config.py delete mode 100644 store/NebulaShell/plugin-loader-pro/core/enhancer.py delete mode 100644 store/NebulaShell/plugin-loader-pro/core/manager.py delete mode 100644 store/NebulaShell/plugin-loader-pro/core/proxy.py delete mode 100644 store/NebulaShell/plugin-loader-pro/core/registry.py delete mode 100644 store/NebulaShell/plugin-loader-pro/fallback/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/fallback/handler.py delete mode 100644 store/NebulaShell/plugin-loader-pro/isolation/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/isolation/timeout.py delete mode 100644 store/NebulaShell/plugin-loader-pro/main.py delete mode 100644 store/NebulaShell/plugin-loader-pro/manifest.json delete mode 100644 store/NebulaShell/plugin-loader-pro/models/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/models/plugin_info.py delete mode 100644 store/NebulaShell/plugin-loader-pro/recovery/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py delete mode 100644 store/NebulaShell/plugin-loader-pro/recovery/health.py delete mode 100644 store/NebulaShell/plugin-loader-pro/retry/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/retry/handler.py delete mode 100644 store/NebulaShell/plugin-loader-pro/utils/__init__.py delete mode 100644 store/NebulaShell/plugin-loader-pro/utils/logger.py delete mode 100644 store/NebulaShell/plugin-loader/PL_EXAMPLE.md delete mode 100644 store/NebulaShell/plugin-loader/README.md delete mode 100644 store/NebulaShell/plugin-loader/SIGNATURE delete mode 100644 store/NebulaShell/plugin-loader/main.py delete mode 100644 store/NebulaShell/plugin-loader/manifest.json delete mode 100644 store/NebulaShell/plugin-storage/README.md delete mode 100644 store/NebulaShell/plugin-storage/SIGNATURE delete mode 100644 store/NebulaShell/plugin-storage/main.py delete mode 100644 store/NebulaShell/plugin-storage/manifest.json delete mode 120000 store/NebulaShell/plugin_bridge delete mode 100644 store/NebulaShell/polyglot-deploy/manifest.json delete mode 100644 store/NebulaShell/signature-verifier/SIGNATURE delete mode 100644 store/NebulaShell/signature-verifier/main.py delete mode 100644 store/NebulaShell/signature-verifier/manifest.json delete mode 100644 store/NebulaShell/webui/SIGNATURE delete mode 100644 store/NebulaShell/webui/config.json delete mode 100644 store/NebulaShell/webui/config/database.sql delete mode 100644 store/NebulaShell/webui/core/__init__.py delete mode 100644 store/NebulaShell/webui/core/server.py delete mode 100644 store/NebulaShell/webui/frontend/assets/css/main.css delete mode 100644 store/NebulaShell/webui/frontend/assets/js/main.js delete mode 100644 store/NebulaShell/webui/frontend/views/index.html delete mode 100644 store/NebulaShell/webui/frontend/views/layout.html delete mode 100644 store/NebulaShell/webui/main.py delete mode 100644 store/NebulaShell/webui/manifest.json delete mode 100644 store/NebulaShell/webui/static/__init__.py delete mode 100644 store/NebulaShell/webui/static/assets.py delete mode 100644 store/NebulaShell/webui/templates/__init__.py delete mode 100644 store/NebulaShell/webui/templates/layout.py delete mode 100644 store/NebulaShell/webui/tui/README.md delete mode 100644 store/NebulaShell/webui/tui/__init__.py delete mode 100644 store/NebulaShell/webui/tui/converter.py delete mode 100644 store/NebulaShell/webui/tui/index.html delete mode 100644 store/NebulaShell/webui/tui/main.py delete mode 100644 store/NebulaShell/webui/tui/manifest.json delete mode 100644 store/NebulaShell/ws-api/README.md delete mode 100644 store/NebulaShell/ws-api/SIGNATURE delete mode 100644 store/NebulaShell/ws-api/events.py delete mode 100644 store/NebulaShell/ws-api/main.py delete mode 100644 store/NebulaShell/ws-api/manifest.json delete mode 100644 store/NebulaShell/ws-api/middleware.py delete mode 100644 store/NebulaShell/ws-api/router.py delete mode 100644 store/NebulaShell/ws-api/server.py create mode 100644 tests/test_nbpf.py diff --git a/LICENSE b/LICENSE index 1361477..ba89e79 100644 --- a/LICENSE +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 1f7e6b4..1210c41 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,421 @@ -# NebulaShell +

+ + + NebulaShell + +

-[![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)]() +

+ Python + License + Build + Coverage + Security +

-NebulaShell 是一个插件化运行时框架。一切功能皆由插件实现,核心仅保留插件加载与调度能力。 +

+ 插件化运行时框架 · 多重签名加密分发 · NIR 一次编译到处运行 · 企业级安全体系 +

+ +
+ +--- + +## 目录 + +- [项目定位](#项目定位) +- [架构总览](#架构总览) +- [核心能力](#核心能力) +- [快速开始](#快速开始) +- [NBPF 包格式](#nbpf-包格式) +- [NIR 中间表示](#nir-中间表示) +- [CLI 工具链](#cli-工具链) +- [插件开发](#插件开发) +- [内置插件](#内置插件) +- [安全体系](#安全体系) +- [性能指标](#性能指标) +- [贡献指南](#贡献指南) +- [许可证](#许可证) + +--- + +## 项目定位 + +NebulaShell 是一个**以安全为基石、以插件为灵魂**的运行时框架。核心只做两件事:**加载插件**与**调度插件**,其余一切功能均由插件生态提供。 + +

+ NebulaShell Philosophy +

+ +### 设计原则 + +| 原则 | 说明 | +|------|------| +| **最小核心** | 核心仅 1100+ 行,职责单一,可独立审计 | +| **插件即产品** | 所有业务功能以插件形式交付,核心不耦合任何业务 | +| **安全默认** | 插件分发强制签名加密,运行时隔离,防篡改防逆向 | +| **一次编译** | NIR 中间表示确保插件跨平台运行,无需为架构适配 | +| **零信任分发** | 每个包经过三层签名验证 + 两层加密解密才可加载 | + +--- + +## 架构总览 + +

+ NebulaShell Architecture +

+ +### 分层架构 + +

+ NebulaShell Layers +

+ +### 数据流 + +

+ NebulaShell Data Flow +

+ +--- + +## 核心能力 + +### 插件化架构 + +- **热插拔**:插件可在运行时动态加载/卸载,无需重启 +- **依赖注入**:通过 `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 包格式 + +### 包结构 + +

+ NBPF Package Structure +

+ +### 加密层级 + +| 层级 | 算法 | 密钥来源 | 保护范围 | +|------|------|----------|----------| +| 外层加密 | AES-256-GCM | key1(RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 | +| 中层加密 | AES-256-GCM | key2(RSA-OAEP 封装) | NIR 数据内容 | +| 外层签名 | Ed25519 | 开发者私钥 | 加密层完整性 | +| 中层签名 | RSA-4096-PSS | 作者私钥 | 模块内容完整性 | +| 内层签名 | HMAC-SHA256 | 派生密钥(key1+key2) | 单个模块完整性 | + +### 安全流程 + +

+ NBPF Security Flow +

+ +--- + +## NIR 中间表示 + +NIR(Nebula Intermediate Representation)是 NebulaShell 的跨平台编译方案。 + +### 技术原理 + +

+ NIR Compilation Flow +

+ +### 代码保护 + +| 技术 | 说明 | +|------|------| +| 混淆导入路径 | 动态 `__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` | 多语言部署支持 | + +--- + +## 安全体系 + +### 全链路安全 + +

+ Full Chain Security +

+ +### 加密标准 + +| 组件 | 标准 | 密钥长度 | +|------|------|----------| +| 对称加密 | AES-256-GCM | 256 位 | +| 非对称加密 | RSA-OAEP | 4096 位 | +| 外层签名 | Ed25519 | 256 位 | +| 中层签名 | RSA-PSS | 4096 位 | +| 内层签名 | HMAC-SHA256 | 256 位 | + +### 密钥管理 + +

+ Key Management Structure +

+ +--- + +## 性能指标 + +| 指标 | 数值 | +|------|------| +| 核心代码行数 | ~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). diff --git a/ai.md b/ai.md index 166908a..d3710b4 100644 --- a/ai.md +++ b/ai.md @@ -794,7 +794,91 @@ Phase 4 (长期) — K8s部署、ADR、类型检查、pre-commit、异步I/O --- -## 21. 变更记录 +## 21. NBPF 包格式系统 + +### 21.1 架构概览 + +NBPF(Nebula 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 密钥1(RSA-OAEP 加密) +│ └── ENC_KEY2.enc # AES 密钥2(RSA-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 | key1(RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 | +| 中层加密 | AES-256-GCM | key2(RSA-OAEP 封装) | NIR 数据内容 | +| 外层签名 | Ed25519 | 开发者私钥 | 加密层完整性 | +| 中层签名 | RSA-4096-PSS | 作者私钥 | 模块内容完整性 | +| 内层签名 | HMAC-SHA256 | 派生密钥(key1+key2) | 单个模块完整性 | + +### 21.3 NIR 编译器 + +NIR(Nebula 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) diff --git a/docs/architecture.svg b/docs/architecture.svg new file mode 100644 index 0000000..32d9a20 --- /dev/null +++ b/docs/architecture.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NEBULASHELL 分层架构 + + + + 应用层 · PLUGINS + + + + + WebUI + + + + HTTP API + + + + WebSocket + + + + Dashboard + + + + Log Terminal + + + + PKG Manager + + + + 26+ 插件 + + + 所有业务功能以插件形式提供,热插拔、隔离运行 + + + + + + + 通信层 · PLUGIN BRIDGE + + + + 事件总线 (Event Bus) + + + + RPC 通信 + + + + use() 依赖注入 + + + + 生命周期管理 + + + + + + + + 加载层 · PLUGIN MANAGER + + + + 源码加载器 + manifest 解析 · 依赖注入 + + + + NBPF 加载器 + 签名验证链 · 解密流水线 + + + + 热重载引擎 + 依赖解析器 · 熔断降级 + + + + + + + + 安全层 · NBPF CORE + + + + 加密引擎 + AES-256-GCM + 双层加密 + RSA-OAEP 密钥封装 + + + + 签名引擎 + Ed25519 外层签名 + RSA-4096-PSS 中层 + HMAC-SHA256 内层 + + + + NIR 编译器 + compile() → code object + marshal 序列化 + 代码混淆保护 + + + + 密钥管理 + 信任公钥白名单 + 私钥安全存储 + 密钥派生 + + + + + + + + 基础设施层 · OSS + + + + 插件类型系统 + + + + 配置管理 + + + + 日志系统 + + + + 错误处理 + + + + 工具库 + + + + NebulaShell Architecture · 核心 ~1,100 行 · 插件 26+ · 测试覆盖率 ~92% + diff --git a/docs/dataflow.svg b/docs/dataflow.svg new file mode 100644 index 0000000..325d694 --- /dev/null +++ b/docs/dataflow.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NBPF 数据流 + + + 开发者 + 分发 + 运行时 + + + + + + + 编写插件源码 + + + nebula nbpf pack + + + NIR 编译 + + + 外层加密 (key1) + + + 中层加密 (key2) + + + Ed25519 + RSA-PSS + HMAC 签名 + + + + + + + .nbpf 包分发 + + META-INF/ · NIR/ · 加密层 + 三层签名 · 双层加密 + + + + + + + nebula nbpf verify + + + Ed25519 验证 + + + RSA-PSS 验证 + + + HMAC 验证 + + + RSA-OAEP 解密密钥 + + + AES-GCM 解密 + + + NIR 反编译 → 加载运行 + diff --git a/docs/key-structure.svg b/docs/key-structure.svg new file mode 100644 index 0000000..ee3a008 --- /dev/null +++ b/docs/key-structure.svg @@ -0,0 +1,23 @@ + + + + + + + + + + 密钥管理目录 + + data/nbpf-keys/ + + trusted/ + 信任的 Ed25519 公钥(白名单) + + rsa/ + 信任的 RSA 公钥(白名单) + + private/ + ed25519.pem — Ed25519 私钥 + rsa.pem — RSA 私钥 + diff --git a/docs/layers.svg b/docs/layers.svg new file mode 100644 index 0000000..6a84aa8 --- /dev/null +++ b/docs/layers.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 应用层 · PLUGINS + + WebUI + + HTTP API + + WebSocket + + Dashboard + + Log Terminal + + PKG Manager + + 26+ 插件 + + + + + + + 通信层 · PLUGIN BRIDGE + + 事件总线 + + RPC 通信 + + use() 依赖注入 + + 生命周期管理 + + + + + + 加载层 · PLUGIN MANAGER + + 源码加载器 + manifest 解析 · 依赖注入 + + NBPF 加载器 + 签名验证链 · 解密流水线 + + 热重载引擎 + 依赖解析器 · 熔断降级 + + + + + + 安全层 · NBPF CORE + + 加密引擎 + AES-256-GCM 双层加密 + RSA-OAEP 密钥封装 + + 签名引擎 + Ed25519 · RSA-4096-PSS + HMAC-SHA256 + + NIR 编译器 + compile() → code object + marshal 序列化 · 混淆 + + 密钥管理 + 信任公钥白名单 + 私钥安全存储 + + + + + + 基础设施层 · OSS + + 插件类型系统 + + 配置管理 + + 日志系统 + + 错误处理 + + 工具库 + + 核心 ~1,100 行 · 插件 26+ · 测试覆盖率 ~92% + diff --git a/docs/nir-flow.svg b/docs/nir-flow.svg new file mode 100644 index 0000000..ca13a0d --- /dev/null +++ b/docs/nir-flow.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + NIR 编译流程 + + + + Python 源码 + + + + compile() + + + + code object (字节码) + + + + marshal.dumps() + + + + .nir 文件 + + + 跨平台 · Python 3.10+ · 代码混淆保护 + diff --git a/docs/package-structure.svg b/docs/package-structure.svg new file mode 100644 index 0000000..214edbf --- /dev/null +++ b/docs/package-structure.svg @@ -0,0 +1,27 @@ + + + + + + + + + + NBPF 包结构 + + my-plugin.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 密钥1(RSA-OAEP 加密) + ENC_KEY2.enc — AES 密钥2(RSA-OAEP 加密) + + NIR/ + module.nir — 编译后的 code object + diff --git a/docs/philosophy.svg b/docs/philosophy.svg new file mode 100644 index 0000000..1d4744f --- /dev/null +++ b/docs/philosophy.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + NEBULASHELL 哲学 + + + + 核心 + = 加载器 + 调度器 → 极简、稳定、可审计 + + + + 插件 + = 一切功能 → 热插拔、隔离、可分发 + + + + 安全 + = 默认内置 → 多重签名 + 多重加密 + NIR + + diff --git a/docs/security-chain.svg b/docs/security-chain.svg new file mode 100644 index 0000000..cdf5020 --- /dev/null +++ b/docs/security-chain.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 全链路安全架构 + + + + 开发阶段 + + 源码审计 + + 代码审查 + + + + + + + 分发阶段 + + NIR 编译 + 双重加密 + + 三层签名 · 防篡改分发 + + + + + + + 加载阶段 + + 签名验证 + 密钥解密 + + 完整性校验 · 防恶意加载 + + + + + + + 运行时 + + 沙箱隔离 + 监控 + + 运行时防护 + + + 从开发到运行时的全链路安全防护 + diff --git a/docs/security-flow.svg b/docs/security-flow.svg new file mode 100644 index 0000000..cf9b7a2 --- /dev/null +++ b/docs/security-flow.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + NBPF 安全流程 + + + 打包流程 + + + 源码 + + + + + NIR 编译 + + + + + 双层加密 + + + + + 三层签名 + + + + + .nbpf + + + + + + 加载流程 + + + .nbpf + + + + + 签名验证链 + + + + + 密钥解密 + + + + + AES 解密 + + + + + 加载运行 + diff --git a/oss/cli.py b/oss/cli.py index 0cf4b0d..e3fb94b 100644 --- a/oss/cli.py +++ b/oss/cli.py @@ -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() @@ -43,42 +76,44 @@ def cli(ctx, config): def serve(ctx, host, port, tcp_port): """启动 NebulaShell 服务端""" config = ctx.obj.get('config', get_config()) - - # 命令行参数覆盖配置 + if host: config.set('HOST', host) if port: config.set('HTTP_API_PORT', port) if tcp_port: config.set('HTTP_TCP_PORT', tcp_port) - - log = 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 '无'}") @@ -103,13 +136,12 @@ def info(ctx): click.echo(f"插件仓库:{config.store_dir.absolute()}") 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,50 +154,234 @@ 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() diff --git a/oss/config/config.py b/oss/config/config.py index 425996f..7a3d8bd 100644 --- a/oss/config/config.py +++ b/oss/config/config.py @@ -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() diff --git a/oss/core/engine.py b/oss/core/engine.py new file mode 100644 index 0000000..bffde0d --- /dev/null +++ b/oss/core/engine.py @@ -0,0 +1,1687 @@ +"""NebulaShell Core Engine — 核心引擎 + +整合功能: +- 插件加载(目录结构) +- 生命周期管理 +- 依赖解析 +- 签名校验(RSA-SHA256) +- PL 注入(沙箱执行) +- 能力注册 +- 文件监控与热重载 +- HTTP 服务(子模块) +- REPL 终端(子模块) +- 全面防护:完整性检查、内存保护、行为审计、防篡改监控、降级恢复 +- 数据存储接口(为 data-store 插件预留) +""" +import sys +import json +import re +import os +import time +import types +import hashlib +import threading +import traceback +import importlib.util +import functools +from pathlib import Path +from typing import Any, Optional, Callable +from collections import deque, defaultdict + +from oss.plugin.types import Plugin, register_plugin_type +from oss.plugin.capabilities import scan_capabilities +from oss.logger.logger import Log +from oss.config import get_config + + +# ═══════════════════════════════════════════════════════════════ +# 生命周期管理 +# ═══════════════════════════════════════════════════════════════ + +class LifecycleState: + PENDING = "pending" + RUNNING = "running" + STOPPED = "stopped" + DEGRADED = "degraded" + CRASHED = "crashed" + + +class LifecycleError(Exception): + pass + + +class Lifecycle: + VALID_TRANSITIONS = { + LifecycleState.PENDING: [LifecycleState.RUNNING], + LifecycleState.RUNNING: [LifecycleState.STOPPED, LifecycleState.DEGRADED, LifecycleState.CRASHED], + LifecycleState.STOPPED: [LifecycleState.RUNNING], + LifecycleState.DEGRADED: [LifecycleState.RUNNING, LifecycleState.STOPPED], + LifecycleState.CRASHED: [LifecycleState.PENDING, LifecycleState.STOPPED], + } + + 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": [], + "on_crash": [], "on_degrade": [], + } + 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 in (LifecycleState.RUNNING, LifecycleState.DEGRADED): + 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 mark_crashed(self): + self.transition(LifecycleState.CRASHED) + for hook in self._hooks["on_crash"]: + hook(self) + + def mark_degraded(self): + self.transition(LifecycleState.DEGRADED) + for hook in self._hooks["on_degrade"]: + hook(self) + + 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 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 + + +# ═══════════════════════════════════════════════════════════════ +# 插件信息 +# ═══════════════════════════════════════════════════════════════ + +class PluginInfo: + """插件信息""" + def __init__(self): + self.name: str = "" + self.version: str = "" + self.author: str = "" + self.description: str = "" + self.readme: str = "" + self.config: dict[str, Any] = {} + self.extensions: dict[str, Any] = {} + self.lifecycle: Any = None + self.capabilities: set[str] = set() + self.dependencies: list[str] = [] + self.pl_injected: bool = False + self.file_hash: str = "" # 文件完整性 hash + + +# ═══════════════════════════════════════════════════════════════ +# 权限与代理 +# ═══════════════════════════════════════════════════════════════ + +class PermissionError(Exception): + """权限错误""" + pass + + +class PluginProxy: + """插件代理 - 防止越级访问""" + def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict): + self._plugin_name = plugin_name + self._plugin_instance = plugin_instance + self._allowed_plugins = set(allowed_plugins) + self._all_plugins = all_plugins + + def get_plugin(self, name: str) -> Any: + if name not in self._allowed_plugins and "*" not in self._allowed_plugins: + raise PermissionError(f"插件 '{self._plugin_name}' 无权访问插件 '{name}'") + if name not in self._all_plugins: + return None + return self._all_plugins[name]["instance"] + + def list_plugins(self) -> list[str]: + if "*" in self._allowed_plugins: + return list(self._all_plugins.keys()) + return [n for n in self._allowed_plugins if n in self._all_plugins] + + def get_capability(self, capability: str) -> Any: + return None + + def __getattr__(self, name: str): + return getattr(self._plugin_instance, name) + + +# ═══════════════════════════════════════════════════════════════ +# 能力注册表 +# ═══════════════════════════════════════════════════════════════ + +class CapabilityRegistry: + """能力注册表""" + def __init__(self, permission_check: bool = True): + self.providers: dict = {} + self.consumers: dict = {} + self.permission_check = permission_check + + def register_provider(self, capability: str, plugin_name: str, instance: Any): + self.providers[capability] = {"plugin": plugin_name, "instance": instance} + if capability not in self.consumers: + self.consumers[capability] = [] + + def register_consumer(self, capability: str, plugin_name: str): + if capability not in self.consumers: + self.consumers[capability] = [] + if plugin_name not in self.consumers[capability]: + self.consumers[capability].append(plugin_name) + + def get_provider(self, capability: str, requester: str = "", allowed_plugins: list = None) -> Optional[Any]: + if capability not in self.providers: + return None + if self.permission_check and allowed_plugins is not None: + pn = self.providers[capability]["plugin"] + if pn != requester and pn not in allowed_plugins and "*" not in allowed_plugins: + raise PermissionError(f"插件 '{requester}' 无权使用能力 '{capability}'") + return self.providers[capability]["instance"] + + def has_capability(self, capability: str) -> bool: + return capability in self.providers + + def get_consumers(self, capability: str) -> list: + return self.consumers.get(capability, []) + + +# ═══════════════════════════════════════════════════════════════ +# 依赖解析 +# ═══════════════════════════════════════════════════════════════ + +class DependencyError(Exception): + pass + + +class DependencyResolver: + def __init__(self): + self.graph: dict[str, list[str]] = {} + + 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 SignatureError(Exception): + pass + + +class SignatureVerifier: + def __init__(self, key_dir: str = None): + config = get_config() + self.key_dir = Path(key_dir or str(config.get("SIGNATURE_KEYS_DIR", "./data/signature-verifier/keys"))) + self.key_dir.mkdir(parents=True, exist_ok=True) + self.public_keys: dict[str, bytes] = {} + self._load_builtin_keys() + + def _load_builtin_keys(self): + pub_dir = self.key_dir / "public" + if not pub_dir.exists(): + return + for key_file in pub_dir.glob("*.pem"): + author_name = key_file.stem + self.public_keys[author_name] = key_file.read_bytes() + + def _compute_plugin_hash(self, plugin_dir: Path) -> str: + hasher = hashlib.sha256() + files_to_hash = [] + for file_path in sorted(plugin_dir.rglob("*")): + if file_path.is_file() and file_path.name != "SIGNATURE": + rel_path = file_path.relative_to(plugin_dir) + files_to_hash.append((str(rel_path), file_path)) + for rel_path, file_path in files_to_hash: + hasher.update(rel_path.encode("utf-8")) + hasher.update(file_path.read_bytes()) + return hasher.hexdigest() + + def verify_plugin(self, plugin_dir: Path, author: str = "Falck") -> tuple[bool, str]: + import base64 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.backends import default_backend + from cryptography.exceptions import InvalidSignature + + signature_file = plugin_dir / "SIGNATURE" + if not signature_file.exists(): + return False, f"Plugin missing signature file: {plugin_dir}" + try: + sig_data = json.loads(signature_file.read_text()) + except json.JSONDecodeError as e: + return False, f"Signature file format error: {e}" + required_fields = ["signature", "signer", "algorithm", "timestamp"] + for field in required_fields: + if field not in sig_data: + return False, f"Signature missing required field: {field}" + signer = sig_data["signer"] + signature = base64.b64decode(sig_data["signature"]) + if signer not in self.public_keys: + return False, f"Unknown signer: {signer}" + try: + public_key = serialization.load_pem_public_key( + self.public_keys[signer], backend=default_backend() + ) + except Exception as e: + return False, f"Public key load failed: {e}" + current_hash = self._compute_plugin_hash(plugin_dir) + try: + signed_data = f"{author}:{current_hash}".encode("utf-8") + public_key.verify( + signature, signed_data, + padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), + hashes.SHA256() + ) + return True, f"Signature verified (signer: {signer})" + except InvalidSignature: + return False, f"Signature mismatch! Plugin may have been tampered with (signer: {signer})" + except Exception as e: + return False, f"Signature verification error: {e}" + + def is_official_plugin(self, plugin_dir: Path) -> bool: + """检查是否为官方插件(使用内置公钥验证)""" + result, _ = self.verify_plugin(plugin_dir, author="NebulaShell") + return result + + +class PluginSigner: + def __init__(self, private_key_path: str = None): + self.private_key = None + if private_key_path: + self.load_private_key(private_key_path) + + def load_private_key(self, key_path: str): + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.backends import default_backend + with open(key_path, "rb") as f: + pem_data = f.read() + self.private_key = serialization.load_pem_private_key( + pem_data, password=None, backend=default_backend() + ) + + def sign_plugin(self, plugin_dir: Path, signer_name: str, author: str = "Falck") -> str: + import base64 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.backends import default_backend + + if not self.private_key: + raise ValueError("Private key not loaded") + hasher = hashlib.sha256() + files_to_hash = [] + for file_path in sorted(plugin_dir.rglob("*")): + if file_path.is_file() and file_path.name not in ("SIGNATURE",): + rel_path = file_path.relative_to(plugin_dir) + files_to_hash.append((str(rel_path), file_path)) + for rel_path, file_path in files_to_hash: + hasher.update(rel_path.encode("utf-8")) + hasher.update(file_path.read_bytes()) + plugin_hash = hasher.hexdigest() + signed_data = f"{author}:{plugin_hash}".encode("utf-8") + signature = self.private_key.sign( + signed_data, + padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), + hashes.SHA256() + ) + sig_data = { + "signature": base64.b64encode(signature).decode(), + "signer": signer_name, + "algorithm": "RSA-SHA256", + "timestamp": time.time(), + "plugin_hash": plugin_hash, + "author": author + } + signature_file = plugin_dir / "SIGNATURE" + signature_file.write_text(json.dumps(sig_data, indent=2)) + return str(signature_file) + + +# ═══════════════════════════════════════════════════════════════ +# 文件监控与热重载 +# ═══════════════════════════════════════════════════════════════ + +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 + self._thread = threading.Thread(target=self._watch_loop, daemon=True) + self._thread.start() + Log.info("Core", "文件监控已启动") + + def stop(self): + self._running = False + if self._thread: + self._thread.join(timeout=5) + + def _watch_loop(self): + """监控文件变化,触发热重载回调""" + while self._running: + try: + for watch_dir in self.watch_dirs: + p = Path(watch_dir) + if not p.exists(): + continue + for f in p.rglob("*"): + if not f.is_file() or f.suffix not in self.extensions: + continue + current_mtime = f.stat().st_mtime + last_mtime = self._file_times.get(str(f)) + if last_mtime is not None and current_mtime > last_mtime: + self._file_times[str(f)] = current_mtime + try: + self.callback(str(f)) + except Exception as e: + Log.error("Core", f"热重载回调执行失败: {e}") + elif last_mtime is None: + self._file_times[str(f)] = current_mtime + except Exception as e: + Log.error("Core", f"文件监控异常: {e}") + time.sleep(2) + + +# ═══════════════════════════════════════════════════════════════ +# 全面防护机制 +# ═══════════════════════════════════════════════════════════════ + +class IntegrityChecker: + """文件完整性检查""" + + def __init__(self): + self._hashes: dict[str, str] = {} + + def compute_hash(self, plugin_dir: Path) -> str: + """计算插件目录的 SHA-256 hash""" + hasher = hashlib.sha256() + for file_path in sorted(plugin_dir.rglob("*")): + if file_path.is_file() and file_path.name not in ("SIGNATURE", "__pycache__"): + rel_path = str(file_path.relative_to(plugin_dir)) + hasher.update(rel_path.encode("utf-8")) + hasher.update(file_path.read_bytes()) + return hasher.hexdigest() + + def register(self, plugin_name: str, plugin_dir: Path): + """注册插件的初始 hash""" + self._hashes[plugin_name] = self.compute_hash(plugin_dir) + + def verify(self, plugin_name: str, plugin_dir: Path) -> tuple[bool, str]: + """验证插件文件是否被篡改""" + if plugin_name not in self._hashes: + return False, f"插件 '{plugin_name}' 未注册完整性检查" + current = self.compute_hash(plugin_dir) + if current == self._hashes[plugin_name]: + return True, "完整性验证通过" + return False, f"文件 hash 不匹配,插件可能被篡改" + + def get_hash(self, plugin_name: str) -> Optional[str]: + return self._hashes.get(plugin_name) + + +class MemoryGuard: + """运行时内存保护 - 防止插件修改 Core 内部状态""" + + FROZEN_ATTRS = { + "plugins", "capability_registry", "lifecycle_manager", + "dependency_resolver", "signature_verifier", "pl_injector", + "integrity_checker", "audit_logger", "tamper_monitor", + "fallback_manager", "http_server", "repl_shell", + } + + def __init__(self, manager: 'PluginManager'): + self._manager = manager + self._protected = True + + def enable(self): + self._protected = True + + def disable(self): + self._protected = False + + def check_setattr(self, obj: Any, name: str, value: Any) -> bool: + """检查是否允许设置属性,返回 False 表示拒绝""" + if not self._protected: + return True + if obj is self._manager and name in self.FROZEN_ATTRS: + Log.warn("Core", f"内存防护: 阻止了对 Core 内部属性 '{name}' 的修改") + return False + return True + + +class AuditLogger: + """插件行为审计""" + + def __init__(self, max_logs: int = 1000): + self._logs: deque = deque(maxlen=max_logs) + self._enabled = True + + def enable(self): + self._enabled = True + + def disable(self): + self._enabled = False + + def log(self, plugin_name: str, action: str, detail: str = ""): + """记录插件行为""" + if not self._enabled: + return + self._logs.append({ + "time": time.time(), + "plugin": plugin_name, + "action": action, + "detail": detail, + }) + + def get_logs(self, plugin_name: str = None, limit: int = 50) -> list[dict]: + """查询审计日志""" + if plugin_name: + filtered = [log for log in self._logs if log["plugin"] == plugin_name] + else: + filtered = list(self._logs) + return filtered[-limit:] + + def get_stats(self) -> dict: + """获取审计统计""" + stats: dict[str, int] = {} + for log in self._logs: + stats[log["plugin"]] = stats.get(log["plugin"], 0) + 1 + return stats + + +class TamperMonitor: + """防篡改监控 - 定期检查已加载插件的文件完整性""" + + def __init__(self, manager: 'PluginManager', interval: int = 30): + self._manager = manager + self._interval = interval + self._running = False + self._thread = None + self._alerts: deque = deque(maxlen=100) + + def start(self): + self._running = True + self._thread = threading.Thread(target=self._monitor_loop, daemon=True) + self._thread.start() + Log.info("Core", f"防篡改监控已启动 (间隔: {self._interval}s)") + + def stop(self): + self._running = False + if self._thread: + self._thread.join(timeout=5) + + def _monitor_loop(self): + while self._running: + try: + for plugin_name, info in self._manager.plugins.items(): + plugin_dir = self._manager._get_plugin_dir(plugin_name) + if not plugin_dir: + continue + valid, msg = self._manager.integrity_checker.verify(plugin_name, plugin_dir) + if not valid: + alert = { + "time": time.time(), + "plugin": plugin_name, + "message": msg, + } + self._alerts.append(alert) + Log.error("Core", f"防篡改告警: 插件 '{plugin_name}' 可能被篡改!") + # 自动停止被篡改的插件 + try: + info["instance"].stop() + lifecycle = self._manager.lifecycle_manager.get(plugin_name) + if lifecycle: + lifecycle.mark_crashed() + except Exception as e: + Log.error("Core", f"停止被篡改插件 '{plugin_name}' 失败: {e}") + except Exception as e: + Log.error("Core", f"防篡改监控异常: {e}") + time.sleep(self._interval) + + def get_alerts(self) -> list[dict]: + return list(self._alerts) + + +class FallbackManager: + """降级恢复机制 - 插件崩溃时自动重启""" + + def __init__(self, manager: 'PluginManager', max_retries: int = 3): + self._manager = manager + self._max_retries = max_retries + self._retry_counts: dict[str, int] = {} + self._degraded: set[str] = set() + + def wrap_plugin_method(self, plugin_name: str, method: Callable) -> Callable: + """包装插件方法,捕获异常后自动重试""" + + @functools.wraps(method) + def safe_method(*args, **kwargs): + try: + return method(*args, **kwargs) + except Exception as e: + Log.error("Core", f"插件 '{plugin_name}' 方法 '{method.__name__}' 异常: {e}") + self._handle_crash(plugin_name) + return None + + return safe_method + + def _handle_crash(self, plugin_name: str): + """处理插件崩溃""" + retry_count = self._retry_counts.get(plugin_name, 0) + lifecycle = self._manager.lifecycle_manager.get(plugin_name) + + if retry_count < self._max_retries: + self._retry_counts[plugin_name] = retry_count + 1 + Log.warn("Core", f"插件 '{plugin_name}' 崩溃,正在重启 (第 {retry_count + 1}/{self._max_retries} 次)") + try: + if lifecycle: + lifecycle.mark_crashed() + self._manager._restart_plugin(plugin_name) + if lifecycle: + lifecycle.start() + Log.ok("Core", f"插件 '{plugin_name}' 重启成功") + except Exception as e: + Log.error("Core", f"插件 '{plugin_name}' 重启失败: {e}") + else: + Log.error("Core", f"插件 '{plugin_name}' 超过最大重试次数 ({self._max_retries}),标记为降级") + self._degraded.add(plugin_name) + if lifecycle: + lifecycle.mark_degraded() + + def recover(self, plugin_name: str) -> bool: + """手动恢复降级的插件""" + if plugin_name not in self._degraded: + return False + self._retry_counts[plugin_name] = 0 + self._degraded.discard(plugin_name) + try: + self._manager._restart_plugin(plugin_name) + lifecycle = self._manager.lifecycle_manager.get(plugin_name) + if lifecycle: + lifecycle.start() + Log.ok("Core", f"插件 '{plugin_name}' 已手动恢复") + return True + except Exception as e: + Log.error("Core", f"恢复插件 '{plugin_name}' 失败: {e}") + return False + + def is_degraded(self, plugin_name: str) -> bool: + return plugin_name in self._degraded + + def get_degraded_plugins(self) -> list[str]: + return list(self._degraded) + + +# ═══════════════════════════════════════════════════════════════ +# 数据存储接口(为 data-store 插件预留) +# ═══════════════════════════════════════════════════════════════ + +class DataStore: + """数据存储抽象接口 + + 默认实现使用 JSON 文件存储到 ~/.nebula/data/ + 后续可由 data-store 插件替换为更完善的实现 + """ + + def __init__(self): + config = get_config() + data_dir_env = os.environ.get("NEBULA_DATA_DIR", "") + default_dir = Path(data_dir_env) if data_dir_env else Path.home() / ".nebula" / "data" + self._base_dir = Path(config.get("DATA_DIR", str(default_dir))) + self._base_dir.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + + def _plugin_dir(self, plugin_name: str) -> Path: + """获取插件专属数据目录""" + pd = self._base_dir / plugin_name + pd.mkdir(parents=True, exist_ok=True) + return pd + + def save(self, plugin_name: str, key: str, data: Any) -> bool: + """保存数据""" + with self._lock: + try: + file_path = self._plugin_dir(plugin_name) / f"{key}.json" + file_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") + return True + except Exception as e: + Log.error("Core", f"数据存储保存失败 [{plugin_name}/{key}]: {e}") + return False + + def load(self, plugin_name: str, key: str, default: Any = None) -> Any: + """加载数据""" + with self._lock: + try: + file_path = self._plugin_dir(plugin_name) / f"{key}.json" + if file_path.exists(): + return json.loads(file_path.read_text(encoding="utf-8")) + return default + except Exception as e: + Log.error("Core", f"数据存储加载失败 [{plugin_name}/{key}]: {e}") + return default + + def delete(self, plugin_name: str, key: str) -> bool: + """删除数据""" + with self._lock: + try: + file_path = self._plugin_dir(plugin_name) / f"{key}.json" + if file_path.exists(): + file_path.unlink() + return True + except Exception as e: + Log.error("Core", f"数据存储删除失败 [{plugin_name}/{key}]: {e}") + return False + + def list_keys(self, plugin_name: str) -> list[str]: + """列出插件所有数据键""" + pd = self._plugin_dir(plugin_name) + if not pd.exists(): + return [] + return [f.stem for f in pd.glob("*.json")] + + def set_custom_path(self, plugin_name: str, custom_path: str) -> bool: + """插件自定义存储路径(不能修改到项目目录内)""" + path = Path(custom_path).expanduser().resolve() + project_dir = Path.cwd().resolve() + if str(path).startswith(str(project_dir)): + Log.error("Core", f"插件 '{plugin_name}' 试图将数据存储到项目目录: {custom_path}") + return False + path.mkdir(parents=True, exist_ok=True) + # 创建符号链接或记录映射 + mapping_file = self._base_dir / "_custom_paths.json" + mappings = {} + if mapping_file.exists(): + try: + mappings = json.loads(mapping_file.read_text()) + except (json.JSONDecodeError, OSError): + pass + mappings[plugin_name] = str(path) + mapping_file.write_text(json.dumps(mappings, indent=2)) + return True + + +# ═══════════════════════════════════════════════════════════════ +# PL 注入 +# ═══════════════════════════════════════════════════════════════ + +class PLValidationError(Exception): + """PL 校验错误""" + pass + + +class PLInjector: + """PL 注入管理器 - 带完整安全限制""" + + MAX_FUNCTIONS_PER_PLUGIN = 50 + MAX_REGISTRATIONS_PER_NAME = 10 + MAX_NAME_LENGTH = 128 + MAX_DESCRIPTION_LENGTH = 256 + + _FUNCTION_NAME_RE = re.compile(r'^[a-zA-Z0-9_:/\-.]+$') + _EVENT_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]+$') + _ROUTE_PATH_RE = re.compile(r'^/[a-zA-Z0-9_\-/.]+$') + _FORBIDDEN_ROUTE_PATTERNS = [r'\.\.', r'//', r'/\.', r'~', r'\%'] + + def __init__(self, plugin_manager: 'PluginManager'): + self._plugin_manager = plugin_manager + self._injections: dict = {} + self._injection_registry: dict = {} + self._plugin_function_count: dict = {} + + def check_and_load_pl(self, plugin_dir: Path, plugin_name: str) -> bool: + """检查并加载 PL 文件夹,返回 True 表示成功""" + pl_dir = plugin_dir / "PL" + if not pl_dir.exists() or not pl_dir.is_dir(): + Log.warn("Core", f"插件 '{plugin_name}' 声明了 pl_injection,但缺少 PL/ 文件夹,拒绝加载") + return False + + pl_main = pl_dir / "main.py" + if not pl_main.exists(): + Log.warn("Core", f"插件 '{plugin_name}' 的 PL/ 文件夹中缺少 main.py,拒绝加载") + return False + + # 禁止危险文件类型 + forbidden_ext = {'.sh', '.bat', '.exe', '.dll', '.so', '.dylib', '.bin'} + for f in pl_dir.rglob('*'): + if f.suffix.lower() in forbidden_ext: + Log.error("Core", f"插件 '{plugin_name}' 的 PL/ 文件夹包含危险文件: {f.name},拒绝加载") + return False + + try: + # 受限沙箱 + 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, 'setattr': setattr, + 'ValueError': ValueError, 'TypeError': TypeError, + 'KeyError': KeyError, 'IndexError': IndexError, + 'Exception': Exception, 'BaseException': BaseException, + } + safe_globals = { + '__builtins__': safe_builtins, + '__name__': f'plugin.{plugin_name}.PL', + '__package__': f'plugin.{plugin_name}.PL', + '__file__': str(pl_main), + } + + with open(pl_main, 'r', encoding='utf-8') as f: + source = f.read() + + # 静态源码安全检查 + self._static_source_check(source, str(pl_main)) + + code = compile(source, str(pl_main), 'exec') + exec(code, safe_globals) + + register_func = safe_globals.get('register') + if register_func and callable(register_func): + register_func(self) + Log.ok("Core", f"插件 '{plugin_name}' PL 注入成功") + else: + Log.warn("Core", f"插件 '{plugin_name}' 的 PL/main.py 缺少 register() 函数,但仍允许加载") + + self._injections[plugin_name] = {"dir": str(pl_dir)} + return True + + except PLValidationError as e: + Log.error("Core", f"插件 '{plugin_name}' PL 安全检查失败: {e}") + return False + except SyntaxError as e: + Log.error("Core", f"插件 '{plugin_name}' PL/main.py 语法错误: {e}") + return False + except FileNotFoundError as e: + Log.error("Core", f"插件 '{plugin_name}' PL 文件不存在:{e}") + return False + except PermissionError as e: + Log.error("Core", f"插件 '{plugin_name}' PL 文件权限错误:{e}") + return False + except Exception as e: + Log.error("Core", f"加载插件 '{plugin_name}' 的 PL 失败:{type(e).__name__}: {e}") + traceback.print_exc() + return False + + def _static_source_check(self, source: str, file_path: str): + """静态源码安全检查 - 增强版,防止字符串拼接/编码绕过""" + import base64 + + # 首先检查是否有 base64 编码的恶意代码 + try: + string_pattern = r'([A-Za-z0-9+/=]{20,})' + for match in re.finditer(string_pattern, source): + try: + decoded = base64.b64decode(match.group(1)).decode('utf-8', errors='ignore') + for dangerous in ['import ', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess']: + if dangerous in decoded: + raise PLValidationError(f"{file_path} - 检测到 base64 编码的恶意代码") + except Exception: + pass + except Exception: + pass + + # 检查字符串拼接绕过 + concat_patterns = [ + r"""['"]ex['"]\s*\+\s*['"]ec['"]""", + r"""['"]impor['"]\s*\+\s*['"]t['"]""", + r"""['"]eva['"]\s*\+\s*['"]l['"]""", + r"""['"]compil['"]\s*\+\s*['"]e['"]""", + ] + for pattern in concat_patterns: + if re.search(pattern, source): + raise PLValidationError(f"{file_path} - 检测到字符串拼接绕过尝试") + + forbidden = [ + (r'^\s*import\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)', '禁止导入系统级模块'), + (r'^\s*from\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)\s+import', '禁止导入系统级模块'), + (r'__import__\s*\(', '禁止使用 __import__'), + (r'(? bool: + if not name or not isinstance(name, str): + return False + if len(name) > self.MAX_NAME_LENGTH: + return False + return bool(self._FUNCTION_NAME_RE.match(name)) + + def _validate_route_path(self, path: str) -> bool: + if not path or not isinstance(path, str): + return False + if len(path) > 256: + return False + if not self._ROUTE_PATH_RE.match(path): + return False + for p in self._FORBIDDEN_ROUTE_PATTERNS: + if re.search(p, path): + return False + return True + + def _validate_event_name(self, event_name: str) -> bool: + if not event_name or not isinstance(event_name, str): + return False + if len(event_name) > self.MAX_NAME_LENGTH: + return False + return bool(self._EVENT_NAME_RE.match(event_name)) + + def _check_plugin_limit(self, plugin_name: str) -> bool: + count = self._plugin_function_count.get(plugin_name, 0) + if count >= self.MAX_FUNCTIONS_PER_PLUGIN: + Log.warn("Core", f"插件 '{plugin_name}' 注册功能数已达上限 ({self.MAX_FUNCTIONS_PER_PLUGIN})") + return False + return True + + def _check_name_limit(self, name: str) -> bool: + registrations = self._injection_registry.get(name, []) + if len(registrations) >= self.MAX_REGISTRATIONS_PER_NAME: + Log.warn("Core", f"功能名称 '{name}' 注册次数已达上限 ({self.MAX_REGISTRATIONS_PER_NAME})") + return False + return True + + def _wrap_function(self, func: Callable, plugin_name: str, name: str) -> Callable: + """包装函数,异常安全""" + def _safe_wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + Log.error("Core", f"PL 注入功能 '{name}' (来自 {plugin_name}) 执行异常: {e}") + return None + return _safe_wrapper + + def _get_caller_plugin_name(self) -> Optional[str]: + """通过栈帧回溯获取调用者插件名""" + stack = traceback.extract_stack() + for frame in stack: + filename = frame.filename + if '/PL/' in filename and 'main.py' in filename: + parts = Path(filename).parts + for i, part in enumerate(parts): + if part == 'PL': + return parts[i - 1] if i > 0 else None + return None + + def register_function(self, name: str, func: Callable, description: str = ""): + """注册注入功能 - 带参数校验和权限限制""" + if not self._validate_function_name(name): + Log.error("Core", f"PL 注入功能名称非法: '{name}'") + return + if not callable(func): + Log.error("Core", f"PL 注入功能 '{name}' 不是可调用对象") + return + if description and len(description) > self.MAX_DESCRIPTION_LENGTH: + description = description[:self.MAX_DESCRIPTION_LENGTH] + + plugin_name = self._get_caller_plugin_name() or "unknown" + + if not self._check_plugin_limit(plugin_name): + return + if not self._check_name_limit(name): + return + + wrapped_func = self._wrap_function(func, plugin_name, name) + + if name not in self._injection_registry: + self._injection_registry[name] = [] + self._injection_registry[name].append({ + "func": wrapped_func, "plugin": plugin_name, "description": description, + }) + self._plugin_function_count[plugin_name] = self._plugin_function_count.get(plugin_name, 0) + 1 + Log.tip("Core", f"PL 注入功能已注册: '{name}' (来自 {plugin_name})") + + def register_route(self, method: str, path: str, handler: Callable): + """注册 HTTP 路由 - 带路径安全校验""" + valid_methods = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'} + method_upper = method.upper() + if method_upper not in valid_methods: + Log.error("Core", f"PL 注入路由方法非法: '{method}'") + return + if not self._validate_route_path(path): + Log.error("Core", f"PL 注入路由路径非法: '{path}'") + return + self.register_function(f"{method_upper}:{path}", handler, f"路由 {method_upper} {path}") + + def register_event_handler(self, event_name: str, handler: Callable): + """注册事件处理器 - 带名称校验""" + if not self._validate_event_name(event_name): + Log.error("Core", f"PL 注入事件名称非法: '{event_name}'") + return + self.register_function(f"event:{event_name}", handler, f"事件 {event_name}") + + def get_injected_functions(self, name: str = None) -> list[Callable]: + if name: + return [e["func"] for e in self._injection_registry.get(name, [])] + return [f for es in self._injection_registry.values() for f in [e["func"] for e in es]] + + def get_injection_info(self, plugin_name: str = None) -> dict: + if plugin_name: + return self._injections.get(plugin_name, {}) + return dict(self._injections) + + def has_injection(self, plugin_name: str) -> bool: + return plugin_name in self._injections + + def get_registry_info(self) -> dict: + info = {} + for name, entries in self._injection_registry.items(): + info[name] = { + "count": len(entries), + "plugins": [e["plugin"] for e in entries], + "descriptions": [e["description"] for e in entries], + } + return info + + +# ═══════════════════════════════════════════════════════════════ +# PluginManager — 核心管理器 +# ═══════════════════════════════════════════════════════════════ + +class PluginManager: + """插件管理器 — Core 的核心""" + + def __init__(self, permission_check: bool = True): + self.plugins: dict = {} + self.capability_registry = CapabilityRegistry(permission_check=permission_check) + self.permission_check = permission_check + self.enforce_signature = True + self.pl_injector = PLInjector(self) + self.lifecycle_manager = LifecycleManager() + self.dependency_resolver = DependencyResolver() + self.signature_verifier = SignatureVerifier() + self.hot_reload_watcher = None + + # 全面防护 + self.integrity_checker = IntegrityChecker() + self.memory_guard = MemoryGuard(self) + self.audit_logger = AuditLogger() + self.tamper_monitor = TamperMonitor(self) + self.fallback_manager = FallbackManager(self) + + # 数据存储 + self.data_store = DataStore() + + # HTTP 服务 & REPL + self.http_server = None + self.repl_shell = None + + # NBPF 组件 + self.nbpf_loader = None + self._nbpf_initialized = False + + # 插件目录映射 + self._plugin_dirs: dict[str, Path] = {} + + # ── NBPF 支持 ── + + def _init_nbpf(self): + """初始化 NBPF 加载器""" + if self._nbpf_initialized: + return + try: + from oss.core.nbpf import NBPFLoader, NBPCrypto, NIRCompiler + + config = get_config() + trusted_keys_dir = Path(config.get("NBPF_TRUSTED_KEYS_DIR", "./data/nbpf-keys/trusted")) + rsa_keys_dir = Path(config.get("NBPF_RSA_KEYS_DIR", "./data/nbpf-keys/rsa")) + + # 加载信任的 Ed25519 公钥 + trusted_ed25519 = {} + if trusted_keys_dir.exists(): + for kf in trusted_keys_dir.glob("*.pem"): + name = kf.stem + trusted_ed25519[name] = kf.read_bytes() + + # 加载信任的 RSA 公钥 + trusted_rsa = {} + if rsa_keys_dir.exists(): + for kf in rsa_keys_dir.glob("*.pem"): + name = kf.stem + trusted_rsa[name] = kf.read_bytes() + + # 加载 RSA 私钥 + rsa_private = None + private_dir = Path(config.get("NBPF_KEYS_DIR", "./data/nbpf-keys")) / "private" + if private_dir.exists(): + pk_files = list(private_dir.glob("*.pem")) + if pk_files: + rsa_private = pk_files[0].read_bytes() + + self.nbpf_loader = NBPFLoader( + crypto=NBPCrypto(), + compiler=NIRCompiler(), + trusted_ed25519_keys=trusted_ed25519, + trusted_rsa_keys=trusted_rsa, + rsa_private_key=rsa_private, + ) + self._nbpf_initialized = True + Log.info("Core", "NBPF 加载器已初始化") + except Exception as e: + Log.warn("Core", f"NBPF 加载器初始化失败: {e}") + + def load_nbpf(self, nbpf_path: Path, plugin_name: str = None) -> Optional[Any]: + """加载 .nbpf 插件文件 + + Args: + nbpf_path: .nbpf 文件路径 + plugin_name: 可选,插件名称 + + Returns: + 插件实例,失败返回 None + """ + if not self._nbpf_initialized: + self._init_nbpf() + if self.nbpf_loader is None: + Log.error("Core", "NBPF 加载器未初始化,无法加载 .nbpf 文件") + return None + + try: + instance, info = self.nbpf_loader.load(nbpf_path, plugin_name) + name = info["name"] + + # 构建 PluginInfo + pinfo = PluginInfo() + pinfo.name = name + pinfo.version = info.get("version", "") + pinfo.author = info.get("author", "") + pinfo.description = info.get("description", "") + pinfo.dependencies = info.get("manifest", {}).get("dependencies", []) + + # 注册到插件列表 + self.plugins[name] = { + "instance": instance, + "module": None, + "info": pinfo, + "permissions": [], + "nbpf_path": str(nbpf_path), + } + self._plugin_dirs[name] = nbpf_path.parent + + # 生命周期 + pinfo.lifecycle = self.lifecycle_manager.create(name) + + # 审计日志 + self.audit_logger.log(name, "loaded", f".nbpf 版本 {pinfo.version}") + + Log.ok("Core", f"NBPF 插件 '{name}' 加载成功") + return instance + except Exception as e: + Log.error("Core", f"NBPF 插件加载失败: {e}") + return None + + def _get_plugin_dir(self, plugin_name: str) -> Optional[Path]: + return self._plugin_dirs.get(plugin_name) + + def _load_manifest(self, plugin_dir: Path) -> dict: + mf = plugin_dir / "manifest.json" + if not mf.exists(): + return {} + with open(mf, "r", encoding="utf-8") as f: + return json.load(f) + + def _load_readme(self, plugin_dir: Path) -> str: + rf = plugin_dir / "README.md" + if not rf.exists(): + return "" + with open(rf, "r", encoding="utf-8") as f: + return f.read() + + def _parse_config_file(self, file_path: Path, file_type: str) -> dict: + """通用配置文件解析 - 使用 ast.literal_eval 安全解析""" + import ast + if not file_path.exists(): + return {} + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + except FileNotFoundError: + Log.warn("Core", f"{file_type}文件不存在:{file_path}") + return {} + except PermissionError as e: + Log.error("Core", f"{file_type}文件无权限读取:{file_path} - {e}") + return {} + except UnicodeDecodeError as e: + Log.error("Core", f"{file_type}文件编码错误:{file_path} - {e}") + return {} + + try: + result = ast.literal_eval(content) + if isinstance(result, dict): + return {k: v for k, v in result.items() if not k.startswith("_")} + except (ValueError, SyntaxError): + pass + + config = {} + for line in content.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line) + if match: + key, value_str = match.groups() + if key.startswith('_'): + continue + try: + value = ast.literal_eval(value_str) + config[key] = value + except (ValueError, SyntaxError): + Log.warn("Core", f"{file_path} 跳过无效的值:{line}") + continue + return config + + def _load_config(self, plugin_dir: Path) -> dict: + return self._parse_config_file(plugin_dir / "config.py", "配置") + + def _load_extensions(self, plugin_dir: Path) -> dict: + return self._parse_config_file(plugin_dir / "extensions.py", "扩展") + + def load(self, plugin_dir: Path, use_sandbox: bool = True) -> Optional[Any]: + """加载单个插件 + + 支持: + - 目录结构插件(main.py) + - .nbpf 文件(直接传入 .nbpf 路径) + """ + # 如果是 .nbpf 文件,使用 NBPF 加载器 + if plugin_dir.suffix == ".nbpf": + return self.load_nbpf(plugin_dir) + + main_file = plugin_dir / "main.py" + if not main_file.exists(): + return None + + manifest = self._load_manifest(plugin_dir) + readme = self._load_readme(plugin_dir) + config = self._load_config(plugin_dir) + extensions = self._load_extensions(plugin_dir) + capabilities = scan_capabilities(plugin_dir) + plugin_name = plugin_dir.name.rstrip("}") + + # 完整性检查:加载前计算 hash + self.integrity_checker.register(plugin_name, plugin_dir) + + # PL 注入检查 + pl_injection = manifest.get("config", {}).get("args", {}).get("pl_injection", False) + if pl_injection: + Log.tip("Core", f"插件 '{plugin_name}' 声明了 pl_injection,正在检查 PL/ 文件夹...") + if not self.pl_injector.check_and_load_pl(plugin_dir, plugin_name): + Log.error("Core", f"插件 '{plugin_name}' 因 PL 注入检查失败被拒绝加载") + return None + Log.ok("Core", f"插件 '{plugin_name}' PL 注入检查通过") + + permissions = manifest.get("permissions", []) + + spec = importlib.util.spec_from_file_location(f"plugin.{plugin_name}", str(main_file)) + module = importlib.util.module_from_spec(spec) + module.__package__ = f"plugin.{plugin_name}" + module.__path__ = [str(plugin_dir)] + sys.modules[spec.name] = module + spec.loader.exec_module(module) + if not hasattr(module, "New"): + return None + instance = module.New() + + if self.permission_check and permissions: + instance = PluginProxy(plugin_name, instance, permissions, self.plugins) + + info = PluginInfo() + meta = manifest.get("metadata", {}) + info.name = meta.get("name", plugin_name) + info.version = meta.get("version", "") + info.author = meta.get("author", "") + info.description = meta.get("description", "") + info.readme = readme + info.config = manifest.get("config", {}).get("args", config) + info.extensions = extensions + info.capabilities = capabilities + info.dependencies = manifest.get("dependencies", []) + info.pl_injected = pl_injection + info.file_hash = self.integrity_checker.get_hash(plugin_name) or "" + + for cap in capabilities: + self.capability_registry.register_provider(cap, plugin_name, instance) + info.lifecycle = self.lifecycle_manager.create(plugin_name) + + self.plugins[plugin_name] = {"instance": instance, "module": module, "info": info, "permissions": permissions} + self._plugin_dirs[plugin_name] = plugin_dir + + # 审计日志 + self.audit_logger.log(plugin_name, "loaded", f"版本 {info.version}") + + return instance + + def _restart_plugin(self, plugin_name: str): + """重启单个插件""" + if plugin_name not in self.plugins: + return + plugin_dir = self._plugin_dirs.get(plugin_name) + if not plugin_dir: + return + # 停止旧实例 + try: + if hasattr(self.plugins[plugin_name]["instance"], "stop"): + self.plugins[plugin_name]["instance"].stop() + except Exception: + pass + # 从 sys.modules 中移除 + module_name = f"plugin.{plugin_name}" + if module_name in sys.modules: + del sys.modules[module_name] + module_name = f"nbpf.{plugin_name}" + if module_name in sys.modules: + del sys.modules[module_name] + # 重新加载 + del self.plugins[plugin_name] + self.load(plugin_dir) + + def load_all(self, store_dir: str = "store"): + if 'plugin' not in sys.modules: + pkg = types.ModuleType('plugin') + pkg.__path__ = [] + pkg.__package__ = 'plugin' + sys.modules['plugin'] = pkg + Log.tip("Core", "已创建 plugin 命名空间包") + + if not self._check_any_plugins(store_dir): + Log.warn("Core", "未检测到任何插件,自动引导安装...") + self._bootstrap_installation() + + self._load_plugins_from_dir(Path(store_dir)) + self._sort_by_dependencies() + + def _load_plugins_from_dir(self, store_dir: Path): + if not store_dir.exists(): + return + core_plugins = set() + skip = {"Core", "archive"} + plugin_dirs = [] + for ad in store_dir.iterdir(): + if ad.is_dir(): + for pd in ad.iterdir(): + if pd.name in skip: + continue + # 支持目录插件(main.py)和 .nbpf 文件 + if pd.is_dir() and (pd / "main.py").exists(): + priority = 100 + manifest_file = pd / "manifest.json" + if manifest_file.exists(): + try: + meta = json.loads(manifest_file.read_text()).get("metadata", {}) + raw = meta.get("load_priority", 100) + priority = 0 if raw == "first" else (int(raw) if isinstance(raw, (int, float)) else 100) + except (json.JSONDecodeError, OSError, (ValueError, TypeError)): + pass + plugin_dirs.append((priority, pd)) + elif pd.suffix == ".nbpf": + # .nbpf 文件,优先级 50(在普通插件之前加载) + plugin_dirs.append((50, pd)) + plugin_dirs.sort(key=lambda x: x[0]) + for _, pd in plugin_dirs: + self.load(pd, use_sandbox=pd.name not in core_plugins) + self._link_capabilities() + + def _check_any_plugins(self, store_dir: str) -> bool: + sp = Path(store_dir) + if sp.exists(): + for ad in sp.iterdir(): + if ad.is_dir(): + for pd in ad.iterdir(): + if pd.name in {"Core", "archive"}: + continue + if pd.is_dir() and (pd / "main.py").exists(): + return True + if pd.suffix == ".nbpf": + return True + return False + + def _bootstrap_installation(self): + Log.info("Core", "跳过引导安装(无可用插件)") + + def _sort_by_dependencies(self): + for n, i in self.plugins.items(): + self.dependency_resolver.add_dependency(n, i["info"].dependencies) + try: + order = self.dependency_resolver.resolve() + sp = {} + for n in order: + if n in self.plugins: + sp[n] = self.plugins[n] + for n in set(self.plugins.keys()) - set(sp.keys()): + sp[n] = self.plugins[n] + self.plugins = sp + except Exception as e: + Log.error("Core", f"依赖解析失败: {e}") + + def _link_capabilities(self): + for pn, info in self.plugins.items(): + for cap in info["info"].capabilities: + if self.capability_registry.has_capability(cap): + for cn in self.capability_registry.get_consumers(cap): + if cn in self.plugins: + ci = self.plugins[cn]["info"] + ca = self.plugins[cn].get("permissions", []) + try: + p = self.capability_registry.get_provider(cap, requester=cn, allowed_plugins=ca) + if p and hasattr(ci, "extensions"): + ci.extensions[f"_{cap}_provider"] = p + except PermissionError as e: + Log.error("Core", f"权限拒绝: {e}") + + def start_all(self): + self._inject_dependencies() + for n, i in self.plugins.items(): + try: + wrapped = self.fallback_manager.wrap_plugin_method(n, i["instance"].start) + wrapped() + except Exception as e: + Log.error("Core", f"启动失败 {n}: {e}") + + def init_and_start_all(self): + Log.info("Core", f"init_and_start_all 被调用,plugins={len(self.plugins)}") + self._inject_dependencies() + ordered = self._get_ordered_plugins() + Log.tip("Core", f"插件启动顺序: {' -> '.join(ordered)}") + for name in ordered: + if "Core" in name: + continue + try: + Log.info("Core", f"初始化: {name}") + wrapped_init = self.fallback_manager.wrap_plugin_method(name, self.plugins[name]["instance"].init) + wrapped_init() + except Exception as e: + Log.error("Core", f"初始化失败 {name}: {e}") + for name in ordered: + if "Core" in name: + continue + try: + Log.info("Core", f"启动: {name}") + wrapped_start = self.fallback_manager.wrap_plugin_method(name, self.plugins[name]["instance"].start) + wrapped_start() + except Exception as e: + Log.error("Core", f"启动失败 {name}: {e}") + + def _get_ordered_plugins(self) -> list[str]: + try: + return [n for n in self.dependency_resolver.resolve() if n in self.plugins] + except Exception as e: + Log.warn("Core", f"依赖解析失败,使用原始顺序: {e}") + return list(self.plugins.keys()) + + def _inject_dependencies(self): + Log.info("Core", f"开始注入依赖,共 {len(self.plugins)} 个插件") + nm = {} + for n in self.plugins: + c = n.rstrip("}") + nm[c] = n + nm[c + "}"] = n + for n, i in self.plugins.items(): + inst = i["instance"] + io = i.get("info") + if not io or not io.dependencies: + continue + for dn in io.dependencies: + ad = nm.get(dn) or nm.get(dn + "}") + if ad and ad in self.plugins: + sn = f"set_{dn.replace('-', '_')}" + if hasattr(inst, sn): + try: + getattr(inst, sn)(self.plugins[ad]["instance"]) + Log.ok("Core", f"注入成功: {n} <- {ad}") + except Exception as e: + Log.error("Core", f"注入依赖失败 {n}.{sn}: {e}") + else: + Log.warn("Core", f"{n} 没有 {sn} 方法") + + def stop_all(self): + for n, i in reversed(list(self.plugins.items())): + try: + if hasattr(i["instance"], "stop"): + i["instance"].stop() + except Exception as e: + Log.error("Core", f"插件 {n} 停止失败:{type(e).__name__}: {e}") + self.lifecycle_manager.stop_all() + + def get_info(self, name: str) -> Optional[PluginInfo]: + if name in self.plugins: + return self.plugins[name]["info"] + return None + + def has_capability(self, capability: str) -> bool: + return self.capability_registry.has_capability(capability) + + def get_capability_provider(self, capability: str) -> Optional[Any]: + return self.capability_registry.get_provider(capability) + + # ── HTTP 服务 ── + + def start_http_server(self): + """启动 HTTP 服务(子模块)""" + try: + from oss.core.http_api.server import HttpServer + from oss.core.http_api.router import HttpRouter + from oss.core.http_api.middleware import MiddlewareChain + + router = HttpRouter() + middleware = MiddlewareChain() + self.http_server = HttpServer(router=router, middleware=middleware) + self.http_server.start() + Log.ok("Core", "HTTP 服务已启动") + except Exception as e: + Log.error("Core", f"HTTP 服务启动失败: {e}") + + def stop_http_server(self): + """停止 HTTP 服务""" + if self.http_server: + try: + self.http_server.stop() + Log.info("Core", "HTTP 服务已停止") + except Exception as e: + Log.error("Core", f"HTTP 服务停止失败: {e}") + + def get_http_router(self): + """获取 HTTP 路由器""" + if self.http_server: + return self.http_server.router + return None + + # ── REPL ── + + def start_repl(self): + """启动 REPL 终端(子模块)""" + try: + from oss.core.repl.main import NebulaShell + self.repl_shell = NebulaShell(self) + Log.ok("Core", "REPL 终端已启动") + self.repl_shell.cmdloop() + except Exception as e: + Log.error("Core", f"REPL 启动失败: {e}") + + # ── 防护管理 ── + + def start_tamper_monitor(self): + """启动防篡改监控""" + self.tamper_monitor.start() + + def stop_tamper_monitor(self): + """停止防篡改监控""" + self.tamper_monitor.stop() + + def get_audit_logs(self, plugin_name: str = None, limit: int = 50) -> list[dict]: + """获取审计日志""" + return self.audit_logger.get_logs(plugin_name, limit) + + def get_tamper_alerts(self) -> list[dict]: + """获取防篡改告警""" + return self.tamper_monitor.get_alerts() + + def get_degraded_plugins(self) -> list[str]: + """获取降级插件列表""" + return self.fallback_manager.get_degraded_plugins() + + def recover_plugin(self, plugin_name: str) -> bool: + """手动恢复降级插件""" + return self.fallback_manager.recover(plugin_name) + + def get_status(self) -> dict: + """获取 Core 状态摘要""" + nbpf_count = sum(1 for i in self.plugins.values() if i.get("nbpf_path")) + return { + "plugins": { + "total": len(self.plugins), + "nbpf": nbpf_count, + "directory": len(self.plugins) - nbpf_count, + "degraded": self.fallback_manager.get_degraded_plugins(), + }, + "nbpf_loader": self._nbpf_initialized, + "http_server": self.http_server is not None, + "tamper_monitor": self.tamper_monitor._running, + "audit_logs": len(self.audit_logger._logs), + "tamper_alerts": len(self.tamper_monitor._alerts), + "data_store": str(self.data_store._base_dir), + } + + +# ═══════════════════════════════════════════════════════════════ +# 类型注册 +# ═══════════════════════════════════════════════════════════════ + +register_plugin_type("PluginManager", PluginManager) +register_plugin_type("PluginInfo", PluginInfo) +register_plugin_type("CapabilityRegistry", CapabilityRegistry) +register_plugin_type("PLInjector", PLInjector) +register_plugin_type("Lifecycle", Lifecycle) +register_plugin_type("LifecycleManager", LifecycleManager) +register_plugin_type("DependencyResolver", DependencyResolver) +register_plugin_type("SignatureVerifier", SignatureVerifier) +register_plugin_type("IntegrityChecker", IntegrityChecker) +register_plugin_type("AuditLogger", AuditLogger) +register_plugin_type("TamperMonitor", TamperMonitor) +register_plugin_type("FallbackManager", FallbackManager) +register_plugin_type("DataStore", DataStore) \ No newline at end of file diff --git a/store/NebulaShell/code-reviewer/checks/__init__.py b/oss/core/http_api/__init__.py similarity index 100% rename from store/NebulaShell/code-reviewer/checks/__init__.py rename to oss/core/http_api/__init__.py diff --git a/oss/core/http_api/middleware.py b/oss/core/http_api/middleware.py new file mode 100644 index 0000000..669d2f3 --- /dev/null +++ b/oss/core/http_api/middleware.py @@ -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 diff --git a/oss/core/http_api/rate_limiter.py b/oss/core/http_api/rate_limiter.py new file mode 100644 index 0000000..7375c62 --- /dev/null +++ b/oss/core/http_api/rate_limiter.py @@ -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() diff --git a/store/NebulaShell/http-api/router.py b/oss/core/http_api/router.py similarity index 100% rename from store/NebulaShell/http-api/router.py rename to oss/core/http_api/router.py diff --git a/store/NebulaShell/http-api/server.py b/oss/core/http_api/server.py similarity index 95% rename from store/NebulaShell/http-api/server.py rename to oss/core/http_api/server.py index e51beb5..e4fb26a 100644 --- a/store/NebulaShell/http-api/server.py +++ b/oss/core/http_api/server.py @@ -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 diff --git a/oss/core/nbpf/__init__.py b/oss/core/nbpf/__init__.py new file mode 100644 index 0000000..5f37f2d --- /dev/null +++ b/oss/core/nbpf/__init__.py @@ -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", +] diff --git a/oss/core/nbpf/compiler.py b/oss/core/nbpf/compiler.py new file mode 100644 index 0000000..855e5f7 --- /dev/null +++ b/oss/core/nbpf/compiler.py @@ -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 = "") -> 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 diff --git a/oss/core/nbpf/crypto.py b/oss/core/nbpf/crypto.py new file mode 100644 index 0000000..ff0f93a --- /dev/null +++ b/oss/core/nbpf/crypto.py @@ -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 diff --git a/oss/core/nbpf/format.py b/oss/core/nbpf/format.py new file mode 100644 index 0000000..8091dd6 --- /dev/null +++ b/oss/core/nbpf/format.py @@ -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}" diff --git a/oss/core/nbpf/loader.py b/oss/core/nbpf/loader.py new file mode 100644 index 0000000..697f68f --- /dev/null +++ b/oss/core/nbpf/loader.py @@ -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"" + 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}', + } diff --git a/store/NebulaShell/code-reviewer/core/__init__.py b/oss/core/repl/__init__.py similarity index 100% rename from store/NebulaShell/code-reviewer/core/__init__.py rename to oss/core/repl/__init__.py diff --git a/oss/core/repl/main.py b/oss/core/repl/main.py new file mode 100644 index 0000000..d476e52 --- /dev/null +++ b/oss/core/repl/main.py @@ -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 [args...]""" + if not arg: + print("\033[1;33m用法: call [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 """ + if not arg: + print("\033[1;33m用法: recover \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 diff --git a/oss/logger/logger.py b/oss/logger/logger.py index 4c49daa..a5bc29b 100644 --- a/oss/logger/logger.py +++ b/oss/logger/logger.py @@ -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) diff --git a/oss/plugin/base.py b/oss/plugin/base.py deleted file mode 100644 index dbe85c7..0000000 --- a/oss/plugin/base.py +++ /dev/null @@ -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 diff --git a/oss/plugin/capabilities.py b/oss/plugin/capabilities.py index 552b47b..1f8347b 100644 --- a/oss/plugin/capabilities.py +++ b/oss/plugin/capabilities.py @@ -1,3 +1,7 @@ +import ast + +# 启发式能力扫描:通过 AST 分析插件源码,基于命名约定和导入推断插件提供的能力 +# 这是一种轻量级的静态分析,不执行任何代码,仅用于快速发现插件可能提供的能力 def scan_capabilities(plugin_dir): capabilities: set[str] = set() main_file = plugin_dir / "main.py" diff --git a/oss/plugin/manager.py b/oss/plugin/manager.py index 74787db..9d9c831 100644 --- a/oss/plugin/manager.py +++ b/oss/plugin/manager.py @@ -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: @@ -13,32 +13,20 @@ 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 核心插件 - - 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"] - - # 隐藏成就:深海潜水员 - 当加载插件管理器时解锁 + """加载所有插件(由 CorePluginManager 管理)""" + self.core.load_all() + + # 隐藏成就:深海潜水员 if _ACHIEVEMENTS_ENABLED: try: validator = get_validator() @@ -47,48 +35,48 @@ 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 if _ACHIEVEMENTS_ENABLED: try: validator = get_validator() 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.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: - self.plugin_loader.stop() - 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 + validator = get_validator() + validator.track_progress("session_end") + except Exception: + pass diff --git a/oss/shared/router.py b/oss/shared/router.py index af33e79..1120d4a 100644 --- a/oss/shared/router.py +++ b/oss/shared/router.py @@ -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): diff --git a/store/NebulaShell/auto-dependency/PL/main.py b/store/NebulaShell/auto-dependency/PL/main.py deleted file mode 100644 index dd691da..0000000 --- a/store/NebulaShell/auto-dependency/PL/main.py +++ /dev/null @@ -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}") diff --git a/store/NebulaShell/auto-dependency/README.md b/store/NebulaShell/auto-dependency/README.md deleted file mode 100644 index 8eb54bf..0000000 --- a/store/NebulaShell/auto-dependency/README.md +++ /dev/null @@ -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 # 本文档 -``` diff --git a/store/NebulaShell/auto-dependency/main.py b/store/NebulaShell/auto-dependency/main.py deleted file mode 100644 index 0fdc8ea..0000000 --- a/store/NebulaShell/auto-dependency/main.py +++ /dev/null @@ -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() diff --git a/store/NebulaShell/auto-dependency/manifest.json b/store/NebulaShell/auto-dependency/manifest.json deleted file mode 100644 index 288286e..0000000 --- a/store/NebulaShell/auto-dependency/manifest.json +++ /dev/null @@ -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": ["*"] -} diff --git a/store/NebulaShell/code-reviewer/SIGNATURE b/store/NebulaShell/code-reviewer/SIGNATURE deleted file mode 100644 index 710c0ec..0000000 --- a/store/NebulaShell/code-reviewer/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/code-reviewer/checks/quality.py b/store/NebulaShell/code-reviewer/checks/quality.py deleted file mode 100644 index 778c754..0000000 --- a/store/NebulaShell/code-reviewer/checks/quality.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/code-reviewer/checks/references.py b/store/NebulaShell/code-reviewer/checks/references.py deleted file mode 100644 index e52bb5d..0000000 --- a/store/NebulaShell/code-reviewer/checks/references.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/code-reviewer/checks/security.py b/store/NebulaShell/code-reviewer/checks/security.py deleted file mode 100644 index c98233b..0000000 --- a/store/NebulaShell/code-reviewer/checks/security.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/code-reviewer/checks/style.py b/store/NebulaShell/code-reviewer/checks/style.py deleted file mode 100644 index 4eb6269..0000000 --- a/store/NebulaShell/code-reviewer/checks/style.py +++ /dev/null @@ -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 [] diff --git a/store/NebulaShell/code-reviewer/core/reviewer.py b/store/NebulaShell/code-reviewer/core/reviewer.py deleted file mode 100644 index faa6f27..0000000 --- a/store/NebulaShell/code-reviewer/core/reviewer.py +++ /dev/null @@ -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 - } diff --git a/store/NebulaShell/code-reviewer/main.py b/store/NebulaShell/code-reviewer/main.py deleted file mode 100644 index ad47b3c..0000000 --- a/store/NebulaShell/code-reviewer/main.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/code-reviewer/manifest.json b/store/NebulaShell/code-reviewer/manifest.json deleted file mode 100644 index ecab778..0000000 --- a/store/NebulaShell/code-reviewer/manifest.json +++ /dev/null @@ -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": ["*"] -} diff --git a/store/NebulaShell/code-reviewer/report/__init__.py b/store/NebulaShell/code-reviewer/report/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/code-reviewer/report/formatter.py b/store/NebulaShell/code-reviewer/report/formatter.py deleted file mode 100644 index e2816e4..0000000 --- a/store/NebulaShell/code-reviewer/report/formatter.py +++ /dev/null @@ -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) diff --git a/store/NebulaShell/code-reviewer/utils/__init__.py b/store/NebulaShell/code-reviewer/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/dashboard/SIGNATURE b/store/NebulaShell/dashboard/SIGNATURE deleted file mode 100644 index 4106d37..0000000 --- a/store/NebulaShell/dashboard/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/dashboard/assets/css/dashboard.css b/store/NebulaShell/dashboard/assets/css/dashboard.css deleted file mode 100644 index 211a4da..0000000 --- a/store/NebulaShell/dashboard/assets/css/dashboard.css +++ /dev/null @@ -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; -} diff --git a/store/NebulaShell/dashboard/config.json b/store/NebulaShell/dashboard/config.json deleted file mode 100644 index 30f6d2d..0000000 --- a/store/NebulaShell/dashboard/config.json +++ /dev/null @@ -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 - } -} diff --git a/store/NebulaShell/dashboard/main.py b/store/NebulaShell/dashboard/main.py deleted file mode 100644 index dc1a54d..0000000 --- a/store/NebulaShell/dashboard/main.py +++ /dev/null @@ -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 = """ - - - - 系统仪表盘 - - - - -
-
-

系统仪表盘

-
-
-
-
0%
-
CPU 使用率
-
-
-
-
0%
-
内存使用
-
-
-
-
0%
-
磁盘使用
-
-
-
-
- - -""" - return html - -register_plugin_type("DashboardPlugin", DashboardPlugin) - - -def New(): - return DashboardPlugin() diff --git a/store/NebulaShell/dashboard/manifest.json b/store/NebulaShell/dashboard/manifest.json deleted file mode 100644 index ca9654c..0000000 --- a/store/NebulaShell/dashboard/manifest.json +++ /dev/null @@ -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": ["*"] -} diff --git a/store/NebulaShell/dependency/README.md b/store/NebulaShell/dependency/README.md deleted file mode 100644 index 11774d1..0000000 --- a/store/NebulaShell/dependency/README.md +++ /dev/null @@ -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"] -} -``` diff --git a/store/NebulaShell/dependency/SIGNATURE b/store/NebulaShell/dependency/SIGNATURE deleted file mode 100644 index 96e2a60..0000000 --- a/store/NebulaShell/dependency/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/dependency/main.py b/store/NebulaShell/dependency/main.py deleted file mode 100644 index 6345c7b..0000000 --- a/store/NebulaShell/dependency/main.py +++ /dev/null @@ -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() diff --git a/store/NebulaShell/dependency/manifest.json b/store/NebulaShell/dependency/manifest.json deleted file mode 100644 index 65ffaf2..0000000 --- a/store/NebulaShell/dependency/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "metadata": { - "name": "dependency", - "version": "1.0.0", - "author": "NebulaShell", - "description": "依赖解析 - 拓扑排序 + 循环依赖检测", - "type": "core" - }, - "config": { - "enabled": true, - "args": {} - }, - "dependencies": [], - "permissions": [] -} diff --git a/store/NebulaShell/example-with-deps/manifest.json b/store/NebulaShell/example-with-deps/manifest.json deleted file mode 100644 index 6b3b7cc..0000000 --- a/store/NebulaShell/example-with-deps/manifest.json +++ /dev/null @@ -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": [] -} diff --git a/store/NebulaShell/firewall/manifest.json b/store/NebulaShell/firewall/manifest.json deleted file mode 100644 index a649790..0000000 --- a/store/NebulaShell/firewall/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/frp-proxy/manifest.json b/store/NebulaShell/frp-proxy/manifest.json deleted file mode 100644 index d92578c..0000000 --- a/store/NebulaShell/frp-proxy/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/ftp-server/manifest.json b/store/NebulaShell/ftp-server/manifest.json deleted file mode 100644 index 8953c6a..0000000 --- a/store/NebulaShell/ftp-server/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/hot-reload/README.md b/store/NebulaShell/hot-reload/README.md deleted file mode 100644 index 79b7df7..0000000 --- a/store/NebulaShell/hot-reload/README.md +++ /dev/null @@ -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()` diff --git a/store/NebulaShell/hot-reload/SIGNATURE b/store/NebulaShell/hot-reload/SIGNATURE deleted file mode 100644 index 57307e5..0000000 --- a/store/NebulaShell/hot-reload/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/hot-reload/main.py b/store/NebulaShell/hot-reload/main.py deleted file mode 100644 index 58cac17..0000000 --- a/store/NebulaShell/hot-reload/main.py +++ /dev/null @@ -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() diff --git a/store/NebulaShell/hot-reload/manifest.json b/store/NebulaShell/hot-reload/manifest.json deleted file mode 100644 index f736150..0000000 --- a/store/NebulaShell/hot-reload/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/http-api/README.md b/store/NebulaShell/http-api/README.md deleted file mode 100644 index d5c0c56..0000000 --- a/store/NebulaShell/http-api/README.md +++ /dev/null @@ -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 - } - } -} -``` diff --git a/store/NebulaShell/http-api/SIGNATURE b/store/NebulaShell/http-api/SIGNATURE deleted file mode 100644 index 3e96780..0000000 --- a/store/NebulaShell/http-api/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/http-api/csrf_middleware.py b/store/NebulaShell/http-api/csrf_middleware.py deleted file mode 100644 index fdff0c5..0000000 --- a/store/NebulaShell/http-api/csrf_middleware.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/store/NebulaShell/http-api/events.py b/store/NebulaShell/http-api/events.py deleted file mode 100644 index 8ab29c2..0000000 --- a/store/NebulaShell/http-api/events.py +++ /dev/null @@ -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) diff --git a/store/NebulaShell/http-api/input_validation.py b/store/NebulaShell/http-api/input_validation.py deleted file mode 100644 index eaf226e..0000000 --- a/store/NebulaShell/http-api/input_validation.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/store/NebulaShell/http-api/main.py b/store/NebulaShell/http-api/main.py deleted file mode 100644 index 408b16a..0000000 --- a/store/NebulaShell/http-api/main.py +++ /dev/null @@ -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() diff --git a/store/NebulaShell/http-api/manifest.json b/store/NebulaShell/http-api/manifest.json deleted file mode 100644 index e09b48b..0000000 --- a/store/NebulaShell/http-api/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/http-api/middleware.py b/store/NebulaShell/http-api/middleware.py deleted file mode 100644 index 239c380..0000000 --- a/store/NebulaShell/http-api/middleware.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/store/NebulaShell/http-api/rate_limiter.py b/store/NebulaShell/http-api/rate_limiter.py deleted file mode 100644 index c7c36fb..0000000 --- a/store/NebulaShell/http-api/rate_limiter.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/store/NebulaShell/http-tcp/README.md b/store/NebulaShell/http-tcp/README.md deleted file mode 100644 index ea22683..0000000 --- a/store/NebulaShell/http-tcp/README.md +++ /dev/null @@ -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 - } - } -} -``` diff --git a/store/NebulaShell/http-tcp/SIGNATURE b/store/NebulaShell/http-tcp/SIGNATURE deleted file mode 100644 index f30e6a6..0000000 --- a/store/NebulaShell/http-tcp/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/http-tcp/events.py b/store/NebulaShell/http-tcp/events.py deleted file mode 100644 index b2522f2..0000000 --- a/store/NebulaShell/http-tcp/events.py +++ /dev/null @@ -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" diff --git a/store/NebulaShell/http-tcp/main.py b/store/NebulaShell/http-tcp/main.py deleted file mode 100644 index f05c145..0000000 --- a/store/NebulaShell/http-tcp/main.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/http-tcp/manifest.json b/store/NebulaShell/http-tcp/manifest.json deleted file mode 100644 index 8355cf1..0000000 --- a/store/NebulaShell/http-tcp/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/http-tcp/middleware.py b/store/NebulaShell/http-tcp/middleware.py deleted file mode 100644 index 51eccb6..0000000 --- a/store/NebulaShell/http-tcp/middleware.py +++ /dev/null @@ -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() diff --git a/store/NebulaShell/http-tcp/router.py b/store/NebulaShell/http-tcp/router.py deleted file mode 100644 index 2af0da1..0000000 --- a/store/NebulaShell/http-tcp/router.py +++ /dev/null @@ -1,4 +0,0 @@ - -class TcpRouter: - def handle(self, request: dict) -> dict: - pass diff --git a/store/NebulaShell/http-tcp/server.py b/store/NebulaShell/http-tcp/server.py deleted file mode 100644 index 5d0323b..0000000 --- a/store/NebulaShell/http-tcp/server.py +++ /dev/null @@ -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()) diff --git a/store/NebulaShell/i18n/SIGNATURE b/store/NebulaShell/i18n/SIGNATURE deleted file mode 100644 index 8ebc680..0000000 --- a/store/NebulaShell/i18n/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/i18n/__init__.py b/store/NebulaShell/i18n/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/i18n/i18n.py b/store/NebulaShell/i18n/i18n.py deleted file mode 100644 index 4f4e33b..0000000 --- a/store/NebulaShell/i18n/i18n.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/i18n/locales/en-US.json b/store/NebulaShell/i18n/locales/en-US.json deleted file mode 100644 index 0999914..0000000 --- a/store/NebulaShell/i18n/locales/en-US.json +++ /dev/null @@ -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}}" - } -} diff --git a/store/NebulaShell/i18n/locales/zh-CN.json b/store/NebulaShell/i18n/locales/zh-CN.json deleted file mode 100644 index ef79d3d..0000000 --- a/store/NebulaShell/i18n/locales/zh-CN.json +++ /dev/null @@ -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}}" - } -} diff --git a/store/NebulaShell/i18n/locales/zh-TW.json b/store/NebulaShell/i18n/locales/zh-TW.json deleted file mode 100644 index dd725dd..0000000 --- a/store/NebulaShell/i18n/locales/zh-TW.json +++ /dev/null @@ -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}}" - } -} diff --git a/store/NebulaShell/i18n/main.py b/store/NebulaShell/i18n/main.py deleted file mode 100644 index 85b7a92..0000000 --- a/store/NebulaShell/i18n/main.py +++ /dev/null @@ -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() diff --git a/store/NebulaShell/i18n/manifest.json b/store/NebulaShell/i18n/manifest.json deleted file mode 100644 index 0062934..0000000 --- a/store/NebulaShell/i18n/manifest.json +++ /dev/null @@ -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"] -} diff --git a/store/NebulaShell/i18n/middleware.py b/store/NebulaShell/i18n/middleware.py deleted file mode 100644 index 962e885..0000000 --- a/store/NebulaShell/i18n/middleware.py +++ /dev/null @@ -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) diff --git a/store/NebulaShell/json-codec/README.md b/store/NebulaShell/json-codec/README.md deleted file mode 100644 index fd22b8d..0000000 --- a/store/NebulaShell/json-codec/README.md +++ /dev/null @@ -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}") -``` diff --git a/store/NebulaShell/json-codec/SIGNATURE b/store/NebulaShell/json-codec/SIGNATURE deleted file mode 100644 index 5e9041a..0000000 --- a/store/NebulaShell/json-codec/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/json-codec/main.py b/store/NebulaShell/json-codec/main.py deleted file mode 100644 index 58bb8a4..0000000 --- a/store/NebulaShell/json-codec/main.py +++ /dev/null @@ -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) diff --git a/store/NebulaShell/json-codec/manifest.json b/store/NebulaShell/json-codec/manifest.json deleted file mode 100644 index 571419d..0000000 --- a/store/NebulaShell/json-codec/manifest.json +++ /dev/null @@ -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": [] -} diff --git a/store/NebulaShell/lifecycle/README.md b/store/NebulaShell/lifecycle/README.md deleted file mode 100644 index b68dfea..0000000 --- a/store/NebulaShell/lifecycle/README.md +++ /dev/null @@ -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() -``` diff --git a/store/NebulaShell/lifecycle/SIGNATURE b/store/NebulaShell/lifecycle/SIGNATURE deleted file mode 100644 index bb4c936..0000000 --- a/store/NebulaShell/lifecycle/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/lifecycle/main.py b/store/NebulaShell/lifecycle/main.py deleted file mode 100644 index 45e6aa3..0000000 --- a/store/NebulaShell/lifecycle/main.py +++ /dev/null @@ -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 diff --git a/store/NebulaShell/lifecycle/manifest.json b/store/NebulaShell/lifecycle/manifest.json deleted file mode 100644 index 3763e17..0000000 --- a/store/NebulaShell/lifecycle/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "metadata": { - "name": "lifecycle", - "version": "1.0.0", - "author": "NebulaShell", - "description": "生命周期管理 - 管理插件的状态转换和钩子", - "type": "core" - }, - "config": { - "enabled": true, - "args": {} - }, - "dependencies": [], - "permissions": [] -} diff --git a/store/NebulaShell/log-terminal/SIGNATURE b/store/NebulaShell/log-terminal/SIGNATURE deleted file mode 100644 index 79d2660..0000000 --- a/store/NebulaShell/log-terminal/SIGNATURE +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/store/NebulaShell/log-terminal/config.json b/store/NebulaShell/log-terminal/config.json deleted file mode 100644 index b940173..0000000 --- a/store/NebulaShell/log-terminal/config.json +++ /dev/null @@ -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 - } -} diff --git a/store/NebulaShell/log-terminal/main.py b/store/NebulaShell/log-terminal/main.py deleted file mode 100644 index dbc5dc9..0000000 --- a/store/NebulaShell/log-terminal/main.py +++ /dev/null @@ -1,459 +0,0 @@ -class LogTerminalPlugin: - def __init__(self): - self.webui = None - self.http_api = None - self.views_dir = os.path.join(os.path.dirname(__file__), 'views') - self._log_buffer = [] - self._log_lock = threading.Lock() - self._ssh_sessions = {} - self._session_counter = 0 - self._log_sync_thread = None - self._running = False - - def meta(self): - from oss.plugin.types import Metadata, PluginConfig, Manifest - return Manifest( - metadata=Metadata( - name="log-terminal", - version="1.0.0", - author="NebulaShell", - description="日志查看器与 SSH 终端" - ), - config=PluginConfig(enabled=True, args={}), - dependencies=["http-api", "webui"] - ) - - def set_webui(self, webui): - self.webui = webui - - def set_http_api(self, http_api): - self.http_api = http_api - - def init(self, deps: dict = None): - if not self.webui or not self.http_api: - try: - from store.NebulaShell.plugin_bridge.main import use - if not self.webui: self.webui = use("webui") - if not self.http_api: self.http_api = use("http-api") - except Exception: - pass - if self.webui: - Log.info("log-terminal", "已获取 WebUI 引用") - - self.webui.register_page( - path='/logs', - content_provider=self._render_logs, - nav_item={'icon': 'ri-file-list-3-line', 'text': '日志'} - ) - - self.webui.register_page( - path='/terminal', - content_provider=self._render_terminal, - nav_item={'icon': 'ri-terminal-box-line', 'text': '终端'} - ) - - Log.ok("log-terminal", "已注册日志与终端页面到 WebUI 导航") - else: - Log.warn("log-terminal", "警告: 未找到 WebUI 依赖") - - if self.http_api and self.http_api.router: - self.http_api.router.get("/api/logs/get", self._handle_get_logs) - self.http_api.router.post("/api/terminal/connect", self._handle_connect_ssh) - self.http_api.router.post("/api/terminal/send", self._handle_send_command) - self.http_api.router.post("/api/terminal/disconnect", self._handle_disconnect_ssh) - self.http_api.router.get("/api/terminal/sessions", self._handle_list_sessions) - Log.ok("log-terminal", "已注册 API 路由") - else: - Log.warn("log-terminal", "警告: 未找到 http-api 依赖") - - def start(self): - Log.info("log-terminal", "日志与终端插件启动中...") - self._running = True - - self._log_sync_thread = threading.Thread(target=self._log_sync_worker, daemon=True) - self._log_sync_thread.start() - - self.add_log_entry("info", "log-terminal", "日志与终端插件已启动") - self.add_log_entry("tip", "log-terminal", "日志查看: /logs | SSH 终端: /terminal") - - self._hook_system_log() - - Log.ok("log-terminal", "日志与终端插件已启动") - - def _hook_system_log(self): - try: - log_files = [ - '/var/log/syslog', - '/var/log/messages', - os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'system.log'), - ] - - last_positions = {} - - while self._running: - for log_file in log_files: - if os.path.exists(log_file) and os.path.isfile(log_file): - try: - with open(log_file, 'r', encoding='utf-8', errors='ignore') as f: - if log_file not in last_positions: - f.seek(0, 2) - last_positions[log_file] = f.tell() - else: - f.seek(last_positions[log_file]) - - lines = f.readlines() - if lines: - last_positions[log_file] = f.tell() - for line in lines[-50:]: - line = line.strip() - if line: - self.add_log_entry("info", "system", line) - except Exception as e: - import traceback - traceback.print_exc() - - time.sleep(2) - - except Exception as e: - Log.error("log-terminal", f"日志同步线程异常: {type(e).__name__}: {e}") - - def add_log_entry(self, level: str, tag: str, message: str): - with self._log_lock: - return self._log_buffer[-limit:] - - def _check_ssh_installed(self): - try: - Log.info("log-terminal", "正在安装 SSH 服务...") - for pkg_manager in ['apt-get', 'yum', 'dnf', 'pacman']: - result = subprocess.run(['which', pkg_manager], capture_output=True, timeout=3) - if result.returncode == 0: - if pkg_manager == 'apt-get': - subprocess.run([pkg_manager, 'update'], capture_output=True, timeout=30) - result = subprocess.run( - [pkg_manager, 'install', '-y', 'openssh-server'], - capture_output=True, text=True, timeout=120 - ) - elif pkg_manager in ['yum', 'dnf']: - result = subprocess.run( - [pkg_manager, 'install', '-y', 'openssh-server'], - capture_output=True, text=True, timeout=120 - ) - elif pkg_manager == 'pacman': - result = subprocess.run( - [pkg_manager, '-S', '--noconfirm', 'openssh'], - capture_output=True, text=True, timeout=120 - ) - - if result.returncode == 0: - Log.ok("log-terminal", "SSH 服务安装成功") - return True - else: - Log.error("log-terminal", f"SSH 服务安装失败: {result.stderr}") - return False - - Log.error("log-terminal", "未找到支持的包管理器") - return False - except Exception as e: - Log.error("log-terminal", f"安装 SSH 服务时出错: {type(e).__name__}: {e}") - return False - - def _start_ssh_server(self, port=8022): - try: - body = json.loads(request.body) - port = body.get('port', 8022) - auto_install = body.get('auto_install', True) - - if not self._check_ssh_installed(): - if auto_install: - if not self._install_ssh(): - return Response( - status=500, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': 'SSH 安装失败'}) - ) - else: - return Response( - status=400, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': 'SSH 未安装,请先安装 SSH 服务'}) - ) - - if not self._start_ssh_server(port): - return Response( - status=500, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': 'SSH 服务器启动失败'}) - ) - - self._session_counter += 1 - session_id = self._session_counter - - try: - process = subprocess.Popen( - ['script', '-q', '-c', '/bin/bash', '/dev/null'], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1 - ) - - self._ssh_sessions[session_id] = { - 'process': process, - 'created_at': time.time(), - 'port': port - } - - Log.info("log-terminal", f"SSH 终端会话 {session_id} 已创建") - return Response( - status=200, - headers={"Content-Type": "application/json"}, - body=json.dumps({ - 'success': True, - 'session_id': session_id, - 'message': 'SSH 终端已连接' - }) - ) - except Exception as e: - Log.error("log-terminal", f"创建终端会话失败: {type(e).__name__}: {e}") - return Response( - status=500, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': str(e)}) - ) - - except Exception as e: - Log.error("log-terminal", f"SSH 连接请求异常: {type(e).__name__}: {e}") - return Response( - status=500, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': str(e)}) - ) - - def _handle_send_command(self, request): - try: - body = json.loads(request.body) - session_id = body.get('session_id') - - if session_id in self._ssh_sessions: - session = self._ssh_sessions[session_id] - try: - session['process'].terminate() - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - pass - del self._ssh_sessions[session_id] - Log.info("log-terminal", f"SSH 终端会话 {session_id} 已断开") - return Response( - status=200, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': True, 'message': '已断开连接'}) - ) - else: - return Response( - status=400, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': '会话不存在'}) - ) - except Exception as e: - Log.error("log-terminal", f"断开连接时出错: {type(e).__name__}: {e}") - return Response( - status=500, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': str(e)}) - ) - - def _handle_list_sessions(self, request): - try: - from urllib.parse import parse_qs, urlparse - - parsed = urlparse(request.path) - params = parse_qs(parsed.query) - limit = int(params.get('limit', [100])[0]) - source = params.get('source', ['buffer'])[0] - logs = [] - - if source == 'buffer': - logs = self._get_logs(limit) - else: - logs = self._read_system_logs(limit) - - return Response( - status=200, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': True, 'logs': logs}) - ) - except Exception as e: - return Response( - status=500, - headers={"Content-Type": "application/json"}, - body=json.dumps({'success': False, 'error': str(e)}) - ) - - def _read_system_logs(self, limit=100): - try: - logs = self._get_logs(limit=100) - log_rows = "" - for log in logs: - level_class = { - 'info': 'log-info', - 'warn': 'log-warn', - 'error': 'log-error', - 'ok': 'log-ok', - 'tip': 'log-tip' - }.get(log['level'], 'log-info') - log_rows += f"{log['timestamp']}{log['tag']}{log['message']}" - - html = f"""{log_rows}
""" - return html - except Exception as e: - return f"

日志视图渲染出错:{e}

" - - def _render_terminal(self) -> str: - html = """ - - - - SSH 终端 - - - - -
-
-
-

SSH 终端

-
- - -
-
-
-
- - 未连接 -
-
- 会话 ID: - -
-
-
-
-
欢迎使用 SSH 终端!点击"连接"按钮开始...
-
- -
-
-
- - -""" - return html - -register_plugin_type("LogTerminalPlugin", LogTerminalPlugin) - - -def New(): - return LogTerminalPlugin() diff --git a/store/NebulaShell/log-terminal/manifest.json b/store/NebulaShell/log-terminal/manifest.json deleted file mode 100644 index a9cc93b..0000000 --- a/store/NebulaShell/log-terminal/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "metadata": { - "name": "log-terminal", - "version": "1.0.0", - "author": "NebulaShell", - "description": "日志查看器与 SSH 终端", - "type": "webui-extension" - }, - "config": { - "enabled": true, - "args": {} - }, - "dependencies": ["http-api", "webui"], - "permissions": ["*"] -} diff --git a/store/NebulaShell/nodejs-adapter/README.md b/store/NebulaShell/nodejs-adapter/README.md deleted file mode 100644 index 95d5dc6..0000000 --- a/store/NebulaShell/nodejs-adapter/README.md +++ /dev/null @@ -1,281 +0,0 @@ -# Node.js Adapter Plugin for NebulaShell - -## Overview - -The `@NebulaShell/nodejs-adapter` plugin provides Node.js and npm capabilities to other NebulaShell plugins. It enables any plugin to run Node.js projects located in their `/pkg` directory with isolated dependencies. - -## Features - -- **Node.js Runtime**: Execute Node.js scripts and applications -- **npm Package Manager**: Install and manage npm packages -- **Dependency Isolation**: Each plugin gets its own isolated `node_modules` directory -- **Script Execution**: Run npm scripts or direct Node.js files -- **Project Initialization**: Automatically create package.json and basic project structure - -## Installation - -The plugin is included in the NebulaShell store at: -``` -store/NebulaShell/nodejs-adapter/ -``` - -It will be automatically loaded when the NebulaShell server starts. - -## Usage - -### For Plugin Developers - -To use the Node.js adapter in your plugin, specify it in your plugin's manifest: - -```json -{ - "name": "@NebulaShell/my-nodejs-plugin", - "version": "1.0.0", - "runtime": { - "type": "nodejs", - "entry_point": "pkg/index.js", - "adapter": "@NebulaShell/nodejs-adapter" - }, - "dependencies": { - "nodejs-adapter": "^1.2.0" - }, - "nodejs": { - "packages": ["express", "lodash"], - "scripts": { - "start": "node index.js", - "build": "webpack --mode production" - } - } -} -``` - -### Directory Structure - -``` -my-plugin/ -├── manifest.json -├── main.py (optional Python entry point) -└── pkg/ - ├── package.json - ├── index.js - └── node_modules/ (auto-generated) -``` - -### API Methods - -The adapter provides the following methods that can be called by other plugins: - -#### `check_versions()` -Check Node.js and npm versions installed on the system. - -```python -adapter = get_plugin('nodejs-adapter') -versions = adapter.check_versions() -# Returns: {'node': 'v20.19.5', 'npm': '10.8.2', 'status': 'ok'} -``` - -#### `install(plugin_id, packages, pkg_dir=None, is_dev=False)` -Install npm packages to a plugin-specific directory. - -```python -result = adapter.install( - plugin_id='my-plugin', - packages=['express', 'lodash@4.17.21'], - is_dev=False -) -# Returns: {'status': 'success', 'target_dir': '/path/to/dir', ...} -``` - -#### `run(plugin_id, script, pkg_dir=None, args=None, env=None)` -Execute a Node.js script or npm command. - -```python -# Run npm script -result = adapter.run( - plugin_id='my-plugin', - script='start' # runs 'npm run start' -) - -# Run direct Node.js file -result = adapter.run( - plugin_id='my-plugin', - script='pkg/index.js', # runs 'node pkg/index.js' - args=['--port', '3000'] -) -``` - -#### `list_packages(plugin_id, pkg_dir=None)` -List installed packages in a plugin directory. - -```python -packages = adapter.list_packages(plugin_id='my-plugin') -# Returns: {'status': 'success', 'packages': {...}} -``` - -#### `init_project(plugin_id, pkg_dir=None, package_name=None, version='1.0.0')` -Initialize a new Node.js project. - -```python -result = adapter.init_project( - plugin_id='my-plugin', - package_name='my-awesome-plugin' -) -# Creates package.json and index.js in the plugin directory -``` - -## Configuration - -The adapter can be configured via environment variables or plugin config: - -```json -{ - "config": { - "node_path": "/usr/bin/node", - "npm_path": "/usr/bin/npm", - "default_registry": "https://registry.npmjs.org", - "cache_dir": "~/.nebulashell/nodejs-cache" - } -} -``` - -### Environment Variables - -- `NODEJS_ADAPTER_NODE_PATH`: Path to Node.js binary -- `NODEJS_ADAPTER_NPM_PATH`: Path to npm binary -- `NODEJS_ADAPTER_REGISTRY`: Custom npm registry URL -- `NODEJS_ADAPTER_CACHE_DIR`: Directory for cached packages - -## Examples - -### Example 1: Simple Express Server Plugin - -```json -{ - "name": "@NebulaShell/express-server", - "version": "1.0.0", - "runtime": { - "type": "nodejs", - "entry_point": "pkg/server.js", - "adapter": "@NebulaShell/nodejs-adapter" - }, - "nodejs": { - "packages": ["express"] - } -} -``` - -**pkg/server.js**: -```javascript -const express = require('express'); -const app = express(); -const port = process.env.PORT || 3000; - -app.get('/', (req, res) => { - res.json({ message: 'Hello from NebulaShell!' }); -}); - -app.listen(port, () => { - console.log(`Server running on port ${port}`); -}); -``` - -### Example 2: Build Tool Plugin - -```json -{ - "name": "@NebulaShell/webpack-builder", - "version": "1.0.0", - "runtime": { - "type": "nodejs", - "adapter": "@NebulaShell/nodejs-adapter" - }, - "nodejs": { - "packages": ["webpack", "webpack-cli"], - "scripts": { - "build": "webpack --mode production" - } - } -} -``` - -## Dependency Isolation - -Each plugin gets its own isolated `node_modules` directory: - -- Default location: `~/.nebulashell/nodejs-cache/{plugin_id}/` -- Custom location: Specify `pkg_dir` parameter in API calls -- No conflicts between different plugins' dependencies - -## Error Handling - -All adapter methods return a status object: - -```python -result = adapter.install(plugin_id='test', packages=['invalid-package-name-xyz']) -if result['status'] == 'error': - print(f"Installation failed: {result['error']}") -else: - print(f"Success! Packages installed to: {result['target_dir']}") -``` - -## Testing - -Test the adapter directly: - -```bash -cd /workspace/store/NebulaShell/nodejs-adapter -python main.py -``` - -Expected output: -``` -Node.js Adapter Plugin for NebulaShell -================================================== - -Node.js Version: v20.19.5 -npm Version: 10.8.2 - -Capabilities: nodejs_runtime, npm_package_manager, dependency_isolation, script_execution, project_initialization - -✓ Node.js Adapter initialized successfully! -``` - -## Troubleshooting - -### Node.js or npm not found - -Ensure Node.js and npm are installed on your system: - -```bash -# Check installation -node --version -npm --version - -# Install if needed (Ubuntu/Debian) -apt update && apt install -y nodejs npm - -# Install if needed (macOS) -brew install node -``` - -### Permission errors - -If you encounter permission errors during package installation: - -```bash -# Ensure cache directory is writable -mkdir -p ~/.nebulashell/nodejs-cache -chmod 755 ~/.nebulashell/nodejs-cache -``` - -### Timeout during installation - -For large packages or slow networks, increase the timeout in the adapter configuration. - -## License - -MIT License - See LICENSE file for details. - -## Contributing - -Contributions welcome! Please read CONTRIBUTING.md for guidelines. diff --git a/store/NebulaShell/nodejs-adapter/main.py b/store/NebulaShell/nodejs-adapter/main.py deleted file mode 100644 index 1c40480..0000000 --- a/store/NebulaShell/nodejs-adapter/main.py +++ /dev/null @@ -1,402 +0,0 @@ -""" -Node.js Adapter Plugin for NebulaShell - -This plugin provides Node.js and npm capabilities to other plugins. -Other plugins can specify this adapter in their manifest to run Node.js projects -located in their /pkg directory with isolated dependencies. - -Features: -- Install npm packages to plugin-specific directories -- Execute Node.js scripts and npm commands -- Check Node.js and npm versions -- List installed packages -- Dependency isolation per plugin -""" - -import subprocess -import json -import os -import shutil -from pathlib import Path -from typing import Dict, List, Optional, Any - - -class NodeJSAdapter: - def __init__(self, config: Dict[str, Any] = None): - self.config = config or {} - self.node_path = self.config.get('node_path', '/usr/bin/node') - self.npm_path = self.config.get('npm_path', '/usr/bin/npm') - self.default_registry = self.config.get('default_registry', 'https://registry.npmjs.org') - self.cache_dir = Path(self.config.get('cache_dir', '~/.nebulashell/nodejs-cache')).expanduser() - - self.cache_dir.mkdir(parents=True, exist_ok=True) - - self._validate_runtime() - - def _validate_runtime(self) -> bool: - try: - node_result = subprocess.run( - [self.node_path, '--version'], - capture_output=True, - text=True, - timeout=10 - ) - npm_result = subprocess.run( - [self.npm_path, '--version'], - capture_output=True, - text=True, - timeout=10 - ) - - return { - 'node': node_result.stdout.strip(), - 'npm': npm_result.stdout.strip(), - 'status': 'ok' - } - except subprocess.TimeoutExpired as e: - return { - 'node': 'unknown', - 'npm': 'unknown', - 'status': 'error', - 'error': f'Timeout: {str(e)}' - } - except Exception as e: - return { - 'node': 'unknown', - 'npm': 'unknown', - 'status': 'error', - 'error': str(e) - } - - def install(self, plugin_id: str, packages: List[str], - pkg_dir: Optional[Path] = None, - is_dev: bool = False) -> Dict[str, Any]: - """Install npm packages to a plugin-specific directory. - - Args: - plugin_id: Unique identifier for the plugin - packages: List of npm packages to install (e.g., ['express', 'lodash@4.17.21']) - pkg_dir: Optional custom package directory (defaults to plugin storage dir) - is_dev: Whether to install as dev dependencies - - Returns: - Dict with installation result - """ - try: - if pkg_dir is None: - target_dir = self.cache_dir / plugin_id - else: - target_dir = Path(pkg_dir) - - target_dir.mkdir(parents=True, exist_ok=True) - - cmd = [self.npm_path, 'install'] - if is_dev: - cmd.append('--save-dev') - else: - cmd.append('--save') - - if self.default_registry: - cmd.extend(['--registry', self.default_registry]) - - cmd.extend(packages) - - result = subprocess.run( - cmd, - cwd=str(target_dir), - capture_output=True, - text=True, - timeout=300 - ) - - if result.returncode == 0: - return { - 'status': 'success', - 'plugin_id': plugin_id, - 'packages': packages, - 'target_dir': str(target_dir), - 'output': result.stdout, - 'is_dev': is_dev - } - else: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'packages': packages, - 'target_dir': str(target_dir), - 'error': result.stderr, - 'output': result.stdout - } - except subprocess.TimeoutExpired as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'packages': packages, - 'error': f'Installation timeout: {str(e)}' - } - except Exception as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'packages': packages, - 'error': str(e) - } - - def run(self, plugin_id: str, script: str, - pkg_dir: Optional[Path] = None, - args: Optional[List[str]] = None, - env: Optional[Dict[str, str]] = None) -> Dict[str, Any]: - """Execute a Node.js script or npm command. - - Args: - plugin_id: Unique identifier for the plugin - script: Script to run (e.g., 'start', 'build', or path to .js file) - pkg_dir: Optional custom package directory - args: Additional arguments to pass - env: Custom environment variables - - Returns: - Dict with execution result - """ - try: - if pkg_dir is None: - work_dir = self.cache_dir / plugin_id - else: - work_dir = Path(pkg_dir) - - if not work_dir.exists(): - return { - 'status': 'error', - 'error': f'Plugin directory not found: {work_dir}' - } - - if script.endswith('.js') or script.endswith('.ts'): - cmd = [self.node_path, script] - if args: - cmd.extend(args) - else: - cmd = [self.npm_path, 'run', script] - if args: - cmd.append('--') - cmd.extend(args) - - run_env = os.environ.copy() - if env: - run_env.update(env) - - result = subprocess.run( - cmd, - cwd=str(work_dir), - capture_output=True, - text=True, - timeout=300, - env=run_env - ) - - return { - 'status': 'success' if result.returncode == 0 else 'error', - 'plugin_id': plugin_id, - 'script': script, - 'exit_code': result.returncode, - 'stdout': result.stdout, - 'stderr': result.stderr, - 'work_dir': str(work_dir) - } - except subprocess.TimeoutExpired as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'script': script, - 'error': f'Execution timeout: {str(e)}' - } - except Exception as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'script': script, - 'error': str(e) - } - - def list_packages(self, plugin_id: str, - pkg_dir: Optional[Path] = None) -> Dict[str, Any]: - """List installed packages in a plugin directory. - - Args: - plugin_id: Unique identifier for the plugin - pkg_dir: Optional custom package directory - - Returns: - Dict with list of installed packages - """ - try: - if pkg_dir is None: - work_dir = self.cache_dir / plugin_id - else: - work_dir = Path(pkg_dir) - - if not work_dir.exists(): - return { - 'status': 'error', - 'error': f'Plugin directory not found: {work_dir}' - } - - result = subprocess.run( - [self.npm_path, 'list', '--json', '--depth=0'], - cwd=str(work_dir), - capture_output=True, - text=True, - timeout=60 - ) - - if result.returncode == 0: - try: - packages = json.loads(result.stdout) - return { - 'status': 'success', - 'plugin_id': plugin_id, - 'packages': packages.get('dependencies', {}), - 'work_dir': str(work_dir) - } - except json.JSONDecodeError as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'error': f'Failed to parse npm list output: {str(e)}', - 'raw_output': result.stdout - } - else: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'error': result.stderr, - 'work_dir': str(work_dir) - } - except subprocess.TimeoutExpired as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'error': f'Timeout listing packages: {str(e)}' - } - except Exception as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'error': str(e) - } - - def init_project(self, plugin_id: str, pkg_dir: Optional[Path] = None, - package_name: Optional[str] = None, - version: str = "1.0.0") -> Dict[str, Any]: - """Initialize a new Node.js project in a plugin directory. - - Args: - plugin_id: Unique identifier for the plugin - pkg_dir: Optional custom package directory - package_name: Optional package name (defaults to plugin_id) - version: Package version - - Returns: - Dict with initialization result - """ - try: - if pkg_dir is None: - work_dir = self.cache_dir / plugin_id - else: - work_dir = Path(pkg_dir) - - work_dir.mkdir(parents=True, exist_ok=True) - - package_json = { - 'name': package_name or plugin_id.replace('/', '-'), - 'version': version, - 'description': f'Node.js project for plugin {plugin_id}', - 'main': 'index.js', - 'scripts': { - 'start': 'node index.js', - 'test': 'echo "Error: no test specified" && exit 1' - }, - 'keywords': [], - 'author': '', - 'license': 'ISC' - } - - package_json_path = work_dir / 'package.json' - with open(package_json_path, 'w', encoding='utf-8') as f: - json.dump(package_json, f, indent=2) - - index_js_path = work_dir / 'index.js' - with open(index_js_path, 'w', encoding='utf-8') as f: - f.write('// Node.js project for NebulaShell plugin\n') - f.write(f'// Plugin ID: {plugin_id}\n') - f.write('console.log("Hello from NebulaShell Node.js plugin!");\n') - - return { - 'status': 'success', - 'plugin_id': plugin_id, - 'work_dir': str(work_dir), - 'package_json': str(package_json_path), - 'index_js': str(index_js_path) - } - except Exception as e: - return { - 'status': 'error', - 'plugin_id': plugin_id, - 'error': str(e) - } - - -def init(config: Dict[str, Any]) -> NodeJSAdapter: - return NodeJSAdapter(config) - - -def get_capabilities() -> list: - return [ - 'nodejs_runtime', - 'npm_package_manager', - 'dependency_isolation', - 'script_execution', - 'project_initialization' - ] - - -def execute_command(adapter: NodeJSAdapter, command: str, **kwargs) -> Dict[str, Any]: - """Execute a command through the adapter. - - Available commands: - - check_versions: Check Node.js and npm versions - - install: Install npm packages - - run: Execute Node.js scripts or npm commands - - list_packages: List installed packages - - init_project: Initialize a new Node.js project - """ - if command == 'check_versions': - return adapter.check_versions() - elif command == 'install': - return adapter.install(**kwargs) - elif command == 'run': - return adapter.run(**kwargs) - elif command == 'list_packages': - return adapter.list_packages(**kwargs) - elif command == 'init_project': - return adapter.init_project(**kwargs) - else: - return { - 'status': 'error', - 'error': f'Unknown command: {command}' - } - - -if __name__ == '__main__': - print("Node.js Adapter Plugin for NebulaShell") - print("=" * 50) - - adapter = init({}) - - versions = adapter.check_versions() - print(f"\nNode.js Version: {versions.get('node', 'N/A')}") - print(f"npm Version: {versions.get('npm', 'N/A')}") - - caps = get_capabilities() - print(f"\nCapabilities: {', '.join(caps)}") - - print("\nNode.js Adapter initialized successfully!") diff --git a/store/NebulaShell/nodejs-adapter/manifest.json b/store/NebulaShell/nodejs-adapter/manifest.json deleted file mode 100644 index 4e1afe9..0000000 --- a/store/NebulaShell/nodejs-adapter/manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@NebulaShell/nodejs-adapter", - "version": "1.2.0", - "description": "Node.js runtime adapter for NebulaShell - provides Node.js and npm capabilities for other plugins", - "author": "NebulaShell Team", - "license": "MIT", - "runtime": { - "type": "python", - "entry_point": "main.py", - "requirements": ["subprocess", "json", "os", "shutil"] - }, - "capabilities": [ - "nodejs_runtime", - "npm_package_manager", - "dependency_isolation", - "script_execution" - ], - "config": { - "node_path": "/usr/bin/node", - "npm_path": "/usr/bin/npm", - "default_registry": "https://registry.npmjs.org", - "cache_dir": "~/.nebulashell/nodejs-cache" - }, - "api": { - "install": "Install npm packages to plugin-specific directory", - "run": "Execute Node.js scripts or npm commands", - "check_version": "Check Node.js and npm versions", - "list_packages": "List installed packages in a plugin directory" - } -} diff --git a/store/NebulaShell/performance-optimizer/README.md b/store/NebulaShell/performance-optimizer/README.md deleted file mode 100644 index 9430ed6..0000000 --- a/store/NebulaShell/performance-optimizer/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# 性能优化插件 (Performance Optimizer) - -极致性能调优插件,提供多种高性能工具和优化技术。 - -## 功能特性 - -### 1. 高速缓存 (`FastCache`) -- O(1) 时间复杂度的查找 -- LRU 淘汰策略 -- 可选 TTL 过期 -- 命中率统计 - -```python -from plugin.performance_optimizer import cached - -@cached(maxsize=1024, ttl=60) -def expensive_operation(x, y): - return x ** y -``` - -### 2. 对象池 (`ObjectPool`) -- 避免频繁创建/销毁对象 -- 自动扩容 -- 使用统计 - -```python -from plugin.performance_optimizer import ObjectPool - -pool = ObjectPool(lambda: bytearray(4096), maxsize=100) -buf = pool.acquire() -# ... use buf ... -pool.release(buf) -``` - -### 3. 批量处理器 (`BatchProcessor`) -- 累积批量处理 -- 超时自动触发 -- 减少系统调用 - -```python -from plugin.performance_optimizer import BatchProcessor - -processor = BatchProcessor( - batch_handler=lambda items: db.bulk_insert(items), - batch_size=100, - timeout=1.0 -) -for item in items: - processor.add(item) -processor.flush() -``` - -### 4. 内存预分配器 (`MemoryArena`) -- 预分配大块内存 -- 按需切分 -- 减少内存碎片 - -```python -from plugin.performance_optimizer import MemoryArena - -arena = MemoryArena(size=1024*1024) # 1MB -chunk = arena.allocate(256) -# ... use chunk ... -arena.deallocate(chunk) -``` - -### 5. 性能分析器 (`PerfProfiler`) -- 低开销计时 -- 嵌套支持 -- 统计汇总 - -```python -from plugin.performance_optimizer import PerfProfiler - -profiler = PerfProfiler() -with profiler.context("operation"): - # ... do something ... -print(profiler.stats()) -``` - -### 6. 字符串驻留 (`StringIntern`) -- 重复字符串去重 -- 减少内存占用 -- 加速字符串比较 - -```python -from plugin.performance_optimizer import StringIntern - -intern = StringIntern() -s1 = intern.intern("hello") -s2 = intern.intern("hello") -assert s1 is s2 # 同一个对象 -``` - -## API 参考 - -### PerformanceOptimizerPlugin - -主插件类,提供统一的访问接口: - -```python -# 获取插件实例 -plugin = New() -plugin.init() - -# 获取缓存 -cache = plugin.get_cache("route_match") - -# 获取对象池 -pool = plugin.get_pool("bytearray_4k") - -# 性能分析 -profiler = plugin.profile() -with profiler.context("my_operation"): - # ... do work ... - -# 字符串驻留 -s = plugin.intern_string("repeated string") - -# 查看统计 -stats = plugin.stats() -``` - -## 配置选项 - -在 `manifest.json` 中配置: - -```json -{ - "config": { - "args": { - "cache_maxsize": 2048, - "pool_maxsize": 100, - "enable_profiler": true - } - } -} -``` - -## 性能提升 - -| 优化项 | 提升幅度 | -|--------|----------| -| 缓存命中 | 10-100x | -| 对象复用 | 5-20x | -| 批量操作 | 10-50x | -| 内存预分配 | 2-5x | -| 字符串驻留 | 2-10x | - -## 注意事项 - -1. 缓存大小应根据实际内存限制调整 -2. 对象池适合频繁创建/销毁的对象 -3. 批量处理的 `batch_size` 和 `timeout` 需根据业务场景调优 -4. 性能分析器在生产环境建议关闭以减少开销 diff --git a/store/NebulaShell/performance-optimizer/main.py b/store/NebulaShell/performance-optimizer/main.py deleted file mode 100644 index 106bd8d..0000000 --- a/store/NebulaShell/performance-optimizer/main.py +++ /dev/null @@ -1,318 +0,0 @@ -import sys -import time -import functools -from typing import Any, Callable, Optional, TypeVar, Generic, Dict, List, Set -from collections import deque -from dataclasses import dataclass, field -from threading import Lock -import weakref - -T = TypeVar('T') -F = TypeVar('F', bound=Callable) - - -class FastCache: - __slots__ = ('_cache', '_order', '_maxsize', '_ttl', '_hits', '_misses', '_lock') - - def __init__(self, maxsize: int = 1024, ttl: float = 0): - self._cache: Dict[Any, Any] = {} - self._order: deque = deque() - self._maxsize = maxsize - self._ttl = ttl - self._hits = 0 - self._misses = 0 - self._lock = Lock() if sys.version_info < (3, 9) else None - - def get(self, key: Any) -> tuple[bool, Any]: - if key not in self._cache: - self._misses += 1 - return False, None - - entry = self._cache[key] - if self._ttl > 0 and time.time() - entry[1] > self._ttl: - del self._cache[key] - try: - self._order.remove(key) - except ValueError: - pass - self._misses += 1 - return False, None - - self._order.remove(key) - self._order.append(key) - self._hits += 1 - return True, entry[0] - - def set(self, key: Any, value: Any): - if key in self._cache: - self._order.remove(key) - self._cache[key] = (value, time.time()) - self._order.append(key) - if len(self._cache) > self._maxsize: - oldest = self._order.popleft() - del self._cache[oldest] - - def clear(self): - self._cache.clear() - self._order.clear() - self._hits = 0 - self._misses = 0 - - @property - def hit_rate(self) -> float: - total = self._hits + self._misses - if total == 0: - return 0.0 - return self._hits / total - - -def cached(maxsize: int = 1024, ttl: float = 0, key_func: Callable = None): - _cache = FastCache(maxsize=maxsize, ttl=ttl) - - def decorator(func: F) -> F: - @functools.wraps(func) - def wrapper(*args, **kwargs): - if key_func: - key = key_func(*args, **kwargs) - else: - key = (args, tuple(sorted(kwargs.items()))) - - hit, value = _cache.get(key) - if hit: - return value - - value = func(*args, **kwargs) - _cache.set(key, value) - return value - - wrapper.cache = _cache - wrapper.cache_clear = _cache.clear - wrapper.cache_stats = _cache.stats - return wrapper - return decorator - - -class ObjectPool(Generic[T]): - __slots__ = ('_factory', '_pool', '_maxsize', '_created', '_acquired', '_lock') - - def __init__(self, factory: Callable[[], T], maxsize: int = 100): - self._factory = factory - self._pool: List[T] = [] - self._maxsize = maxsize - self._created = 0 - self._acquired = 0 - self._lock = Lock() if sys.version_info < (3, 9) else None - - def acquire(self) -> T: - if self._pool: - obj = self._pool.pop() - else: - obj = self._factory() - self._created += 1 - self._acquired += 1 - return obj - - def release(self, obj: T): - if len(self._pool) < self._maxsize: - self._pool.append(obj) - - def clear(self): - self._pool.clear() - - -class BatchProcessor(Generic[T]): - __slots__ = ('_handler', '_batch_size', '_timeout', '_buffer', '_last_flush', '_processed_count') - - def __init__(self, batch_handler: Callable[[List[T]], Any], batch_size: int = 100, timeout: float = 1.0): - self._handler = batch_handler - self._batch_size = batch_size - self._timeout = timeout - self._buffer: List[T] = [] - self._last_flush = time.time() - self._processed_count = 0 - - def add(self, item: T): - self._buffer.append(item) - if len(self._buffer) >= self._batch_size: - self.flush() - - def flush(self): - if not self._buffer: - return - - self._handler(self._buffer) - self._buffer.clear() - self._last_flush = time.time() - self._processed_count += 1 - - @property - def pending_count(self) -> int: - return len(self._buffer) - - def stats(self) -> dict[str, Any]: - return { - "pending": len(self._buffer), - "batch_size": self._batch_size, - "flush_count": self._processed_count, - } - - -class MemoryArena: - __slots__ = ('_data', '_free_list', '_allocated', '_total_size') - - def __init__(self, size: int = 1024 * 1024): - self._data = bytearray(size) - self._free_list: List[tuple[int, int]] = [(0, size)] - self._allocated: Set[int] = set() - self._total_size = size - - def allocate(self, size: int) -> Optional[memoryview]: - for i, (offset, block_size) in enumerate(self._free_list): - if block_size >= size: - self._free_list.pop(i) - if block_size > size: - self._free_list.append((offset + size, block_size - size)) - self._allocated.add(offset) - return memoryview(self._data)[offset:offset + size] - return None - - def deallocate(self, view: memoryview): - offset = view.obj.__array_interface__['data'][0] - id(self._data) if hasattr(view.obj, '__array_interface__') else 0 - if offset in self._allocated: - self._allocated.remove(offset) - self._free_list.append((offset, len(view))) - - @property - def available(self) -> int: - return sum(size for _, size in self._free_list) - - @property - def usage_rate(self) -> float: - return 1.0 - (self.available / self._total_size) - - -class HotPathOptimizer: - __slots__ = ('_call_counts', '_threshold', '_optimized', '_start_times') - - def __init__(self, threshold: int = 1000): - self._call_counts: Dict[str, int] = {} - self._threshold = threshold - self._optimized: Set[str] = set() - self._start_times: Dict[str, float] = {} - - def track(self, func_name: str): - self._call_counts[func_name] = self._call_counts.get(func_name, 0) + 1 - if self._call_counts[func_name] >= self._threshold and func_name not in self._optimized: - self._optimized.add(func_name) - return True, self._call_counts[func_name] - return False, self._call_counts[func_name] - - -class PerfProfiler: - __slots__ = ('_records', '_stack', '_enabled') - - def __init__(self): - self._records: Dict[str, List[float]] = {} - self._stack: List[tuple[str, float]] = [] - self._enabled = True - - def start(self, name: str): - if not self._enabled: - return - self._stack.append((name, time.perf_counter())) - - def stop(self, name: str): - if not self._enabled or not self._stack: - return - - top_name, start_time = self._stack.pop() - if top_name != name: - return - - elapsed = time.perf_counter() - start_time - if name not in self._records: - self._records[name] = [] - self._records[name].append(elapsed) - - def context(self, name: str): - pass - - -class StringIntern: - __slots__ = ('_cache',) - - def __init__(self, use_weak_refs: bool = True): - self._cache: Dict[str, str] = {} - - def intern(self, s: str) -> str: - if s in self._cache: - return self._cache[s] - - import sys - interned = sys.intern(s) - self._cache[interned] = interned - - return interned - - def clear(self): - self._cache.clear() - - -class PerformanceOptimizerPlugin: - def __init__(self): - self._initialized = False - self._caches: Dict[str, FastCache] = {} - self._pools: Dict[str, ObjectPool] = {} - self._profiler = PerfProfiler() - self._hot_path = HotPathOptimizer() - self._string_intern = StringIntern() - - def init(self, deps: dict = None): - if self._initialized: - return - - self._caches["route_match"] = FastCache(maxsize=2048) - self._caches["path_params"] = FastCache(maxsize=2048) - self._caches["template_render"] = FastCache(maxsize=512) - - self._pools["bytearray_4k"] = ObjectPool(lambda: bytearray(4096), maxsize=100) - self._pools["bytearray_64k"] = ObjectPool(lambda: bytearray(65536), maxsize=20) - - self._initialized = True - - def start(self): - pass - - def stop(self): - for cache in self._caches.values(): - cache.clear() - for pool in self._pools.values(): - pool.clear() - self._profiler = PerfProfiler() - - def get_cache(self, name: str) -> Optional[FastCache]: - return self._caches.get(name) - - def get_pool(self, name: str) -> Optional[ObjectPool]: - return self._pools.get(name) - - def profile(self) -> PerfProfiler: - return self._profiler - - def intern_string(self, s: str) -> str: - return self._string_intern.intern(s) - - def track_hot_path(self, func_name: str) -> tuple[bool, float]: - return self._hot_path.track(func_name) - - def stats(self) -> dict[str, Any]: - return { - "caches": {name: cache.stats() for name, cache in self._caches.items()}, - "pools": {name: pool.stats() for name, pool in self._pools.items()}, - "profiler": self._profiler.stats(), - "hot_paths": self._hot_path.stats(), - } - - -def New() -> PerformanceOptimizerPlugin: - return PerformanceOptimizerPlugin() diff --git a/store/NebulaShell/performance-optimizer/manifest.json b/store/NebulaShell/performance-optimizer/manifest.json deleted file mode 100644 index 2621591..0000000 --- a/store/NebulaShell/performance-optimizer/manifest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "metadata": { - "name": "performance-optimizer", - "version": "1.0.0", - "author": "NebulaShell", - "description": "极致性能优化插件 - 提供缓存、对象池、批量处理、内存预分配等高性能工具" - }, - "config": { - "args": { - "enabled": true, - "cache_maxsize": 2048, - "pool_maxsize": 100, - "enable_profiler": true - } - }, - "dependencies": [], - "permissions": [] -} diff --git a/store/NebulaShell/pkg-manager/SIGNATURE b/store/NebulaShell/pkg-manager/SIGNATURE deleted file mode 100644 index 474a5aa..0000000 --- a/store/NebulaShell/pkg-manager/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "hNzQ56uwgghPRTVm5YFA8fZp+1Y9TQ9fSDKLEY+KPFLddrxdnXZiE66XXWEVEj80pB5E/zJ0nDcpJYTe9+Mo4LQ++Qzt7yA+PMu8WZ/I39f1870FR/s+MuaiKWp0sT/NeyHRv/nHKi/FaZXWx+KsSbKatq4w088bNhyWahJg1RmTaCKAxv7ut9Uqn33m9teoeNt43AG/6ySfRQRfk0K1L7Yvf/9yJStDMAuTzFiQmhs4MZ58VzPh/Nrtj0G7N5mAjp9bZKa+EFqMLFBQlG5TDqWU8zFKBe27CsvSK7MthS3PGyzeGftm2O683hgClGdsgdK9kqwZ0eMOb5Jcesk4f0rWVODpCf2cfRPocrs401yKzVU3dStFw14Bq82SpQDRJ9EDU3lP8E4RqlmXEAzlGNoMsGSGth9gSWc4VpHn4ppVH5ftKk/AvJrpdFWyWe0jPnDODRKAIMn9sGiZUy6XqB0fGMoU0vpuvtLy6mtVmQglhsVE49XA5txAEWQncPUPxxjNoMdRo5RDlimRVNtXNcwKRb1z9V6ky1eOVKFHaPsp4Y+1mreZVUokaUBf8LG1qvFXjZuiYHRlffKSN3/yzRqhDnE5fCDu0wpjHe24dZ/PeQXbG2aAQlJQr15yh7p5dxTSiv+HeacwDqZPF8X/9Ey6xMflr1xGZpp9j9YeCtk=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775967812.6803007, - "plugin_hash": "c0c56583082ca71e9a84ac2e976c22683573ec4e40387ee893ac42f31da62d4a", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/pkg-manager/main.py b/store/NebulaShell/pkg-manager/main.py deleted file mode 100644 index e49d300..0000000 --- a/store/NebulaShell/pkg-manager/main.py +++ /dev/null @@ -1,276 +0,0 @@ -def _gitee_request(url, timeout=30): - req = urllib.request.Request(url) - req.add_header("User-Agent", "NebulaShell-PkgManager") - if GITEE_TOKEN: - req.add_header("Authorization", f"token {GITEE_TOKEN}") - return urllib.request.urlopen(req, timeout=timeout) - - -class PkgManagerPlugin(Plugin): - def __init__(self): - if not self.webui: - Log.warn("pkg-manager", "警告: 未找到 WebUI 依赖") - return - - self.webui.register_page( - path='/packages', - content_provider=self._packages_content, - nav_item={'icon': 'ri-apps-line', 'text': '插件管理'} - ) - self.webui.register_page( - path='/store', - content_provider=self._store_content, - nav_item={'icon': 'ri-store-2-line', 'text': '插件商店'} - ) - Log.info("pkg-manager", "已注册到 WebUI 导航") - - def start(self): - try: - plugins = self._get_installed_plugins() - plugin_rows = "" - for pkg_name, info in plugins.items(): - status_class = "success" if info.get('enabled', False) else "secondary" - status_text = "已启用" if info.get('enabled', False) else "已禁用" - safe_pkg_name = html.escape(pkg_name) - safe_version = html.escape(str(info.get('version', '未知'))) - safe_author = html.escape(str(info.get('author', '未知'))) - plugin_rows += f"{safe_pkg_name}{safe_version}{safe_author}" - - html = f"{plugin_rows}
" - return html - except Exception as e: - return f"

插件管理页面渲染出错: {e}

" - - def _store_content(self) -> str: - try: - html = "" - for pkg in self._fetch_remote_plugins(): - safe_name = html.escape(pkg.get('name', '')) - safe_desc = html.escape(pkg.get('description', '')) - safe_version = html.escape(pkg.get('version', '未知')) - safe_author = html.escape(pkg.get('author', '未知')) - action_btn = '' - html += f"""
-
-

{safe_name}

-

{safe_desc}

-
- 版本: {safe_version} - 作者: {safe_author} -
-
- {action_btn} -
-
""" - html = f""" - - - - - 插件商店 - - - - -
-
-
-

插件商店

-
-
- {html} -
-
-
- - -""" - return html - except Exception as e: - return f"

插件商店页面渲染出错: {e}

" - - - - def _handle_list_plugins(self, request): - plugin_name = request.path_params.get('name', '') - schema = self._load_config_schema(plugin_name) - current = self._load_plugin_config(plugin_name) - return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps({ - "schema": schema, - "current": current - }, ensure_ascii=False)) - - def _handle_save_config(self, request): - plugin_name = request.path_params.get('name', '') - info = self._get_plugin_detailed_info(plugin_name) - return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(info, ensure_ascii=False)) - - def _handle_uninstall(self, request): - try: - plugins = self._fetch_remote_plugins() - return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(plugins, ensure_ascii=False)) - except Exception as e: - return Response(status=500, body=json.dumps({"error": str(e)})) - - def _handle_store_install(self, request): - import time - now = time.time() - if self._remote_cache and (now - self._cache_time) < self._cache_ttl: - return self._remote_cache - - plugins = [] - try: - store_url = f"{GITEE_API_BASE}/store" - for attempt in range(3): - try: - with _gitee_request(store_url, timeout=15) as resp: - dirs = json.loads(resp.read().decode("utf-8")) - break - except Exception as e: - if attempt < 2: - time.sleep(1 + attempt) - continue - raise - - time.sleep(0.5) - - for dir_info in dirs: - if dir_info.get("type") != "dir": - continue - author = dir_info.get("name", "") - if not author.startswith("@{"): - continue - - author_url = f"{GITEE_API_BASE}/store/{urllib.parse.quote(author, safe='')}" - for attempt in range(3): - try: - with _gitee_request(author_url, timeout=15) as resp: - plugin_dirs = json.loads(resp.read().decode("utf-8")) - break - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - if attempt < 2: - time.sleep(1 + attempt) - continue - raise - - time.sleep(0.5) - - for plugin_dir in plugin_dirs: - if plugin_dir.get("type") != "dir": - continue - plugin_name = plugin_dir.get("name", "") - - manifest_url = f"{GITEE_API_BASE}/store/{urllib.parse.quote(author, safe='')}/{plugin_name}/manifest.json" - manifest = {} - for attempt in range(3): - try: - with _gitee_request(manifest_url, timeout=15) as resp: - manifest = json.loads(resp.read().decode("utf-8")) - break - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - if attempt < 2: - time.sleep(1 + attempt) - continue - - plugins.append({ - "name": plugin_name, - "author": author, - "full_name": f"{author}/{plugin_name}", - "metadata": manifest.get("metadata", {}), - "dependencies": manifest.get("dependencies", []), - "has_config": False, - "is_installed": self._is_plugin_installed(plugin_name, author) - }) - - time.sleep(0.5) - - self._remote_cache = plugins - self._cache_time = now - except Exception as e: - Log.error("pkg-manager", f"获取远程插件列表失败: {type(e).__name__}: {e}") - - return plugins - - def _install_from_gitee(self, plugin_name: str, author: str) -> bool: - import time - try: - api_url = f"{GITEE_API_BASE}/store/{author}/{plugin}/{sub_dir}" - with _gitee_request(api_url, timeout=15) as resp: - items = json.loads(resp.read().decode("utf-8")) - - local_dir.mkdir(parents=True, exist_ok=True) - for item in items: - if item.get("type") == "file": - filename = item.get("name") - raw_url = f"{GITEE_RAW_BASE}/store/{author}/{plugin}/{sub_dir}/{filename}" - try: - with _gitee_request(raw_url, timeout=15) as resp: - content = resp.read() - with open(local_dir / filename, 'wb') as f: - f.write(content) - except: - pass - elif item.get("type") == "dir": - self._download_dir_raw(author, plugin, f"{sub_dir}/{item.get('name')}", local_dir / item.get("name")) - except: - pass - - - def _scan_all_plugins(self) -> list: - plugin_dir = self.store_dir / author / plugin_name - return (plugin_dir / "main.py").exists() - - def _find_plugin_dir(self, plugin_name: str) -> Path | None: - plugin_dir = self._find_plugin_dir(plugin_name) - if not plugin_dir: - return {} - schema_path = plugin_dir / "config.json" - if not schema_path.exists(): - return {} - with open(schema_path, 'r', encoding='utf-8') as f: - return json.load(f) - - def _load_plugin_config(self, plugin_name: str) -> dict: - if self.storage: - storage_instance = self.storage.get_storage("pkg-manager") - return storage_instance.get(f"plugin_config.{plugin_name}", {}) - return {} - - def _get_plugin_detailed_info(self, plugin_name: str) -> dict: - return {} diff --git a/store/NebulaShell/pkg-manager/manifest.json b/store/NebulaShell/pkg-manager/manifest.json deleted file mode 100644 index 1a86890..0000000 --- a/store/NebulaShell/pkg-manager/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "metadata": { - "name": "pkg-manager", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件包管理器 - 配置管理/商店/多语言项目部署支持", - "type": "webui-extension" - }, - "config": { - "enabled": true, - "args": { - "store_url": "https://store.nebulashell.org", - "auto_update": false, - "verify_signatures": true, - "cache_enabled": true, - "max_cache_size": 524288000 - } - }, - "dependencies": ["http-api", "webui", "plugin-storage", "i18n"], - "permissions": ["lifecycle", "plugin-storage"] -} diff --git a/store/NebulaShell/plugin-bridge/README.md b/store/NebulaShell/plugin-bridge/README.md deleted file mode 100644 index ef274aa..0000000 --- a/store/NebulaShell/plugin-bridge/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# plugin-bridge 插件桥接器 - -提供插件间的事件共享、广播、桥接和 RPC 服务调用。 - -## 功能 - -- **事件总线**: 插件间共享事件(发布/订阅) -- **广播**: 向多个插件发送消息 -- **桥接**: 将不同插件的事件互相映射 -- **RPC 服务调用**: 插件 A 调用插件 B 的方法并获取返回值 - -## 事件总线(发布/订阅 + 解耦) - -```python -bridge = plugin_mgr.get("plugin-bridge") -bus = bridge.event_bus - -# 订阅事件(发布者和订阅者解耦) -bus.on("http.request", lambda event: print(f"收到请求: {event.payload}")) - -# 发布事件 -bus.emit(BridgeEvent( - type="http.request", - source_plugin="http-api", - payload={"path": "/api/users"} -)) -``` - -## RPC 服务调用 - -```python -# 插件 B 注册服务 -bridge.services.register("plugin-b", "get_user", lambda user_id: {"id": user_id, "name": "test"}) - -# 插件 A 调用插件 B 的服务 -result = bridge.services.call("plugin-b", "get_user", 123) -print(result) # {"id": 123, "name": "test"} -``` - -## 广播 - -```python -broadcast = bridge.broadcast - -# 创建频道 -broadcast.create_channel("system", ["lifecycle", "metrics"]) - -# 广播消息 -broadcast.broadcast("system", {"action": "shutdown"}, "plugin-loader") -``` - -## 桥接 - -```python -bridge_mgr = bridge.bridge - -# 创建桥接:将 http-api 的事件映射到 metrics -bridge_mgr.create_bridge( - name="http-to-metrics", - from_plugin="http-api", - to_plugin="metrics", - event_mapping={ - "http.request": "metrics.http_request", - "http.error": "metrics.http_error", - } -) -``` - -## 事件历史 - -```python -# 查询历史 -history = bus.get_history("http.request") - -# 清空历史 -bus.clear_history() -``` diff --git a/store/NebulaShell/plugin-bridge/SIGNATURE b/store/NebulaShell/plugin-bridge/SIGNATURE deleted file mode 100644 index d834740..0000000 --- a/store/NebulaShell/plugin-bridge/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "yHmcdnBP6fx7TYFqHyiQeVYiP+S/9o7gx+fCC7nELQ2ZM55yXn9e4qpYWgPGAEw4zmuZbnKwLj0JQ1sE8BW28059+HWCj34ytUY/gckNvEkN+cGrqefwxWPGU19tysDC9Iy+HgBc+t34/igLZvRbcqpCpE0KH9SGfe34de6C60fL/HYZ1v3A29R05VmoPUBIOUY3X/9R5q4fYkjQqzvJ9LXujRR7Uyg8vP4dQo3k/MdxALg0xemXrMNRvX9F2g7i7DLCG8ABNxLHl7u5BymNXqBBClSu+/Fuf0HeyzLyYoOUP0Jhbxf56ep8jFLZRTU1qbt6itmaZgF8YSUh4oq1rWNYHZLZYH9sO6H32XsqXSq/509DkKXWJDZtIvJB/yrmVpt1Anj8YfMyA4pZ/R+htMa+coOlCAw20lnN0IMJW8oduKoYHFKMKkE7b++TzUv+7jon7WRWW8/2BXUFGV62jUSkPzI5o4TOgflHcCbLJ6SuOutxTpGiereVdDxlLRUVwBcRxY89DM9LKzqBPCbfG4Q6bVTtIvnyHn/ARQuYYXw41QzJGUYss/pS0YIH0YgYUHR88RCFqlZI53JXv1Y7kzieEprEWBBWEr6YxmYhx010W36hI0mM7YpBK3XWVkN7oJFBDt7DzFSQEYeeKDV/U0ZZgA5ufSiB8LYLYVjpz9Y=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.957446, - "plugin_hash": "97113f6d132bf58ea11688416b0fa3dda3a3642f3b82fd1e0b65ad06f8aad39c", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-bridge/main.py b/store/NebulaShell/plugin-bridge/main.py deleted file mode 100644 index cddeaa6..0000000 --- a/store/NebulaShell/plugin-bridge/main.py +++ /dev/null @@ -1,217 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Callable -from pathlib import Path -import importlib.util - -from oss.plugin.types import Plugin - - -@dataclass -class BridgeEvent: - type: str - source_plugin: str - payload: Any = None - context: dict[str, Any] = field(default_factory=dict) - - -class EventBus: - def __init__(self): - self._handlers: dict[str, list[Callable]] = {} - self._history: list[BridgeEvent] = [] - - def emit(self, event: BridgeEvent): - self._history.append(event) - handlers = self._handlers.get(event.type, []) - wildcard_handlers = self._handlers.get("*", []) - for handler in handlers + wildcard_handlers: - try: - handler(event) - except Exception as e: - import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc() - - def on(self, event_type: str, handler: Callable): - if event_type not in self._handlers: - self._handlers[event_type] = [] - self._handlers[event_type].append(handler) - - def off(self, event_type: str, handler: Callable): - if event_type in self._handlers: - try: - self._handlers[event_type].remove(handler) - except ValueError: - pass - - def once(self, event_type: str, handler: Callable): - def wrapper(event): - self.off(event_type, wrapper) - handler(event) - self.on(event_type, wrapper) - - def get_history(self, event_type: str = None) -> list[BridgeEvent]: - if event_type: - return [e for e in self._history if e.type == event_type] - return self._history.copy() - - def clear_history(self): - self._history.clear() - - -class BroadcastManager: - def __init__(self, event_bus: EventBus): - self.event_bus = event_bus - self._channels: dict[str, list[str]] = {} - - def create_channel(self, name: str, plugins: list[str]): - self._channels[name] = plugins - - def broadcast(self, channel: str, payload: Any, source_plugin: str = ""): - if channel not in self._channels: - return - event = BridgeEvent( - type=f"broadcast.{channel}", - source_plugin=source_plugin, - payload=payload - ) - self.event_bus.emit(event) - - def get_channels(self) -> dict[str, list[str]]: - return dict(self._channels) - - -class ServiceRegistry: - def __init__(self): - self._services: dict[str, dict[str, Callable]] = {} - - def register(self, plugin_name: str, service_name: str, handler: Callable): - if plugin_name not in self._services: - self._services[plugin_name] = {} - self._services[plugin_name][service_name] = handler - - def unregister(self, plugin_name: str, service_name: str = None): - if plugin_name in self._services: - if service_name: - self._services[plugin_name].pop(service_name, None) - else: - del self._services[plugin_name] - - def call(self, plugin_name: str, service_name: str, *args, **kwargs) -> Any: - plugin = self._services.get(plugin_name) - if plugin and service_name in plugin: - return plugin[service_name](*args, **kwargs) - return None - - def list_services(self, plugin_name: str = None) -> dict: - if plugin_name: - return self._services.get(plugin_name, {}).copy() - return {k: v.copy() for k, v in self._services.items()} - - -class BridgeManager: - def __init__(self, event_bus: EventBus): - self.event_bus = event_bus - self._bridges: dict = {} - - def create_bridge(self, name: str, from_plugin: str, to_plugin: str, event_mapping: dict): - self._bridges[name] = { - "from": from_plugin, - "to": to_plugin, - "mapping": event_mapping, - } - for src_event, dst_event in event_mapping.items(): - def handler(event, dst_event=dst_event): - bridged = BridgeEvent( - type=dst_event, - source_plugin=event.source_plugin, - payload=event.payload, - context={**event.context, "_bridged_from": event.type} - ) - self.event_bus.emit(bridged) - self.event_bus.on(src_event, handler) - - def remove_bridge(self, name: str): - self._bridges.pop(name, None) - - def get_bridges(self) -> dict: - return self._bridges.copy() - - -_use_cache: dict[str, Any] = {} - -def use(plugin_name: str): - if plugin_name in _use_cache: - return _use_cache[plugin_name] - - from oss.plugin.manager import get_plugin_manager - manager = get_plugin_manager() - if manager and plugin_name in manager.plugins: - _use_cache[plugin_name] = manager.plugins[plugin_name] - return _use_cache[plugin_name] - - # 插件未通过 plugin-loader 加载,记录警告 - from oss.logger.logger import Log - Log.warn("plugin-bridge", f"use('{plugin_name}') 绕过 plugin-loader 直接加载,建议通过 plugin-loader 管理插件生命周期") - - from oss.config import get_config - config = get_config() - store_dir = Path(config.get("store_dir", "store")) - - if not store_dir.exists(): - return None - - for ns_dir in store_dir.iterdir(): - if not ns_dir.is_dir(): - continue - for pdir in ns_dir.iterdir(): - if not pdir.is_dir(): - continue - manifest = pdir / "manifest.json" - if not manifest.exists(): - continue - try: - meta = json.loads(manifest.read_text()) - name = meta.get("name", pdir.name) - if name == plugin_name: - main_file = pdir / "main.py" - if not main_file.exists(): - continue - PluginClass = None - if manager and plugin_name in manager._plugin_types: - PluginClass = manager._plugin_types[plugin_name] - if PluginClass is None: - spec = importlib.util.spec_from_file_location(f"use_{plugin_name}", str(main_file)) - if spec and spec.loader: - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - for attr in dir(mod): - cls = getattr(mod, attr) - if isinstance(cls, type) and issubclass(cls, Plugin) and cls is not Plugin: - PluginClass = cls - break - if PluginClass: - instance = PluginClass() if isinstance(PluginClass, type) else PluginClass - _use_cache[plugin_name] = instance - if manager: - manager.plugins[plugin_name] = instance - if hasattr(instance, "start"): - instance.start() - return instance - except (json.JSONDecodeError, OSError): - continue - return None - - -class PluginBridgePlugin(Plugin): - def __init__(self): - self.event_bus = EventBus() - self.services = ServiceRegistry() - self.broadcast = BroadcastManager(self.event_bus) - self.bridge = BridgeManager(self.event_bus) - - def start(self): - self.event_bus.clear_history() - - def set_plugin_storage(self, storage_plugin): - pass - - def stop(self): - self.event_bus.clear_history() diff --git a/store/NebulaShell/plugin-bridge/manifest.json b/store/NebulaShell/plugin-bridge/manifest.json deleted file mode 100644 index 34b1bf0..0000000 --- a/store/NebulaShell/plugin-bridge/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "metadata": { - "name": "plugin-bridge", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件桥接器 - 共享事件/广播/桥接/多语言支持", - "type": "core", - "load_priority": "first" - }, - "config": { - "enabled": true, - "args": { - "max_events": 1000, - "event_ttl": 3600, - "broadcast_enabled": true, - "queue_size": 5000 - } - }, - "dependencies": ["plugin-storage", "i18n"], - "permissions": ["plugin-storage", "lifecycle"] -} diff --git a/store/NebulaShell/plugin-loader-pro/SIGNATURE b/store/NebulaShell/plugin-loader-pro/SIGNATURE deleted file mode 100644 index b94b203..0000000 --- a/store/NebulaShell/plugin-loader-pro/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "j3U1ZFmpc+pOBC8auYyj84O9DMaAmhOx7F0yGIdrpnclTvteuuXDa7qdBduF+cTu7JStUxN9Yx4oA8dZkorvCZgShQ26jWgLxTAUpa74Pqv6b1q1KQVGcgmiIcF5spIu3zNH4R2tfAWidm7Jncmd2BDDrjVMg16d6Bk73fvMN8GajAaNt3PELIr55LFEER3mOMB9ooeuvUmr7EIoDvZap5bLO4iP88kZaKd6xArNhYi5sCgm4HOxKxUFBOLRAnmJFcOKTqGLL0kYwsoqiN1UPLEawndQKNyX47ZQRfKCut8qQZEPpXl4rYpI6j++Lw7NNrj/jX+IEWFpqMaXiumJAG3tDWKWd5I/7/CAOpttERooJEjG2tVyM2ka9HjIyrc4TrWD9DZTamwkRlrbWm0Q7soTn3O6ZkolQ2n/WUxWKu1o84OHkeeoXDg9AS/uiKsOf7ufTpL7doXUm4bj4xTNkPk63D5PlAoF/kLBgcLHo2UkdxYhv9Y/moig2ogqr//nU5ucIZLmGIIX2Bag8RKgwnhRnKZ+KIGJntIuOoAuoH1H3G/EV42/siqU/AsRSOBtCxhAoqBxaHzZMnyios8kguE/6BfIEs7yS4DzN2ANNcA6tXfbvWGq7oeEB2DBAdamPbyVB76rSsdi0/4zGugvXmBJO4yZuxcuu/HeBH7ES+0=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964226.5213168, - "plugin_hash": "bed620b64c10798828613a45e3227a7849a9a450e471dfd009135354fb650a1e", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader-pro/circuit/__init__.py b/store/NebulaShell/plugin-loader-pro/circuit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/circuit/breaker.py b/store/NebulaShell/plugin-loader-pro/circuit/breaker.py deleted file mode 100644 index 712b67e..0000000 --- a/store/NebulaShell/plugin-loader-pro/circuit/breaker.py +++ /dev/null @@ -1,28 +0,0 @@ - -class CircuitBreaker: - def __init__(self, failure_threshold: int = 3, recovery_timeout: int = 60, half_open_requests: int = 1): - self.failure_threshold = failure_threshold - self.recovery_timeout = recovery_timeout - self.half_open_requests = half_open_requests - - self.state = CircuitState.CLOSED - self.failure_count = 0 - self.success_count = 0 - self.last_failure_time = 0 - self.half_open_calls = 0 - - def call(self, func: Callable, *args, **kwargs) -> Any: - self.failure_count = 0 - if self.state == CircuitState.HALF_OPEN: - self.half_open_calls += 1 - if self.half_open_calls >= self.half_open_requests: - self.state = CircuitState.CLOSED - self.half_open_calls = 0 - - def _on_failure(self): - self.state = CircuitState.CLOSED - self.failure_count = 0 - self.half_open_calls = 0 - - def get_state(self) -> str: - return self.state diff --git a/store/NebulaShell/plugin-loader-pro/circuit/state.py b/store/NebulaShell/plugin-loader-pro/circuit/state.py deleted file mode 100644 index 6a173dd..0000000 --- a/store/NebulaShell/plugin-loader-pro/circuit/state.py +++ /dev/null @@ -1,4 +0,0 @@ -class CircuitState: - CLOSED = "closed" - OPEN = "open" - HALF_OPEN = "half_open" \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader-pro/core/__init__.py b/store/NebulaShell/plugin-loader-pro/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/core/config.py b/store/NebulaShell/plugin-loader-pro/core/config.py deleted file mode 100644 index ed950c1..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/config.py +++ /dev/null @@ -1,23 +0,0 @@ -class ProConfig: - def __init__(self, config: dict = None): - config = config or {} - self.failure_threshold = config.get("failure_threshold", 3) - self.recovery_timeout = config.get("recovery_timeout", 60) - self.half_open_requests = config.get("half_open_requests", 1) - - -class RetryConfig: - def __init__(self, config: dict = None): - config = config or {} - self.interval = config.get("interval", 30) - self.timeout = config.get("timeout", 5) - self.max_failures = config.get("max_failures", 5) - - -class AutoRecoveryConfig: - def __init__(self, config: dict = None): - config = config or {} - self.enabled = config.get("enabled", True) - self.timeout_per_plugin = config.get("timeout_per_plugin", 30) - - diff --git a/store/NebulaShell/plugin-loader-pro/core/enhancer.py b/store/NebulaShell/plugin-loader-pro/core/enhancer.py deleted file mode 100644 index 34dec8a..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/enhancer.py +++ /dev/null @@ -1,102 +0,0 @@ - -class PluginLoaderEnhancer: - def __init__(self, plugin_manager, config: ProConfig): - self.pm = plugin_manager - self.config = config - self._breakers = {} - self._health_checker = None - self._auto_recovery = AutoRecovery( - config.auto_recovery.max_attempts, - config.auto_recovery.delay - ) - self._enhanced = False - - def enhance(self): - for name, info in self.pm.plugins.items(): - self._breakers[name] = CircuitBreaker( - self.config.circuit_breaker.failure_threshold, - self.config.circuit_breaker.recovery_timeout, - self.config.circuit_breaker.half_open_requests - ) - ProLogger.debug("enhancer", f"为 {name} 创建熔断器") - - def _wrap_start_methods(self): - ordered = self._get_ordered_plugins() - - for name in ordered: - self._safe_call(name, 'init', '初始化') - - for name in ordered: - self._safe_call(name, 'start', '启动') - - def _safe_start_all(self): - info = self.pm.plugins.get(name) - if not info: - return - - instance = info.get("instance") - if not instance or not hasattr(instance, method): - return - - breaker = self._breakers.get(name) - if not breaker: - try: - getattr(instance, method)() - except Exception as e: - ProLogger.error("safe", f"{name} {action}失败: {type(e).__name__}: {e}") - self._on_plugin_error(name, info, str(e)) - return - - def do_call(): - return getattr(instance, method)() - - try: - breaker.call(do_call) - info["info"].error_count = 0 - ProLogger.info("safe", f"{name} {action}成功") - except Exception as e: - ProLogger.error("safe", f"{name} {action}失败: {type(e).__name__}: {e}") - self._on_plugin_error(name, info, str(e)) - - def _on_plugin_error(self, name: str, info: dict, error: str): - self._health_checker = HealthChecker( - self.config.health_check.interval, - self.config.health_check.timeout, - self.config.health_check.max_failures - ) - - for name, info in self.pm.plugins.items(): - self._health_checker.add_plugin(name, info["instance"]) - - self._health_checker.start( - on_failure_callback=self._on_health_check_failure - ) - ProLogger.info("enhancer", "健康检查已启动") - - def _on_health_check_failure(self, name: str): - ordered = [] - visited = set() - - def visit(name): - if name in visited: - return - visited.add(name) - - info = self.pm.plugins.get(name) - if not info: - return - - for dep in info["info"].dependencies: - clean_dep = dep.rstrip("}") - if clean_dep in self.pm.plugins: - visit(clean_dep) - - ordered.append(name) - - for name in self.pm.plugins: - visit(name) - - return ordered - - def disable(self): - pass diff --git a/store/NebulaShell/plugin-loader-pro/core/manager.py b/store/NebulaShell/plugin-loader-pro/core/manager.py deleted file mode 100644 index 8d2589f..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/manager.py +++ /dev/null @@ -1,106 +0,0 @@ - -class ProPluginManager: - def __init__(self, config: ProConfig): - self.config = config - self.plugins: dict[str, dict[str, Any]] = {} - self.capability_registry = CapabilityRegistry() - self._breakers: dict[str, CircuitBreaker] = {} - self._health_checker = HealthChecker( - config.health_check.interval, - config.health_check.timeout, - config.health_check.max_failures - ) - self._auto_recovery = AutoRecovery( - config.auto_recovery.max_attempts, - config.auto_recovery.delay - ) - - def load_all(self, store_dir: str = "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 - - main_file = plugin_dir / "main.py" - if not main_file.exists(): - continue - - self._load_single_plugin(plugin_dir) - - def _load_single_plugin(self, plugin_dir: Path) -> Optional[Any]: - ProLogger.info("manager", "开始初始化所有插件...") - - self._inject_dependencies() - ordered = self._get_ordered_plugins() - - for name in ordered: - self._safe_init(name) - - ProLogger.info("manager", "开始启动所有插件...") - for name in ordered: - self._safe_start(name) - - self._health_checker.start( - on_failure_callback=self._on_plugin_failure - ) - - def _safe_init(self, name: str): - info = self.plugins[name] - instance = info["instance"] - breaker = self._breakers[name] - - try: - breaker.call(instance.start) - info["info"].status = "running" - self._health_checker.add_plugin(name, instance) - ProLogger.info("manager", f"已启动: {name}") - except Exception as e: - ProLogger.error("manager", f"启动失败 {name}: {type(e).__name__}: {e}") - info["info"].status = "error" - info["info"].error_count += 1 - info["info"].last_error = str(e) - - def stop_all(self): - info = self.plugins[name] - instance = info["instance"] - - try: - instance.stop() - info["info"].status = "stopped" - ProLogger.info("manager", f"已停止: {name}") - except Exception as e: - ProLogger.warn("manager", f"停止异常 {name}: {type(e).__name__}: {e}") - - def _on_plugin_failure(self, name: str): - name_map = {} - for name in self.plugins: - clean = name.rstrip("}") - name_map[clean] = name - name_map[clean + "}"] = name - - for name, info in self.plugins.items(): - deps = info["info"].dependencies - if not deps: - continue - - for dep_name in deps: - actual_dep = name_map.get(dep_name) or name_map.get(dep_name + "}") - if actual_dep and actual_dep in self.plugins: - dep_instance = self.plugins[actual_dep]["instance"] - setter = f"set_{dep_name.replace('-', '_')}" - - if hasattr(info["instance"], setter): - try: - getattr(info["instance"], setter)(dep_instance) - ProLogger.info("inject", f"{name} <- {actual_dep}") - except Exception as e: - ProLogger.error("inject", f"注入失败 {name}.{setter}: {type(e).__name__}: {e}") - - def _get_ordered_plugins(self) -> list[str]: - return [] diff --git a/store/NebulaShell/plugin-loader-pro/core/proxy.py b/store/NebulaShell/plugin-loader-pro/core/proxy.py deleted file mode 100644 index 4121287..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/proxy.py +++ /dev/null @@ -1,21 +0,0 @@ -class ProPluginProxy: - pass - - -class PluginProxy: - def __init__(self, plugin_name: str, allowed_plugins: list[str], all_plugins: dict): - self._plugin_name = plugin_name - self._allowed_plugins = allowed_plugins - self._all_plugins = all_plugins - - def get_plugin(self, name: str): - if name not in self._allowed_plugins and "*" not in self._allowed_plugins: - raise PermissionError( - f"插件 '{self._plugin_name}' 无权访问插件 '{name}'" - ) - if name not in self._all_plugins: - return None - return self._all_plugins[name]["instance"] - - def list_plugins(self) -> list[str]: - return list(self._all_plugins.keys()) diff --git a/store/NebulaShell/plugin-loader-pro/core/registry.py b/store/NebulaShell/plugin-loader-pro/core/registry.py deleted file mode 100644 index 3206544..0000000 --- a/store/NebulaShell/plugin-loader-pro/core/registry.py +++ /dev/null @@ -1,16 +0,0 @@ - -class ProCapabilityRegistry: - def __init__(self, permission_check: bool = True): - self.providers: dict[str, dict[str, Any]] = {} - self.consumers: dict[str, list[str]] = {} - self.permission_check = permission_check - - def register_provider(self, capability: str, plugin_name: str, instance: Any): - if capability not in self.consumers: - self.consumers[capability] = [] - if plugin_name not in self.consumers[capability]: - self.consumers[capability].append(plugin_name) - - def get_provider(self, capability: str, requester: str = "", - allowed_plugins: list[str] = None) -> Optional[Any]: - return None diff --git a/store/NebulaShell/plugin-loader-pro/fallback/__init__.py b/store/NebulaShell/plugin-loader-pro/fallback/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/fallback/handler.py b/store/NebulaShell/plugin-loader-pro/fallback/handler.py deleted file mode 100644 index 8b1ac82..0000000 --- a/store/NebulaShell/plugin-loader-pro/fallback/handler.py +++ /dev/null @@ -1,20 +0,0 @@ -class FallbackHandler: - RETURN_DEFAULT = "return_default" - RETURN_CACHE = "return_cache" - RETURN_NULL = "return_null" - CALL_ALTERNATIVE = "call_alternative" - - def __init__(self): - self._cache = {} - - def execute(self, plugin_name: str, func: Callable, *args, **kwargs): - try: - result = func(*args, **kwargs) - self._cache[plugin_name] = result - return result - except Exception as e: - ProLogger.warn("fallback", f"插件 {plugin_name} 执行失败,触发降级: {type(e).__name__}: {e}") - return self._apply_fallback(plugin_name) - - def _apply_fallback(self, plugin_name: str) -> Any: - return None diff --git a/store/NebulaShell/plugin-loader-pro/isolation/__init__.py b/store/NebulaShell/plugin-loader-pro/isolation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/isolation/timeout.py b/store/NebulaShell/plugin-loader-pro/isolation/timeout.py deleted file mode 100644 index bab75ea..0000000 --- a/store/NebulaShell/plugin-loader-pro/isolation/timeout.py +++ /dev/null @@ -1,21 +0,0 @@ -class TimeoutIsolation: - pass - - -class TimeoutController: - def __init__(self, timeout: int = 30): - self.timeout = timeout - - def execute(self, func: Callable, *args, **kwargs): - def handler(signum, frame): - raise TimeoutError(f"执行超时 (>{self.timeout}s)") - - old_handler = signal.signal(signal.SIGALRM, handler) - signal.alarm(self.timeout) - - try: - result = func(*args, **kwargs) - signal.alarm(0) - return result - finally: - signal.signal(signal.SIGALRM, old_handler) diff --git a/store/NebulaShell/plugin-loader-pro/main.py b/store/NebulaShell/plugin-loader-pro/main.py deleted file mode 100644 index 5dca411..0000000 --- a/store/NebulaShell/plugin-loader-pro/main.py +++ /dev/null @@ -1,73 +0,0 @@ -class PluginLoaderProPlugin: - def __init__(self): - self.plugin_loader = None - self.enhancer = None - self.config = None - self._started = False - - def meta(self): - from oss.plugin.types import Metadata, PluginConfig, Manifest - return Manifest( - metadata=Metadata( - name="plugin-loader-pro", - version="1.0.0", - author="NebulaShell", - description="为 plugin-loader 提供熔断、降级、容错、自动修复等高级机制" - ), - config=PluginConfig( - enabled=True, - args={} - ), - dependencies=["plugin-loader"] - ) - - def set_plugin_loader(self, plugin_loader): - self.plugin_loader = plugin_loader - ProLogger.info("main", "已注入 plugin-loader") - - def init(self, deps: dict = None): - if not self.plugin_loader: - try: - from store.NebulaShell.plugin_bridge.main import use - self.plugin_loader = use("plugin-loader") - except Exception: - pass - if not self.plugin_loader: - ProLogger.warn("main", "未找到 plugin-loader 依赖") - return - - config = {} - if deps: - config = deps.get("config", {}) - - self.config = ProConfig(config) - self.enhancer = PluginLoaderEnhancer( - self.plugin_loader.manager, - self.config - ) - - ProLogger.info("main", "增强器已初始化") - - def start(self): - if self._started: - return - self._started = True - - if not self.enhancer: - ProLogger.warn("main", "增强器未初始化,跳过启动") - return - - ProLogger.info("main", "开始增强 plugin-loader...") - self.enhancer.enhance() - - def stop(self): - ProLogger.info("main", "停止增强器...") - if self.enhancer: - self.enhancer.disable() - - -register_plugin_type("PluginLoaderPro", PluginLoaderPro) - - -def New(): - return PluginLoaderPro() diff --git a/store/NebulaShell/plugin-loader-pro/manifest.json b/store/NebulaShell/plugin-loader-pro/manifest.json deleted file mode 100644 index adbde77..0000000 --- a/store/NebulaShell/plugin-loader-pro/manifest.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "metadata": { - "name": "plugin-loader-pro", - "version": "1.0.0", - "author": "NebulaShell", - "description": "插件加载 Pro - 为 plugin-loader 提供熔断、降级、容错、自动修复等高级机制", - "type": "enhancer" - }, - "config": { - "enabled": true, - "args": { - "circuit_breaker": { - "failure_threshold": 3, - "recovery_timeout": 60, - "half_open_requests": 1 - }, - "retry": { - "max_retries": 3, - "backoff_factor": 2, - "initial_delay": 1 - }, - "health_check": { - "interval": 30, - "timeout": 5, - "max_failures": 5 - }, - "auto_recovery": { - "enabled": true, - "max_attempts": 3, - "delay": 10 - }, - "isolation": { - "enabled": true, - "timeout_per_plugin": 30 - } - } - }, - "dependencies": ["plugin-loader"], - "permissions": ["*"] -} diff --git a/store/NebulaShell/plugin-loader-pro/models/__init__.py b/store/NebulaShell/plugin-loader-pro/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/models/plugin_info.py b/store/NebulaShell/plugin-loader-pro/models/plugin_info.py deleted file mode 100644 index 1e72db1..0000000 --- a/store/NebulaShell/plugin-loader-pro/models/plugin_info.py +++ /dev/null @@ -1,25 +0,0 @@ -class ProPluginInfo: - def __init__(self): - self.name: str = "" - self.version: str = "" - self.author: str = "" - self.description: str = "" - self.readme: str = "" - self.config: dict[str, Any] = {} - self.extensions: dict[str, Any] = {} - self.lifecycle: Any = None - self.capabilities: set[str] = set() - self.dependencies: list[str] = [] - self.status: str = "idle" - self.error_count: int = 0 - self.last_error: str = "" - - def to_dict(self) -> dict: - return { - "name": self.name, - "version": self.version, - "author": self.author, - "description": self.description, - "status": self.status, - "error_count": self.error_count - } diff --git a/store/NebulaShell/plugin-loader-pro/recovery/__init__.py b/store/NebulaShell/plugin-loader-pro/recovery/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py b/store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py deleted file mode 100644 index 998cdc7..0000000 --- a/store/NebulaShell/plugin-loader-pro/recovery/auto_fix.py +++ /dev/null @@ -1,13 +0,0 @@ - -class AutoFixRecovery: - def __init__(self, max_attempts: int = 3, delay: int = 10): - self.max_attempts = max_attempts - self.delay = delay - self._recovery_attempts: dict[str, int] = {} - - def attempt_recovery(self, name: str, plugin_dir: Path, - module: any, instance: any) -> bool: - self._recovery_attempts[name] = 0 - - def get_attempts(self, name: str) -> int: - return self._recovery_attempts.get(name, 0) diff --git a/store/NebulaShell/plugin-loader-pro/recovery/health.py b/store/NebulaShell/plugin-loader-pro/recovery/health.py deleted file mode 100644 index 663722e..0000000 --- a/store/NebulaShell/plugin-loader-pro/recovery/health.py +++ /dev/null @@ -1,36 +0,0 @@ - -class HealthChecker: - def __init__(self, interval: int = 30, timeout: int = 5, max_failures: int = 5): - self.interval = interval - self.timeout = timeout - self.max_failures = max_failures - - self._running = False - self._thread = None - self._plugins: dict[str, Any] = {} - self._failure_counts: dict[str, int] = {} - self._on_failure_callback = None - - def add_plugin(self, name: str, instance: Any): - self._on_failure_callback = on_failure_callback - self._running = True - self._thread = threading.Thread(target=self._check_loop, daemon=True) - self._thread.start() - ProLogger.info("health", "健康检查已启动") - - def stop(self): - while self._running: - for name, instance in self._plugins.items(): - self._check_plugin(name, instance) - time.sleep(self.interval) - - def _check_plugin(self, name: str, instance: Any): - self._failure_counts[name] = self._failure_counts.get(name, 0) + 1 - - if self._failure_counts[name] >= self.max_failures: - ProLogger.warn("health", f"插件 {name} 连续失败 {self._failure_counts[name]} 次") - if self._on_failure_callback: - self._on_failure_callback(name) - - def reset_failure_count(self, name: str): - return self._failure_counts.get(name, 0) diff --git a/store/NebulaShell/plugin-loader-pro/retry/__init__.py b/store/NebulaShell/plugin-loader-pro/retry/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/retry/handler.py b/store/NebulaShell/plugin-loader-pro/retry/handler.py deleted file mode 100644 index 3ae9c45..0000000 --- a/store/NebulaShell/plugin-loader-pro/retry/handler.py +++ /dev/null @@ -1,12 +0,0 @@ - -class RetryHandler: - def __init__(self, config: RetryConfig = None): - config = config or RetryConfig() - self.max_retries = config.max_retries - self.backoff_factor = config.backoff_factor - self.initial_delay = config.initial_delay - - def execute(self, func: Callable, *args, **kwargs) -> Any: - delay = self.initial_delay * (self.backoff_factor ** attempt) - jitter = random.uniform(0, delay * 0.1) - return delay + jitter diff --git a/store/NebulaShell/plugin-loader-pro/utils/__init__.py b/store/NebulaShell/plugin-loader-pro/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/plugin-loader-pro/utils/logger.py b/store/NebulaShell/plugin-loader-pro/utils/logger.py deleted file mode 100644 index 6c828c1..0000000 --- a/store/NebulaShell/plugin-loader-pro/utils/logger.py +++ /dev/null @@ -1,29 +0,0 @@ - -class ProLogger: - _COLORS = { - "reset": "\033[0m", - "white": "\033[0;37m", - "yellow": "\033[1;33m", - "blue": "\033[1;34m", - "red": "\033[1;31m", - } - - @staticmethod - def _colorize(text: str, color: str) -> str: - tag = ProLogger._colorize(f"[pro:{component}]", "white") - msg = ProLogger._colorize(message, "white") - print(f"{tag} {msg}") - - @staticmethod - def warn(component: str, message: str): - tag = ProLogger._colorize(f"[pro:{component}]", "red") - icon = ProLogger._colorize("✗", "red") - msg = ProLogger._colorize(message, "red") - print(f"{tag} {icon} {msg}") - - @staticmethod - def debug(component: str, message: str): - tag = ProLogger._colorize(f"[pro:{component}]", "blue") - icon = ProLogger._colorize("→", "blue") - msg = ProLogger._colorize(message, "blue") - print(f"{tag} {icon} {msg}") diff --git a/store/NebulaShell/plugin-loader/PL_EXAMPLE.md b/store/NebulaShell/plugin-loader/PL_EXAMPLE.md deleted file mode 100644 index ffeb653..0000000 --- a/store/NebulaShell/plugin-loader/PL_EXAMPLE.md +++ /dev/null @@ -1,172 +0,0 @@ -# PL 注入机制使用说明 - -## 概述 - -PL 注入机制允许插件通过 `PL/` 文件夹向插件加载器注册自定义功能。插件加载器在启动时会自动扫描所有插件,检查其 `manifest.json` 中是否声明了 `pl_injection` 配置项。 - -## 使用步骤 - -### 1. 在 manifest.json 中声明 pl_injection - -在插件的 `manifest.json` 的 `config.args` 中添加 `"pl_injection": true`: - -```json -{ - "metadata": { - "name": "my-plugin", - "version": "1.0.0", - "author": "MyName", - "description": "我的插件", - "type": "utility" - }, - "config": { - "enabled": true, - "args": { - "pl_injection": true - } - }, - "dependencies": [], - "permissions": [] -} -``` - -### 2. 创建 PL/ 文件夹和 PL/main.py - -在插件目录下创建 `PL/` 文件夹,并在其中创建 `main.py`: - -``` -store/@{MyName}/my-plugin/ -├── manifest.json # 声明 pl_injection: true -├── main.py # 插件主逻辑 -├── PL/ # PL 注入文件夹 -│ └── main.py # 注入逻辑(必须包含 register() 函数) -└── README.md -``` - -### 3. 实现 PL/main.py - -`PL/main.py` 必须导出一个 `register(injector)` 函数,接收一个 `PLInjector` 实例: - -```python -# PL/main.py -"""PL 注入 - 向插件加载器注册功能""" - -def register(injector): - """向插件加载器注册功能 - - Args: - injector: PLInjector 实例,提供以下注册方法: - - register_function(name, func, description="") - - register_route(method, path, handler) - - register_event_handler(event_name, handler) - """ - - # 示例 1: 注册一个普通功能 - def my_helper(): - print("这是从 PL 注入的功能") - - injector.register_function("my_helper", my_helper, "一个辅助功能") - - # 示例 2: 注册 HTTP 路由 - def hello_handler(request): - return {"message": "Hello from PL injection!"} - - injector.register_route("GET", "/pl/hello", hello_handler) - - # 示例 3: 注册事件处理器 - def on_plugin_started(plugin_name): - print(f"插件 {plugin_name} 已启动") - - injector.register_event_handler("plugin.started", on_plugin_started) -``` - -### 4. 引用其他文件 - -`PL/main.py` 可以引用 `PL/` 文件夹下的其他 Python 文件: - -``` -store/@{MyName}/my-plugin/PL/ -├── main.py # 入口,包含 register() 函数 -├── helpers.py # 辅助函数(被 main.py 引用) -└── routes.py # 路由定义(被 main.py 引用) -``` - -```python -# PL/main.py -from .helpers import format_response -from .routes import register_routes - -def register(injector): - def my_handler(): - return format_response("Hello") - injector.register_function("my_handler", my_handler) - register_routes(injector) -``` - -## 行为说明 - -| 场景 | 结果 | -|------|------| -| manifest.json 中 `pl_injection: true` + 存在 `PL/main.py` | ✅ 正常加载,执行注入 | -| manifest.json 中 `pl_injection: true` + 缺少 `PL/` 文件夹 | ❌ 警告并拒绝加载该插件 | -| manifest.json 中 `pl_injection: true` + 存在 `PL/` 但缺少 `main.py` | ❌ 警告并拒绝加载该插件 | -| manifest.json 中未声明 `pl_injection` | ✅ 正常加载,跳过 PL 检查 | -| manifest.json 中 `pl_injection: false` | ✅ 正常加载,跳过 PL 检查 | - -## 安全限制 - -PL 注入机制实施了多层安全限制,防止恶意代码注入: - -### 1. 文件类型限制 -- PL 文件夹中禁止包含 `.sh`、`.bat`、`.exe`、`.dll`、`.so`、`.dylib`、`.bin` 等可执行/二进制文件 -- 违反则拒绝加载该插件 - -### 2. 静态源码安全检查 -PL/main.py 源码在编译前会进行静态扫描,禁止以下操作: -- 导入系统级模块(`os`、`sys`、`subprocess`、`shutil`、`socket`、`ctypes`、`cffi`、`multiprocessing`、`threading`) -- 使用 `__import__`、`exec`、`eval`、`compile` -- 直接操作文件(`open`) -- 访问 `__builtins__` - -### 3. 沙箱执行环境 -PL/main.py 在受限的沙箱中执行,仅提供安全的 builtins: -- 基础类型:`dict`、`list`、`str`、`int`、`float`、`bool`、`tuple`、`set` -- 安全函数:`len`、`range`、`enumerate`、`zip`、`map`、`filter`、`sorted` 等 -- 异常类型:`Exception`、`ValueError`、`TypeError`、`KeyError`、`IndexError` - -### 4. 参数校验 -| 校验项 | 限制 | -|--------|------| -| 功能名称 | 仅允许字母、数字、下划线、冒号、斜杠、连字符、点,最长 128 字符 | -| 路由路径 | 必须以 `/` 开头,禁止 `..`、`//`、`/\.`、`~`、`%`,最长 256 字符 | -| HTTP 方法 | 仅允许 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS | -| 事件名称 | 字母开头,仅允许字母、数字、点、下划线,最长 128 字符 | -| 功能描述 | 最长 256 字符 | - -### 5. 数量限制 -| 限制项 | 上限 | -|--------|------| -| 每个插件最多注册的功能数 | 50 | -| 每个功能名称最多被注册次数 | 10 | - -### 6. 异常安全 -- 所有注册的函数会被自动包装,执行时抛出异常不会影响主流程 -- 异常会被记录到日志,函数返回 `None` - -### 7. 调用者溯源 -- 通过栈帧回溯自动识别调用者插件名 -- 防止其他插件冒充注册 - -## 注入器 API - -`PLInjector` 实例提供以下方法供 `PL/main.py` 调用: - -| 方法 | 说明 | -|------|------| -| `register_function(name, func, description="")` | 注册一个注入功能 | -| `register_route(method, path, handler)` | 注册 HTTP 路由 | -| `register_event_handler(event_name, handler)` | 注册事件处理器 | -| `get_injected_functions(name=None)` | 获取已注册的注入功能 | -| `get_injection_info(plugin_name=None)` | 获取注入信息 | -| `has_injection(plugin_name)` | 检查插件是否有 PL 注入 | -| `get_registry_info()` | 获取注册表完整信息(用于监控) | \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader/README.md b/store/NebulaShell/plugin-loader/README.md deleted file mode 100644 index d4c368f..0000000 --- a/store/NebulaShell/plugin-loader/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# plugin-loader 插件加载器 - -核心插件,负责扫描、加载和管理所有其他插件。 - -## 功能 - -- 自动扫描 `store/` 目录 -- 动态加载 `main.py` 并调用 `New()` 获取实例 -- 解析 `manifest.json` 获取插件元数据 -- 自动扫描插件能力(AST 分析) -- 按依赖关系排序加载顺序 -- 关联能力提供者与消费者 - -## 使用 - -无需手动使用,框架启动时自动加载。 diff --git a/store/NebulaShell/plugin-loader/SIGNATURE b/store/NebulaShell/plugin-loader/SIGNATURE deleted file mode 100644 index ea9b389..0000000 --- a/store/NebulaShell/plugin-loader/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "XqvMe6BmA+hE8PqWCdKTDT8B9lKt2Oo9ZzZ0/KNlHEM3mk3cRzs4ZCUR4Qx4veqDAycTixT0cwp0hSh0VGNjBTg/hqVkmsDMb02Ky1TUhM1VGXYoffaKP7F5cqVLRc6Le0/mrL8MtdUUxt5USsdpFCuF+LLs+HQ2w/xPZ50n6GwdFIE2cvQJUGpMjLgI7jebmTFFLeED/DK9v9Pki1n27R3tvV333h9SAMO6L30IwJy6dwpssZb60RxLMkYvwokWYRePHKWzfdS9+huZ0o8fK6bYcs2CtBzZ4RDpwojSBPElIaBdn647+kspVTefEFlXvamdPM42pkojWsMU4Ed2Hgnasrz1aAlL7u94b6zcjOWQguRNgVsWFB8kKFR0nLaKUWQvULtDduEFpegU/dI0u1zZuRVmd58TSaLVXReUuARG0viop04pxiqf3H2IGwEafzlprnwQe9IWINgvABdC34UpCw/enBRUj2gjan2Up7nRhz0CMAUKo1TBhRMErp5f0AthZwbHrrq3g5wwKRoftV6O7GSiirbPSMe/ypb5mkdQmdHqOUvhlCexeeMhKB/9J7e2UhJ8YSlq7uZMrMc8dEWwkqMQeiw1uOCnCujlHYfk2RmPRwEZTUB/VQJmYuJhzSuI1XXA52ZcJaHf7Bh62d/ftMZ9OQimTpJg4y365jk=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775970391.1348627, - "plugin_hash": "0052362f57f6c9b50adc7ff19a37fa57344f298eade3dd5152c916054879b846", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-loader/main.py b/store/NebulaShell/plugin-loader/main.py deleted file mode 100644 index bf9a7d6..0000000 --- a/store/NebulaShell/plugin-loader/main.py +++ /dev/null @@ -1,758 +0,0 @@ -"""插件加载器插件 - 支持能力扫描和扩展 + PL 注入机制""" -import sys -import json -import re -import types -import traceback -import importlib.util -from pathlib import Path -from typing import Any, Optional, Callable - -from oss.plugin.types import Plugin, register_plugin_type -from oss.plugin.capabilities import scan_capabilities - - -class Log: - """智能彩色日志""" - _TTY = sys.stdout.isatty() - _C = {"reset": "\033[0m", "white": "\033[0;37m", "yellow": "\033[1;33m", "blue": "\033[1;34m", "red": "\033[1;31m"} - - @classmethod - def c(cls, text: str, color: str) -> str: - if not cls._TTY: return text - return f"{cls._C.get(color, '')}{text}{cls._C['reset']}" - - @classmethod - def info(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'white')} {cls.c(msg, 'white')}") - @classmethod - def warn(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'yellow')} {cls.c('⚠', 'yellow')} {cls.c(msg, 'yellow')}") - @classmethod - def tip(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'blue')} {cls.c('ℹ', 'blue')} {cls.c(msg, 'blue')}") - @classmethod - def error(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'red')} {cls.c('✗', 'red')} {cls.c(msg, 'red')}") - @classmethod - def ok(cls, tag: str, msg: str): print(f"{cls.c(f'[{tag}]', 'white')} {cls.c(msg, 'white')}") - - -class PluginInfo: - """插件信息""" - def __init__(self): - self.name: str = "" - self.version: str = "" - self.author: str = "" - self.description: str = "" - self.readme: str = "" - self.config: dict[str, Any] = {} - self.extensions: dict[str, Any] = {} - self.lifecycle: Any = None - self.capabilities: set[str] = set() - self.dependencies: list[str] = [] - self.pl_injected: bool = False - - -class PermissionError(Exception): - """权限错误""" - pass - - -class PluginProxy: - """插件代理 - 防止越级访问""" - def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict): - self._plugin_name = plugin_name - self._plugin_instance = plugin_instance - self._allowed_plugins = set(allowed_plugins) - self._all_plugins = all_plugins - - def get_plugin(self, name: str) -> Any: - if name not in self._allowed_plugins and "*" not in self._allowed_plugins: - raise PermissionError(f"插件 '{self._plugin_name}' 无权访问插件 '{name}'") - if name not in self._all_plugins: return None - return self._all_plugins[name]["instance"] - - def list_plugins(self) -> list[str]: - if "*" in self._allowed_plugins: return list(self._all_plugins.keys()) - return [n for n in self._allowed_plugins if n in self._all_plugins] - - def get_capability(self, capability: str) -> Any: return None - def __getattr__(self, name: str): return getattr(self._plugin_instance, name) - - -class CapabilityRegistry: - """能力注册表""" - def __init__(self, permission_check: bool = True): - self.providers: dict = {} - self.consumers: dict = {} - self.permission_check = permission_check - - def register_provider(self, capability: str, plugin_name: str, instance: Any): - self.providers[capability] = {"plugin": plugin_name, "instance": instance} - if capability not in self.consumers: self.consumers[capability] = [] - - def register_consumer(self, capability: str, plugin_name: str): - if capability not in self.consumers: self.consumers[capability] = [] - if plugin_name not in self.consumers[capability]: self.consumers[capability].append(plugin_name) - - def get_provider(self, capability: str, requester: str = "", allowed_plugins: list = None) -> Optional[Any]: - if capability not in self.providers: return None - if self.permission_check and allowed_plugins is not None: - pn = self.providers[capability]["plugin"] - if pn != requester and pn not in allowed_plugins and "*" not in allowed_plugins: - raise PermissionError(f"插件 '{requester}' 无权使用能力 '{capability}'") - return self.providers[capability]["instance"] - - def has_capability(self, capability: str) -> bool: return capability in self.providers - def get_consumers(self, capability: str) -> list: return self.consumers.get(capability, []) - - -class PLValidationError(Exception): - """PL 校验错误""" - pass - - -class PLInjector: - """PL 注入管理器 - 带完整安全限制""" - - MAX_FUNCTIONS_PER_PLUGIN = 50 - MAX_REGISTRATIONS_PER_NAME = 10 - MAX_NAME_LENGTH = 128 - MAX_DESCRIPTION_LENGTH = 256 - - _FUNCTION_NAME_RE = re.compile(r'^[a-zA-Z0-9_:/\-.]+$') - _EVENT_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]+$') - _ROUTE_PATH_RE = re.compile(r'^/[a-zA-Z0-9_\-/.]+$') - _FORBIDDEN_ROUTE_PATTERNS = [r'\.\.', r'//', r'/\.', r'~', r'\%'] - - def __init__(self, plugin_manager: 'PluginManager'): - self._plugin_manager = plugin_manager - self._injections: dict = {} - self._injection_registry: dict = {} - self._plugin_function_count: dict = {} - - def check_and_load_pl(self, plugin_dir: Path, plugin_name: str) -> bool: - """检查并加载 PL 文件夹,返回 True 表示成功""" - pl_dir = plugin_dir / "PL" - if not pl_dir.exists() or not pl_dir.is_dir(): - Log.warn("plugin-loader", f"插件 '{plugin_name}' 声明了 pl_injection,但缺少 PL/ 文件夹,拒绝加载") - return False - - pl_main = pl_dir / "main.py" - if not pl_main.exists(): - Log.warn("plugin-loader", f"插件 '{plugin_name}' 的 PL/ 文件夹中缺少 main.py,拒绝加载") - return False - - # 禁止危险文件类型 - forbidden_ext = {'.sh', '.bat', '.exe', '.dll', '.so', '.dylib', '.bin'} - for f in pl_dir.rglob('*'): - if f.suffix.lower() in forbidden_ext: - Log.error("plugin-loader", f"插件 '{plugin_name}' 的 PL/ 文件夹包含危险文件: {f.name},拒绝加载") - return False - - try: - # 受限沙箱 - 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, 'setattr': setattr, - 'ValueError': ValueError, 'TypeError': TypeError, - 'KeyError': KeyError, 'IndexError': IndexError, - 'Exception': Exception, 'BaseException': BaseException, - } - safe_globals = { - '__builtins__': safe_builtins, - '__name__': f'plugin.{plugin_name}.PL', - '__package__': f'plugin.{plugin_name}.PL', - '__file__': str(pl_main), - } - - with open(pl_main, 'r', encoding='utf-8') as f: - source = f.read() - - # 静态源码安全检查 - self._static_source_check(source, str(pl_main)) - - code = compile(source, str(pl_main), 'exec') - exec(code, safe_globals) - - register_func = safe_globals.get('register') - if register_func and callable(register_func): - register_func(self) - Log.ok("plugin-loader", f"插件 '{plugin_name}' PL 注入成功") - else: - Log.warn("plugin-loader", f"插件 '{plugin_name}' 的 PL/main.py 缺少 register() 函数,但仍允许加载") - - self._injections[plugin_name] = {"dir": str(pl_dir)} - return True - - except PLValidationError as e: - Log.error("plugin-loader", f"插件 '{plugin_name}' PL 安全检查失败: {e}") - return False - except SyntaxError as e: - Log.error("plugin-loader", f"插件 '{plugin_name}' PL/main.py 语法错误: {e}") - return False - except FileNotFoundError as e: - Log.error("plugin-loader", f"插件 '{plugin_name}' PL 文件不存在:{e}") - return False - except PermissionError as e: - Log.error("plugin-loader", f"插件 '{plugin_name}' PL 文件权限错误:{e}") - return False - except Exception as e: - Log.error("plugin-loader", f"加载插件 '{plugin_name}' 的 PL 失败:{type(e).__name__}: {e}") - import traceback - traceback.print_exc() - return False - - def _static_source_check(self, source: str, file_path: str): - """静态源码安全检查 - 增强版,防止字符串拼接/编码绕过""" - import base64 - - # 首先检查是否有 base64 编码的恶意代码 - try: - # 查找所有字符串字面量 - string_pattern = r'([A-Za-z0-9+/=]{20,})' - for match in re.finditer(string_pattern, source): - try: - decoded = base64.b64decode(match.group(1)).decode('utf-8', errors='ignore') - # 检查解码后的内容 - for dangerous in ['import ', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess']: - if dangerous in decoded: - raise PLValidationError(f"{file_path} - 检测到 base64 编码的恶意代码") - except: - pass - except: - pass - - # 检查字符串拼接绕过 (如 'ex' + 'ec') - concat_patterns = [ - r"""['"]ex['"]\s*\+\s*['"]ec['"]""", - r"""['"]impor['"]\s*\+\s*['"]t['"]""", - r"""['"]eva['"]\s*\+\s*['"]l['"]""", - r"""['"]compil['"]\s*\+\s*['"]e['"]""", - ] - for pattern in concat_patterns: - if re.search(pattern, source): - raise PLValidationError(f"{file_path} - 检测到字符串拼接绕过尝试") - - forbidden = [ - (r'^\s*import\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)', '禁止导入系统级模块'), - (r'^\s*from\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)\s+import', '禁止导入系统级模块'), - (r'__import__\s*\(', '禁止使用 __import__'), - (r'(? bool: - if not name or not isinstance(name, str): return False - if len(name) > self.MAX_NAME_LENGTH: return False - return bool(self._FUNCTION_NAME_RE.match(name)) - - def _validate_route_path(self, path: str) -> bool: - if not path or not isinstance(path, str): return False - if len(path) > 256: return False - if not self._ROUTE_PATH_RE.match(path): return False - for p in self._FORBIDDEN_ROUTE_PATTERNS: - if re.search(p, path): return False - return True - - def _validate_event_name(self, event_name: str) -> bool: - if not event_name or not isinstance(event_name, str): return False - if len(event_name) > self.MAX_NAME_LENGTH: return False - return bool(self._EVENT_NAME_RE.match(event_name)) - - def _check_plugin_limit(self, plugin_name: str) -> bool: - count = self._plugin_function_count.get(plugin_name, 0) - if count >= self.MAX_FUNCTIONS_PER_PLUGIN: - Log.warn("plugin-loader", f"插件 '{plugin_name}' 注册功能数已达上限 ({self.MAX_FUNCTIONS_PER_PLUGIN})") - return False - return True - - def _check_name_limit(self, name: str) -> bool: - registrations = self._injection_registry.get(name, []) - if len(registrations) >= self.MAX_REGISTRATIONS_PER_NAME: - Log.warn("plugin-loader", f"功能名称 '{name}' 注册次数已达上限 ({self.MAX_REGISTRATIONS_PER_NAME})") - return False - return True - - def _wrap_function(self, func: Callable, plugin_name: str, name: str) -> Callable: - """包装函数,异常安全""" - def _safe_wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception as e: - Log.error("plugin-loader", f"PL 注入功能 '{name}' (来自 {plugin_name}) 执行异常: {e}") - return None - return _safe_wrapper - - def _get_caller_plugin_name(self) -> Optional[str]: - """通过栈帧回溯获取调用者插件名""" - stack = traceback.extract_stack() - for frame in stack: - filename = frame.filename - if '/PL/' in filename and 'main.py' in filename: - parts = Path(filename).parts - for i, part in enumerate(parts): - if part == 'PL': - return parts[i - 1] if i > 0 else None - return None - - def register_function(self, name: str, func: Callable, description: str = ""): - """注册注入功能 - 带参数校验和权限限制""" - if not self._validate_function_name(name): - Log.error("plugin-loader", f"PL 注入功能名称非法: '{name}'") - return - if not callable(func): - Log.error("plugin-loader", f"PL 注入功能 '{name}' 不是可调用对象") - return - if description and len(description) > self.MAX_DESCRIPTION_LENGTH: - description = description[:self.MAX_DESCRIPTION_LENGTH] - - plugin_name = self._get_caller_plugin_name() or "unknown" - - if not self._check_plugin_limit(plugin_name): return - if not self._check_name_limit(name): return - - wrapped_func = self._wrap_function(func, plugin_name, name) - - if name not in self._injection_registry: - self._injection_registry[name] = [] - self._injection_registry[name].append({ - "func": wrapped_func, "plugin": plugin_name, "description": description, - }) - self._plugin_function_count[plugin_name] = self._plugin_function_count.get(plugin_name, 0) + 1 - Log.tip("plugin-loader", f"PL 注入功能已注册: '{name}' (来自 {plugin_name})") - - def register_route(self, method: str, path: str, handler: Callable): - """注册 HTTP 路由 - 带路径安全校验""" - valid_methods = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'} - method_upper = method.upper() - if method_upper not in valid_methods: - Log.error("plugin-loader", f"PL 注入路由方法非法: '{method}'") - return - if not self._validate_route_path(path): - Log.error("plugin-loader", f"PL 注入路由路径非法: '{path}'") - return - self.register_function(f"{method_upper}:{path}", handler, f"路由 {method_upper} {path}") - - def register_event_handler(self, event_name: str, handler: Callable): - """注册事件处理器 - 带名称校验""" - if not self._validate_event_name(event_name): - Log.error("plugin-loader", f"PL 注入事件名称非法: '{event_name}'") - return - self.register_function(f"event:{event_name}", handler, f"事件 {event_name}") - - def get_injected_functions(self, name: str = None) -> list[Callable]: - if name: return [e["func"] for e in self._injection_registry.get(name, [])] - return [f for es in self._injection_registry.values() for f in [e["func"] for e in es]] - - def get_injection_info(self, plugin_name: str = None) -> dict: - if plugin_name: return self._injections.get(plugin_name, {}) - return dict(self._injections) - - def has_injection(self, plugin_name: str) -> bool: - return plugin_name in self._injections - - def get_registry_info(self) -> dict: - info = {} - for name, entries in self._injection_registry.items(): - info[name] = { - "count": len(entries), - "plugins": [e["plugin"] for e in entries], - "descriptions": [e["description"] for e in entries], - } - return info - - -class PluginManager: - """插件管理器""" - - def __init__(self, permission_check: bool = True): - self.plugins: dict = {} - self.lifecycle_plugin = None - self._dependency_plugin = None - self._signature_verifier = None - self.capability_registry = CapabilityRegistry(permission_check=permission_check) - self.permission_check = permission_check - self.enforce_signature = True - self.pl_injector = PLInjector(self) - - def set_signature_verifier(self, verifier): self._signature_verifier = verifier - def set_lifecycle(self, lifecycle_plugin): self.lifecycle_plugin = lifecycle_plugin - - def _load_manifest(self, plugin_dir: Path) -> dict: - mf = plugin_dir / "manifest.json" - if not mf.exists(): return {} - with open(mf, "r", encoding="utf-8") as f: return json.load(f) - - def _load_readme(self, plugin_dir: Path) -> str: - rf = plugin_dir / "README.md" - if not rf.exists(): return "" - with open(rf, "r", encoding="utf-8") as f: return f.read() - - def _load_config(self, plugin_dir: Path) -> dict: - """加载插件配置文件 - 使用 ast.literal_eval 安全解析""" - import ast - cf = plugin_dir / "config.py" - if not cf.exists(): - return {} - try: - with open(cf, "r", encoding="utf-8") as f: - content = f.read() - except FileNotFoundError: - Log.warn("plugin-loader", f"配置文件不存在:{cf}") - return {} - except PermissionError as e: - Log.error("plugin-loader", f"配置文件无权限读取:{cf} - {e}") - return {} - except UnicodeDecodeError as e: - Log.error("plugin-loader", f"配置文件编码错误:{cf} - {e}") - return {} - - # 使用 ast.literal_eval 安全解析(只允许字面量,不会执行代码) - try: - result = ast.literal_eval(content) - if isinstance(result, dict): - return {k: v for k, v in result.items() if not k.startswith("_")} - except (ValueError, SyntaxError): - pass - - # 如果失败,尝试提取简单的键值对 - config = {} - for line in content.split('\n'): - line = line.strip() - if not line or line.startswith('#'): - continue - match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line) - if match: - key, value_str = match.groups() - if key.startswith('_'): - continue - try: - value = ast.literal_eval(value_str) - config[key] = value - except (ValueError, SyntaxError): - Log.warn("plugin-loader", f"{cf} 跳过无效的值:{line}") - continue - return config - - - def _load_extensions(self, plugin_dir: Path) -> dict: - """加载插件扩展配置 - 使用 ast.literal_eval 安全解析""" - import ast - ef = plugin_dir / "extensions.py" - if not ef.exists(): - return {} - try: - with open(ef, "r", encoding="utf-8") as f: - content = f.read() - except Exception as e: - Log.error("plugin-loader", f"扩展文件读取失败:{e}") - return {} - - # 使用 ast.literal_eval 安全解析(只允许字面量,不会执行代码) - try: - result = ast.literal_eval(content) - if isinstance(result, dict): - return {k: v for k, v in result.items() if not k.startswith("_")} - except (ValueError, SyntaxError): - pass - - # 如果失败,尝试提取简单的键值对 - extensions = {} - for line in content.split('\n'): - line = line.strip() - if not line or line.startswith('#'): - continue - match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line) - if match: - key, value_str = match.groups() - if key.startswith('_'): - continue - try: - value = ast.literal_eval(value_str) - extensions[key] = value - except (ValueError, SyntaxError): - Log.warn("plugin-loader", f"{ef} 跳过无效的值:{line}") - continue - return extensions - - - def load(self, plugin_dir: Path, use_sandbox: bool = True) -> Optional[Any]: - """加载单个插件""" - main_file = plugin_dir / "main.py" - if not main_file.exists(): return None - - manifest = self._load_manifest(plugin_dir) - readme = self._load_readme(plugin_dir) - config = self._load_config(plugin_dir) - extensions = self._load_extensions(plugin_dir) - capabilities = scan_capabilities(plugin_dir) - plugin_name = plugin_dir.name.rstrip("}") - - # PL 注入检查 - pl_injection = manifest.get("config", {}).get("args", {}).get("pl_injection", False) - if pl_injection: - Log.tip("plugin-loader", f"插件 '{plugin_name}' 声明了 pl_injection,正在检查 PL/ 文件夹...") - if not self.pl_injector.check_and_load_pl(plugin_dir, plugin_name): - Log.error("plugin-loader", f"插件 '{plugin_name}' 因 PL 注入检查失败被拒绝加载") - return None - Log.ok("plugin-loader", f"插件 '{plugin_name}' PL 注入检查通过") - - permissions = manifest.get("permissions", []) - - # 不再使用沙箱,所有插件都直接加载(核心插件是可信的) - # use_sandbox 参数保留但不再实际使用 - spec = importlib.util.spec_from_file_location(f"plugin.{plugin_name}", str(main_file)) - module = importlib.util.module_from_spec(spec) - module.__package__ = f"plugin.{plugin_name}" - module.__path__ = [str(plugin_dir)] - sys.modules[spec.name] = module - spec.loader.exec_module(module) - if not hasattr(module, "New"): - return None - instance = module.New() - - if self.permission_check and permissions: - instance = PluginProxy(plugin_name, instance, permissions, self.plugins) - - info = PluginInfo() - meta = manifest.get("metadata", {}) - info.name = meta.get("name", plugin_name) - info.version = meta.get("version", "") - info.author = meta.get("author", "") - info.description = meta.get("description", "") - info.readme = readme - info.config = manifest.get("config", {}).get("args", config) - info.extensions = extensions - info.capabilities = capabilities - info.dependencies = manifest.get("dependencies", []) - info.pl_injected = pl_injection - - for cap in capabilities: - self.capability_registry.register_provider(cap, plugin_name, instance) - if self.lifecycle_plugin and plugin_name != "lifecycle": - info.lifecycle = self.lifecycle_plugin.create(plugin_name) - - self.plugins[plugin_name] = {"instance": instance, "module": module, "info": info, "permissions": permissions} - return instance - - def load_all(self, store_dir: str = "store"): - if 'plugin' not in sys.modules: - pkg = types.ModuleType('plugin') - pkg.__path__ = []; pkg.__package__ = 'plugin' - sys.modules['plugin'] = pkg - Log.tip("plugin-loader", "已创建 plugin 命名空间包") - - if not self._check_any_plugins(store_dir): - Log.warn("plugin-loader", "未检测到任何插件,自动引导安装...") - self._bootstrap_installation() - - lifecycle_plugin = None - lc_dir = Path(store_dir) / "NebulaShell" / "lifecycle" - if lc_dir.exists() and (lc_dir / "main.py").exists(): - try: - inst = self.load(lc_dir) - if inst: lifecycle_plugin = inst; self.plugins.pop("lifecycle", None) - except Exception as e: Log.warn("plugin-loader", f"lifecycle 插件加载失败:{type(e).__name__}: {e}") - - dep_plugin = None - dep_dir = Path(store_dir) / "NebulaShell" / "dependency" - if dep_dir.exists() and (dep_dir / "main.py").exists(): - try: - inst = self.load(dep_dir) - if inst: dep_plugin = inst; self._dependency_plugin = inst; self.plugins.pop("dependency", None) - except Exception as e: Log.warn("plugin-loader", f"dependency 插件加载失败:{type(e).__name__}: {e}") - - sig_dir = Path(store_dir) / "NebulaShell" / "signature-verifier" - if sig_dir.exists() and (sig_dir / "main.py").exists(): - try: - inst = self.load(sig_dir) - if inst: self.set_signature_verifier(inst.verifier); Log.ok("plugin-loader", "签名验证服务已加载") - except Exception as e: Log.warn("plugin-loader", f"signature-verifier 加载失败: {e}") - - if lifecycle_plugin: self.set_lifecycle(lifecycle_plugin) - self._load_plugins_from_dir(Path(store_dir)) - if dep_plugin: self._sort_by_dependencies(dep_plugin) - - def _load_plugins_from_dir(self, store_dir: Path): - if not store_dir.exists(): return - core_plugins = {"webui", "dashboard", "pkg-manager"} - skip = {"plugin-loader"} - plugin_dirs = [] - for ad in store_dir.iterdir(): - if ad.is_dir(): - for pd in ad.iterdir(): - if not pd.is_dir() or pd.name in skip or not (pd / "main.py").exists(): - continue - # 读取 load_priority,默认为 100 - priority = 100 - manifest_file = pd / "manifest.json" - if manifest_file.exists(): - try: - meta = json.loads(manifest_file.read_text()).get("metadata", {}) - raw = meta.get("load_priority", 100) - priority = 0 if raw == "first" else (int(raw) if isinstance(raw, (int, float)) else 100) - except (json.JSONDecodeError, OSError, (ValueError, TypeError)): - pass - plugin_dirs.append((priority, pd)) - # 按优先级升序排序(数值越小越先加载) - plugin_dirs.sort(key=lambda x: x[0]) - for _, pd in plugin_dirs: - self.load(pd, use_sandbox=pd.name not in core_plugins) - self._link_capabilities() - - def _check_any_plugins(self, store_dir: str) -> bool: - sp = Path(store_dir) - if sp.exists(): - for ad in sp.iterdir(): - if ad.is_dir(): - for pd in ad.iterdir(): - if pd.is_dir() and (pd / "main.py").exists(): return True - return False - - def _bootstrap_installation(self): Log.info("plugin-loader", "跳过引导安装(pkg 插件已移除)") - - def _sort_by_dependencies(self, dep_plugin): - if not dep_plugin: return - for n, i in self.plugins.items(): dep_plugin.add_plugin(n, i["info"].dependencies) - try: - order = dep_plugin.resolve() - sp = {} - for n in order: - if n in self.plugins: sp[n] = self.plugins[n] - for n in set(self.plugins.keys()) - set(sp.keys()): sp[n] = self.plugins[n] - self.plugins = sp - except Exception as e: Log.error("plugin-loader", f"依赖解析失败: {e}") - - def _link_capabilities(self): - for pn, info in self.plugins.items(): - for cap in info["info"].capabilities: - if self.capability_registry.has_capability(cap): - for cn in self.capability_registry.get_consumers(cap): - if cn in self.plugins: - ci = self.plugins[cn]["info"] - ca = self.plugins[cn].get("permissions", []) - try: - p = self.capability_registry.get_provider(cap, requester=cn, allowed_plugins=ca) - if p and hasattr(ci, "extensions"): ci.extensions[f"_{cap}_provider"] = p - except PermissionError as e: Log.error("plugin-loader", f"权限拒绝: {e}") - - def start_all(self): - self._inject_dependencies() - for n, i in self.plugins.items(): - try: i["instance"].start() - except Exception as e: Log.error("plugin-loader", f"启动失败 {n}: {e}") - - def init_and_start_all(self): - Log.info("plugin-loader", f"init_and_start_all 被调用,plugins={len(self.plugins)}") - self._inject_dependencies() - ordered = self._get_ordered_plugins() - Log.tip("plugin-loader", f"插件启动顺序: {' -> '.join(ordered)}") - for name in ordered: - if "plugin-loader" in name: continue - try: - Log.info("plugin-loader", f"初始化: {name}") - self.plugins[name]["instance"].init() - except Exception as e: Log.error("plugin-loader", f"初始化失败 {name}: {e}") - for name in ordered: - if "plugin-loader" in name: continue - try: - Log.info("plugin-loader", f"启动: {name}") - self.plugins[name]["instance"].start() - except Exception as e: Log.error("plugin-loader", f"启动失败 {name}: {e}") - - def _get_ordered_plugins(self) -> list[str]: - if not self._dependency_plugin: return list(self.plugins.keys()) - try: return [n for n in self._dependency_plugin.resolve() if n in self.plugins] - except Exception as e: Log.warn("plugin-loader", f"依赖解析失败,使用原始顺序: {e}"); return list(self.plugins.keys()) - - def _inject_dependencies(self): - Log.info("plugin-loader", f"开始注入依赖,共 {len(self.plugins)} 个插件") - nm = {} - for n in self.plugins: - c = n.rstrip("}"); nm[c] = n; nm[c + "}"] = n - for n, i in self.plugins.items(): - inst = i["instance"]; io = i.get("info") - if not io or not io.dependencies: continue - for dn in io.dependencies: - ad = nm.get(dn) or nm.get(dn + "}") - if ad and ad in self.plugins: - sn = f"set_{dn.replace('-', '_')}" - if hasattr(inst, sn): - try: getattr(inst, sn)(self.plugins[ad]["instance"]); Log.ok("plugin-loader", f"注入成功: {n} <- {ad}") - except Exception as e: Log.error("plugin-loader", f"注入依赖失败 {n}.{sn}: {e}") - else: Log.warn("plugin-loader", f"{n} 没有 {sn} 方法") - - def stop_all(self): - for n, i in reversed(list(self.plugins.items())): - try: i["instance"].stop() - except Exception as e: Log.error("plugin-loader", f"插件 {n} 停止失败:{type(e).__name__}: {e}") - if self.lifecycle_plugin: self.lifecycle_plugin.stop_all() - - def get_info(self, name: str) -> Optional[PluginInfo]: - if name in self.plugins: return self.plugins[name]["info"] - return None - - def has_capability(self, capability: str) -> bool: return self.capability_registry.has_capability(capability) - def get_capability_provider(self, capability: str) -> Optional[Any]: return self.capability_registry.get_provider(capability) - - -class PluginLoaderPlugin(Plugin): - """插件加载器插件""" - def __init__(self): - self.manager = PluginManager() - self._loaded = False - self._started = False - self._ensure_plugin_package() - - def _ensure_plugin_package(self): - if 'plugin' not in sys.modules: - pkg = types.ModuleType('plugin'); pkg.__path__ = []; sys.modules['plugin'] = pkg - - def init(self, deps: dict = None): - if self._loaded: return - self._loaded = True - self._ensure_plugin_package() - Log.info("plugin-loader", "开始加载插件...") - self.manager.load_all() - - def start(self): - if self._started: return - self._started = True - Log.info("plugin-loader", "启动插件...") - self.manager.init_and_start_all() - - def stop(self): - Log.info("plugin-loader", "停止插件...") - self.manager.stop_all() - - -register_plugin_type("PluginManager", PluginManager) -register_plugin_type("PluginInfo", PluginInfo) -register_plugin_type("CapabilityRegistry", CapabilityRegistry) -register_plugin_type("PLInjector", PLInjector) - - -def New(): - return PluginLoaderPlugin() diff --git a/store/NebulaShell/plugin-loader/manifest.json b/store/NebulaShell/plugin-loader/manifest.json deleted file mode 100644 index 9690e4f..0000000 --- a/store/NebulaShell/plugin-loader/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "metadata": { - "name": "plugin-loader", - "version": "1.0.0", - "author": "NebulaShell", - "description": "插件加载器 - 负责扫描、加载和管理所有插件", - "type": "core" - }, - "config": { - "enabled": true, - "args": { - "scan_dirs": ["store"], - "sandbox_enabled": true, - "permission_check": true - } - }, - "dependencies": [], - "permissions": ["*"] -} diff --git a/store/NebulaShell/plugin-storage/README.md b/store/NebulaShell/plugin-storage/README.md deleted file mode 100644 index 2543ab9..0000000 --- a/store/NebulaShell/plugin-storage/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# plugin-storage 插件存储 - -为所有插件提供隔离的键值存储服务。 - -## 功能 - -- **隔离存储**:每个插件有独立的命名空间 -- **持久化**:数据自动保存到 JSON 文件 -- **线程安全**:支持并发访问 -- **共享访问**:通过 plugin-bridge 可跨插件访问 - -## 基本使用 - -```python -storage_plugin = plugin_mgr.get("plugin-storage") - -# 获取插件的隔离存储 -storage = storage_plugin.get_storage("my-plugin") - -# 设置值 -storage.set("key", "value") -storage.set("config", {"theme": "dark", "lang": "zh"}) - -# 获取值 -value = storage.get("key") -config = storage.get("config", default={}) - -# 检查键 -if storage.has("key"): - print("存在") - -# 删除 -storage.delete("key") - -# 批量设置 -storage.set_many({"a": 1, "b": 2, "c": 3}) - -# 获取所有数据 -all_data = storage.get_all() - -# 清空 -storage.clear() -``` - -## 通过 plugin-bridge 访问 - -```python -bridge = plugin_mgr.get("plugin-bridge") -shared_storage = bridge.storage # 假设 bridge 集成了 storage - -# 获取其他插件的存储(需要权限) -other_storage = shared_storage.get_plugin_storage("other-plugin") -data = other_storage.get("some_key") -``` - -## 存储位置 - -``` -./data/storage/ -├── plugin-a/ -│ └── data.json -├── plugin-b/ -│ └── data.json -└── ... -``` - -## 元信息 - -```python -meta = storage.get_meta() -# {"plugin": "my-plugin", "keys": 5, "path": "./data/storage/my-plugin"} -``` diff --git a/store/NebulaShell/plugin-storage/SIGNATURE b/store/NebulaShell/plugin-storage/SIGNATURE deleted file mode 100644 index 8937148..0000000 --- a/store/NebulaShell/plugin-storage/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "TA08EBmVwhP0tyOhplpioxsGD4T8fRhj2ekEvQFGEaK2L1/USEGTcGXt/tciHNMU0AWVJ1bD9MY6aBj2+ljlSNCEyNlMMOeZot1/blcQG9wPsHbVXKm8VuyK0KBHwrM39DppbKIn4dlGL6A2Eua0bp20oCnmd2VF3IuTHGnGKmoXmehffXiVIlCgqIX2+wEqlD2TqYfP6LU+XWDYMtQ/ShS3ImbcIoChOzhj3H0LZKg+jd4d4N97B2z9uUinojZ4jJxix1qe6hednBiZNEGUub6/bn8DKtdRidPjwtwObKjL8etBFlca0mHEYvHe/T33uoqi1URGnbqXiiYneSKj4J1zJRMfDxfZhZ6ubeCcSiMufIgzNbMii1lR0mq/pCcUUM0X0I4ean2xk7ygW7xrN8ra3/73gOHvVBnVElwyPIpZaiPzvVnQ14nyv2zWFFezJNdkB0MOaoy0RRzk9Jp7DjGxn9B4f3sGAZX1gTUSzu91BvdhZOfy6VhS+t8Wf3PfY2t0OYyLPg28S0bQhfZ64HnjR2LR098jut40ckbzDRif0ZFtyj0OQWzbC0dTKlSEW6p2ozuWSROX2OMxY4F/srzAfh7SJL42FR9Lm/tlCVSRUVQ2NRMA8UFdtorsp09GDQmwy7xJxn9ghRWoXmrsaDSwvrZUlVo6LMa6ocvhvSo=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.8741558, - "plugin_hash": "e317a24422dcd005fc3f2db0fa34848bd81a45a3eb40cda0b8d20aadf2391cd1", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/plugin-storage/main.py b/store/NebulaShell/plugin-storage/main.py deleted file mode 100644 index 43a4b6f..0000000 --- a/store/NebulaShell/plugin-storage/main.py +++ /dev/null @@ -1,262 +0,0 @@ -from typing import Optional -from pathlib import Path - - -class PluginStorage: - def __init__(self, plugin_name: str, data_dir: str = None): - config = get_config() - self.plugin_name = plugin_name - self.data_dir = Path(data_dir or str(config.data_dir)) / plugin_name - self.data_dir.mkdir(parents=True, exist_ok=True) - self._data: dict[str, Any] = {} - self._lock = threading.Lock() - self._load() - - - def _load(self): - """从 data.json 加载持久化数据""" - data_file = self.data_dir / "data.json" - if data_file.exists(): - try: - with open(data_file, "r", encoding="utf-8") as f: - self._data = json.load(f) - except (json.JSONDecodeError, OSError) as e: - Log.error("plugin-storage", f"加载数据失败 {self.plugin_name}: {e}") - self._data = {} - else: - self._data = {} - - def _save(self): - """将数据持久化到 data.json""" - data_file = self.data_dir / "data.json" - try: - with open(data_file, "w", encoding="utf-8") as f: - json.dump(self._data, f, ensure_ascii=False, indent=2) - except OSError as e: - Log.error("plugin-storage", f"保存数据失败 {self.plugin_name}: {e}") - - def get(self, key: str, default: Any = None) -> Any: - with self._lock: - return self._data.get(key, default) - - def set(self, key: str, value: Any): - with self._lock: - self._data[key] = value - self._save() - - def delete(self, key: str) -> bool: - with self._lock: - if key in self._data: - del self._data[key] - self._save() - return True - return False - - def keys(self) -> list[str]: - with self._lock: - return list(self._data.keys()) - - def size(self) -> int: - with self._lock: - return len(self._data) - - def set_many(self, data: dict[str, Any]): - with self._lock: - self._data.update(data) - self._save() - - - def read_file(self, path: str, mode: str = "r") -> Optional[str | bytes]: - try: - file_path = self._resolve_path(path) - if file_path is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{path}") - return None - if not file_path.exists() or not file_path.is_file(): - return None - with open(file_path, mode, encoding="utf-8" if mode == "r" else None) as f: - return f.read() - except Exception as e: - Log.error("plugin-storage", f"读取文件失败 {self.plugin_name}/{path}: {type(e).__name__}: {e}") - return None - - def write_file(self, path: str, content: str | bytes): - try: - file_path = self._resolve_path(path) - if file_path is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{path}") - return - file_path.parent.mkdir(parents=True, exist_ok=True) - if isinstance(content, bytes): - with open(file_path, "wb") as f: - f.write(content) - else: - with open(file_path, "w", encoding="utf-8") as f: - f.write(content) - except Exception as e: - Log.error("plugin-storage", f"写入文件失败 {self.plugin_name}/{path}: {type(e).__name__}: {e}") - - def delete_file(self, path: str) -> bool: - try: - file_path = self._resolve_path(path) - if file_path is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{path}") - return False - if file_path.exists() and file_path.is_file(): - file_path.unlink() - return True - return False - except Exception as e: - Log.error("plugin-storage", f"删除文件失败 {self.plugin_name}/{path}: {type(e).__name__}: {e}") - return False - - def list_files(self, prefix: str = "") -> list[str]: - try: - search_dir = self._resolve_path(prefix) if prefix else self.data_dir - if search_dir is None: - Log.warn("plugin-storage", f"路径穿越被拒绝: {self.plugin_name}/{prefix}") - return [] - if not search_dir.exists(): - return [] - files = [] - for f in search_dir.rglob("*"): - if f.is_file(): - files.append(str(f.relative_to(self.data_dir))) - return sorted(files) - except Exception as e: - Log.error("plugin-storage", f"列出文件失败:{type(e).__name__}: {e}") - return [] - - def file_exists(self, path: str) -> bool: - file_path = self._resolve_path(path) - if file_path is None: - return False - return file_path.exists() and file_path.is_file() - - def serve_file(self, path: str): - try: - file_path = self._resolve_path(path) - if file_path is None: - return Response(status=403, body="Forbidden: path traversal detected") - - if not file_path.exists() or not file_path.is_file(): - return Response(status=404, body=f"File not found: {path}") - - content_type, _ = mimetypes.guess_type(str(file_path)) - if not content_type: - content_type = "application/octet-stream" - - if content_type.startswith("text/") or content_type in ( - "application/json", "application/javascript", "application/xml", - "text/css", "text/html", "image/svg+xml" - ): - content = file_path.read_text(encoding="utf-8") - else: - content = file_path.read_bytes() - - return Response( - status=200, - headers={ - "Content-Type": content_type, - "Cache-Control": "public, max-age=3600", - }, - body=content, - ) - except Exception as e: - return Response(status=500, body=f"Error serving file: {e}") - - def _resolve_path(self, path: str) -> Optional[Path]: - """安全解析路径,防止路径穿越 - - 将 path 拼接到 data_dir 下,resolve 后校验是否仍在 data_dir 范围内。 - 如果 path 试图穿越到 data_dir 之外,返回 None。 - """ - try: - target = (self.data_dir / path).resolve() - # 校验是否仍在 data_dir 范围内 - target.relative_to(self.data_dir.resolve()) - return target - except (ValueError, OSError): - return None - - -class SharedStorage: - def __init__(self, manager, shared_dir: Path): - self._manager = manager - self._shared_dir = shared_dir - self._shared_dir.mkdir(parents=True, exist_ok=True) - - def get_shared(self, key: str, default: Any = None) -> Any: - shared_file = self._shared_dir / f"{key}.json" - if not shared_file.exists(): - return default - with open(shared_file, "r", encoding="utf-8") as f: - return json.load(f) - - def set_shared(self, key: str, value: Any): - shared_file = self._shared_dir / f"{key}.json" - with open(shared_file, "w", encoding="utf-8") as f: - json.dump(value, f, ensure_ascii=False, indent=2) - - def list_storages(self) -> list[str]: - return [p.stem for p in self._shared_dir.glob("*.json")] - - -class PluginStoragePlugin(Plugin): - def __init__(self): - self.storages: dict[str, PluginStorage] = {} - self.shared = None - self.config = {} - self.data_root = Path("./data") - - def init(self, deps: dict = None): - """初始化时加载配置并初始化共享存储""" - config_path = Path("./data/plugin-storage/config.json") - if config_path.exists(): - try: - with open(config_path, "r", encoding="utf-8") as f: - self.config = json.load(f) - self.data_root = Path(self.config.get("data_root", "./data")) - shared_dir_name = self.config.get("shared_dir", "DCIM") - shared_dir = self.data_root / shared_dir_name - except (json.JSONDecodeError, OSError) as e: - Log.error("plugin-storage", f"加载配置失败: {e}") - shared_dir = self.data_root / "DCIM" - else: - Log.warn("plugin-storage", "config.json 不存在,使用默认配置") - self.config = {"data_root": "./data", "shared_dir": "DCIM"} - self.data_root = Path("./data") - shared_dir = self.data_root / "DCIM" - - self.shared = SharedStorage(self, shared_dir=shared_dir) - - def start(self): - Log.info("plugin-storage", f"插件存储服务已启动 (root={self.data_root})") - - def stop(self): - Log.info("plugin-storage", "插件存储服务已停止") - - def get_storage(self, plugin_name: str) -> PluginStorage: - if plugin_name not in self.storages: - self.storages[plugin_name] = PluginStorage(plugin_name) - return self.storages[plugin_name] - - def remove_storage(self, plugin_name: str) -> bool: - if plugin_name in self.storages: - del self.storages[plugin_name] - data_dir = PluginStorage(plugin_name).data_dir - if data_dir.exists(): - shutil.rmtree(data_dir) - return True - return False - - def list_storages(self) -> list[str]: - return list(self.storages.keys()) - - -register_plugin_type("PluginStorage", PluginStorage) -register_plugin_type("SharedStorage", SharedStorage) - - -def New(): - return PluginStoragePlugin() diff --git a/store/NebulaShell/plugin-storage/manifest.json b/store/NebulaShell/plugin-storage/manifest.json deleted file mode 100644 index 3047ad1..0000000 --- a/store/NebulaShell/plugin-storage/manifest.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "metadata": { - "name": "plugin-storage", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件存储 - 为所有插件提供隔离的键值存储服务/多语言支持", - "type": "utility" - }, - "config": { - "enabled": true, - "args": { - "data_dir": "./data/storage", - "max_size_per_plugin": 104857600, - "compression_enabled": true, - "encryption_enabled": false, - "backup_enabled": true, - "backup_interval": 86400 - } - }, - "dependencies": ["i18n"], - "permissions": ["lifecycle"] -} diff --git a/store/NebulaShell/plugin_bridge b/store/NebulaShell/plugin_bridge deleted file mode 120000 index f7c65b4..0000000 --- a/store/NebulaShell/plugin_bridge +++ /dev/null @@ -1 +0,0 @@ -plugin-bridge \ No newline at end of file diff --git a/store/NebulaShell/polyglot-deploy/manifest.json b/store/NebulaShell/polyglot-deploy/manifest.json deleted file mode 100644 index 3e19a08..0000000 --- a/store/NebulaShell/polyglot-deploy/manifest.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "metadata": { - "name": "polyglot-deploy", - "version": "1.1.0", - "author": "NebulaShell", - "description": "多语言项目部署服务 - 支持 Node.js/Python/Java/Go/Rust 等项目的一键部署/WebUI 管理", - "type": "deployment" - }, - "config": { - "enabled": true, - "args": { - "supported_languages": ["python", "nodejs", "java", "go", "rust", "php", "ruby"], - "build_timeout": 300, - "deploy_timeout": 600, - "max_projects": 50, - "workspace_dir": "/workspace/polyglot-projects", - "auto_cleanup": true, - "cleanup_interval": 3600, - "log_level": "info", - "docker_enabled": true, - "docker_network": "polyglot-net" - } - }, - "dependencies": ["http-api", "i18n", "pkg-manager"], - "permissions": ["lifecycle", "plugin-storage"] -} diff --git a/store/NebulaShell/signature-verifier/SIGNATURE b/store/NebulaShell/signature-verifier/SIGNATURE deleted file mode 100644 index 5cd49a7..0000000 --- a/store/NebulaShell/signature-verifier/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "ymWLA+iMcmMAum/qQIJdTgk31K/dNejA9BtOe5yUjAi58yKKW4mPx8c+6x9I28XM+eG1KkSAMrb94NFVVARR/k/Lc4QnNwbJVoYheEfKscMih6G4meiVYPFGkjMwukNL+k8qon+HJwAtHtZ9DFWclfZEXDKVptbr+3juovmBGIYdGlZ9pS2AmrYLsxx5SjCtuJb+cuOE0U5S5GBxBsv7tvP5IQAZZP1Crf0Cxe9Op6+UzX0TYfzWswIqcYzZdgPEUbMorwUlRPVgHGiaYtoHqQGISjR2kAgk5XW3NCuPAAQoiY9XTe+YD+3wDfQ7ic0tkESIJBBt7Zq2VJMmh8lwWMTRi0+xVgZHua2HpLdHDatSggWoCXQiMakm7rA1Z/Xto30mx0Pk4fh2vcYeuThoY6a+GsPbMxG4Bj52jSzGwHu264cgnSe8wS+HmbU0Ch1t4qD0lAZh297qRBqdr4hW+Lzf/FxVd5kQiL3rYcq8mi3Kd4nmiQ4/gev5mX7uSf6fhByCrXqe4pvgAt5q7BgByC9C/krcTRNIwV+u9PQe7zboXZY7x0CzYzITKkWzIY98Fl+8qmvuanLoPQ3tqAdixJr5I1Lpk71l2B5cW5ht2iB48RL4b+oPNqksIweRmPSDyTGBf3jS2YQNY62tdt4yXkWHO00qIOU0UQkGF+BKRSw=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775969853.454207, - "plugin_hash": "762549e109001210eb9fb1fbfc2b9a3c25cbb3ea899b796d07357d6ee32949d5", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/signature-verifier/main.py b/store/NebulaShell/signature-verifier/main.py deleted file mode 100644 index 8df4b6f..0000000 --- a/store/NebulaShell/signature-verifier/main.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -Plugin Signature Verification Service -- Verify integrity and origin authenticity of official plugins -- Support multiple signers (Falck unique signature) -- RSA-SHA256 asymmetric encryption scheme -""" - -import os -import json -import hashlib -import base64 -from pathlib import Path -from typing import Optional, Dict, List, Tuple - -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa -from cryptography.hazmat.backends import default_backend -from cryptography.exceptions import InvalidSignature - -from oss.plugin.types import Plugin -from oss.config import get_config - - -FALCK_PUBLIC_KEY_PEM = "" - -NEBULASHELL_PUBLIC_KEY_PEM = "" - - -class SignatureError(Exception): - pass - - -class SignatureVerifier: - def __init__(self, key_dir: str = None): - config = get_config() - self.key_dir = Path(key_dir or str(config.get("SIGNATURE_KEYS_DIR", "./data/signature-verifier/keys"))) - self.key_dir.mkdir(parents=True, exist_ok=True) - self.public_keys: Dict[str, bytes] = {} - self._load_builtin_keys() - - def _load_builtin_keys(self): - pub_dir = self.key_dir / "public" - if not pub_dir.exists(): - return - for key_file in pub_dir.glob("*.pem"): - author_name = key_file.stem - self.public_keys[author_name] = key_file.read_bytes() - - def _compute_plugin_hash(self, plugin_dir: Path) -> str: - """Compute content hash of the plugin directory. - Includes relative path + content of all files. - """ - hasher = hashlib.sha256() - - files_to_hash = [] - for file_path in sorted(plugin_dir.rglob("*")): - if file_path.is_file() and file_path.name != "SIGNATURE": - rel_path = file_path.relative_to(plugin_dir) - files_to_hash.append((str(rel_path), file_path)) - - for rel_path, file_path in files_to_hash: - hasher.update(rel_path.encode("utf-8")) - hasher.update(file_path.read_bytes()) - - return hasher.hexdigest() - - def verify_plugin(self, plugin_dir: Path, author: str = "Falck") -> Tuple[bool, str]: - """Verify plugin signature. - Returns: (is_valid, details) - """ - signature_file = plugin_dir / "SIGNATURE" - - if not signature_file.exists(): - return False, f"Plugin missing signature file: {plugin_dir}" - - try: - sig_data = json.loads(signature_file.read_text()) - except json.JSONDecodeError as e: - return False, f"Signature file format error: {e}" - - required_fields = ["signature", "signer", "algorithm", "timestamp"] - for field in required_fields: - if field not in sig_data: - return False, f"Signature missing required field: {field}" - - signer = sig_data["signer"] - signature = base64.b64decode(sig_data["signature"]) - - if signer not in self.public_keys: - return False, f"Unknown signer: {signer}" - - try: - public_key = serialization.load_pem_public_key( - self.public_keys[signer], - backend=default_backend() - ) - except Exception as e: - return False, f"Public key load failed: {e}" - - current_hash = self._compute_plugin_hash(plugin_dir) - - try: - signed_data = f"{author}:{current_hash}".encode("utf-8") - public_key.verify( - signature, - signed_data, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - return True, f"Signature verified (signer: {signer})" - except InvalidSignature: - return False, f"Signature mismatch! Plugin may have been tampered with (signer: {signer})" - except Exception as e: - return False, f"Signature verification error: {e}" - - def is_official_plugin(self, plugin_dir: Path) -> bool: - pass - - -class PluginSigner: - def __init__(self, private_key_path: Optional[str] = None): - self.private_key = None - if private_key_path: - self.load_private_key(private_key_path) - - def load_private_key(self, key_path: str): - with open(key_path, "rb") as f: - pem_data = f.read() - self.private_key = serialization.load_pem_private_key( - pem_data, - password=None, - backend=default_backend() - ) - - def sign_plugin(self, plugin_dir: Path, signer_name: str, author: str = "Falck") -> str: - """Generate signature for a plugin. - Returns: path to the signature file - """ - if not self.private_key: - raise ValueError("Private key not loaded") - - hasher = hashlib.sha256() - files_to_hash = [] - for file_path in sorted(plugin_dir.rglob("*")): - if file_path.is_file() and file_path.name not in ("SIGNATURE",): - rel_path = file_path.relative_to(plugin_dir) - files_to_hash.append((str(rel_path), file_path)) - - for rel_path, file_path in files_to_hash: - hasher.update(rel_path.encode("utf-8")) - hasher.update(file_path.read_bytes()) - - plugin_hash = hasher.hexdigest() - - import time - signed_data = f"{author}:{plugin_hash}".encode("utf-8") - signature = self.private_key.sign( - signed_data, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - - sig_data = { - "signature": base64.b64encode(signature).decode(), - "signer": signer_name, - "algorithm": "RSA-SHA256", - "timestamp": time.time(), - "plugin_hash": plugin_hash, - "author": author - } - - signature_file = plugin_dir / "SIGNATURE" - signature_file.write_text(json.dumps(sig_data, indent=2)) - - return str(signature_file) - - -class SignatureVerifierPlugin(Plugin): - def __init__(self): - self.verifier = SignatureVerifier() - self.signer = None - - def verify(self, plugin_dir: Path, author: str = "Falck") -> Tuple[bool, str]: - return self.verifier.verify_plugin(plugin_dir, author) - - def is_official(self, plugin_dir: Path) -> bool: - return self.verifier.is_official_plugin(plugin_dir) - - def sign(self, plugin_dir: Path, signer_name: str, author: str = "Falck") -> str: - if not self.signer: - raise SignatureError("Private key not loaded, cannot sign") - return self.signer.sign_plugin(plugin_dir, signer_name, author) - - def generate_keypair(self, author: str, key_dir: str = None): - pass diff --git a/store/NebulaShell/signature-verifier/manifest.json b/store/NebulaShell/signature-verifier/manifest.json deleted file mode 100644 index bbcb400..0000000 --- a/store/NebulaShell/signature-verifier/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "metadata": { - "name": "signature-verifier", - "version": "1.1.0", - "author": "NebulaShell", - "description": "插件签名验证服务 - 验证官方插件完整性与来源真实性/安全增强", - "type": "core" - }, - "config": { - "enabled": true, - "args": { - "enforce_official": true, - "key_dir": "data/signature-verifier/keys", - "algorithm": "RSA-SHA256", - "key_size": 2048, - "auto_verify": true, - "cache_enabled": true, - "cache_ttl": 3600 - } - }, - "dependencies": ["plugin-storage", "i18n"], - "permissions": ["plugin-storage"] -} diff --git a/store/NebulaShell/webui/SIGNATURE b/store/NebulaShell/webui/SIGNATURE deleted file mode 100644 index 692b193..0000000 --- a/store/NebulaShell/webui/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "EHeyw9j4zTQyLiTHSqEHQQL1wrzJOjm4jRKHIWuIiRUY6YORSLip1aDVP+aGpCf+KYGROE/pMt3SDUI7h+5VWAh9x/AYf0UrCOq38dNJ4+5TeHxOwUZvic2Ua26LBWRp0GfdRq/t06/dtXkIwD+0albetQNJoPkORBTCuxPVZqGVU6WkKWuYJ9xuQDhpn266qy6ZQfVe88BcNPbO//AIR8+t5gpd+hRmhbhxV58Omm+R0jtlx3ABEOH4g2HGkX961UvUdFSaoVMw7KR4lv9GQU1rMraP/zyHTLAQQlt/SxJAi3db51KWzFuH8rDsGKnB7LbJvnV32ojUNQs0SIO8935UY6RuHnKr8KHuAxFNX/1GA4MdloHhrK0Fm6Tx5FDXamthUFqJzYvjMtsGGN24p7/DQwaHqonB9AJ5szRf/vBYmsGs1WTCX/e89IN/uiVUPuqEiRxiJBRMLwpr2mz0r6e3keozWdPuxZ58WVH3Gd3gXvLngs+Gx3FyCd7RLtn24gkq/w16bCuA3XBE+9+n6QvAUBfvjiODCb9fjdPL/YNoJRMKqE1iAhMI+I5Cmu0ISOdTL4aYZEjZP3YwjauMKlpXMhclOwIv2I2btNQIKPOJj4SormqPweK0QXAVbOr+u/S0Z4L2vISGwJBetQl8fpSpdL2NLVmZM9xAoa1AZTY=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.7199903, - "plugin_hash": "1aee0b23a28d31b62a8863d1feff8a53e0a1221572cba160642ac18d10a8f52f", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/webui/config.json b/store/NebulaShell/webui/config.json deleted file mode 100644 index dffed56..0000000 --- a/store/NebulaShell/webui/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "title": { - "type": "string", - "name": "网站标题", - "description": "侧边栏和页面标题显示的文字", - "default": "NebulaShell", - "order": 1 - }, - "port": { - "type": "number", - "name": "端口号", - "description": "WebUI 监听端口", - "default": 8080, - "min": 1024, - "max": 65535, - "order": 2 - }, - "theme": { - "type": "select", - "name": "主题", - "description": "界面主题风格", - "default": "dark", - "options": [ - { "label": "深色", "value": "dark" }, - { "label": "浅色", "value": "light" } - ], - "order": 3 - } -} diff --git a/store/NebulaShell/webui/config/database.sql b/store/NebulaShell/webui/config/database.sql deleted file mode 100644 index e7835d1..0000000 --- a/store/NebulaShell/webui/config/database.sql +++ /dev/null @@ -1,49 +0,0 @@ --- NebulaShell WebUI 数据库初始化脚本 --- 此脚本创建基础表结构,其他插件可以添加自己的表 - -CREATE DATABASE IF NOT EXISTS nebulashell -CHARACTER SET utf8mb4 -COLLATE utf8mb4_unicode_ci; - -USE nebulashell; - --- 用户表 (示例) -CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - email VARCHAR(100) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_username (username), - INDEX idx_email (email) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- 插件配置表 -CREATE TABLE IF NOT EXISTS plugin_configs ( - id INT AUTO_INCREMENT PRIMARY KEY, - plugin_name VARCHAR(100) NOT NULL, - config_key VARCHAR(100) NOT NULL, - config_value TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY unique_plugin_config (plugin_name, config_key), - INDEX idx_plugin_name (plugin_name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- 系统日志表 -CREATE TABLE IF NOT EXISTS system_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - level VARCHAR(20) NOT NULL DEFAULT 'INFO', - message TEXT NOT NULL, - source VARCHAR(100), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_level (level), - INDEX idx_created_at (created_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- 插入默认配置 -INSERT IGNORE INTO plugin_configs (plugin_name, config_key, config_value) VALUES -('webui', 'theme', 'dark'), -('webui', 'title', 'NebulaShell'), -('webui', 'version', '1.0.0'); diff --git a/store/NebulaShell/webui/core/__init__.py b/store/NebulaShell/webui/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/webui/core/server.py b/store/NebulaShell/webui/core/server.py deleted file mode 100644 index 1069f6d..0000000 --- a/store/NebulaShell/webui/core/server.py +++ /dev/null @@ -1,66 +0,0 @@ -class WebUIServer: - def __init__(self, router, config: dict): - self.router = router - self.config = config - self.frontend_dir = Path(__file__).parent.parent / "frontend" - - self.pages = {} - self.nav_items = [] - - def start(self): - self.pages[path] = content_provider - if nav_item: - nav_item['url'] = path - self.nav_items.append(nav_item) - - self.router.get(path, lambda req: self._render_page(path, req)) - - def _render_page(self, path: str, request): - page_title = self.config.get("title", "NebulaShell") - - template_file = self.frontend_dir / "views" / "layout.html" - with open(template_file, 'r', encoding='utf-8') as f: - html_template = f.read() - - html = html_template.replace('{{ pageTitle }}', page_title) - html = html.replace('{{ navItems }}', nav_html) - html = html.replace('{{ content }}', content) - - return Response( - status=200, - headers={"Content-Type": "text/html; charset=utf-8"}, - body=html - ) - - def _default_home_content(self) -> str: - return """
-
-

欢迎使用 NebulaShell

-

一切皆为插件的轻量级框架

-
-
""" - - def _execute_php(self, php_file: str, variables: dict = None) -> str: - items = [] - for key, value in py_dict.items(): - if isinstance(value, str): - items.append(f"'{key}' => '{value.replace(chr(39), chr(92) + chr(39))}'") - elif isinstance(value, dict): - items.append(f"'{key}' => {self._php_array(value)}") - else: - items.append(f"'{key}' => {value}") - return "[" + ", ".join(items) + "]" - - def _php_array_list(self, py_list: list) -> str: - html = "" - return Response(status=200, headers={"Content-Type": "text/html; charset=utf-8"}, body=html) - - def _handle_tui_page(self, request): - pass - - def _handle_tui_css(self, request): - css = "" - return Response(status=200, headers={"Content-Type": "text/css"}, body=css) - - def _handle_tui_pages(self, request): - pass diff --git a/store/NebulaShell/webui/frontend/assets/css/main.css b/store/NebulaShell/webui/frontend/assets/css/main.css deleted file mode 100644 index 668ea1b..0000000 --- a/store/NebulaShell/webui/frontend/assets/css/main.css +++ /dev/null @@ -1,145 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background: #f5f5f5; - color: #333; - overflow: hidden; -} - -.app { - display: flex; - height: 100vh; -} - -/* Dock 侧边栏 */ -.sidebar { - width: 64px; - min-width: 64px; - background: #1a1a2e; - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 16px 0; - overflow: hidden; -} - -.sidebar-nav { - display: flex; - flex-direction: column; - gap: 8px; - padding: 0 12px; -} - -.nav-item { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - margin: 0 auto; - color: rgba(255, 255, 255, 0.7); - text-decoration: none; - border-radius: 10px; - transition: all 0.2s ease; - position: relative; -} - -.nav-item i { - font-size: 22px; -} - -.nav-item:hover { - background: rgba(255, 255, 255, 0.1); - color: #fff; -} - -.nav-item.active { - background: rgba(74, 144, 217, 0.2); - color: #4a90d9; -} - -.nav-item.active::before { - content: ''; - position: absolute; - left: -12px; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 20px; - background: #4a90d9; - border-radius: 0 2px 2px 0; -} - -/* Tooltip on hover */ -.nav-item:hover::after { - content: attr(title); - position: absolute; - left: 52px; - background: rgba(0, 0, 0, 0.8); - color: #fff; - padding: 6px 12px; - border-radius: 6px; - font-size: 13px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.2s; - z-index: 100; -} - -.nav-item:hover:hover::after { - opacity: 1; -} - -.sidebar-footer { - padding: 0 12px; -} - -.settings-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - margin: 0 auto; - background: rgba(255, 255, 255, 0.05); - border: none; - color: rgba(255, 255, 255, 0.7); - border-radius: 10px; - cursor: pointer; - transition: all 0.2s; -} - -.settings-btn i { - font-size: 22px; -} - -.settings-btn:hover { - background: rgba(255, 255, 255, 0.1); - color: #fff; -} - -/* 内容区 */ -.content { - flex: 1; - overflow-y: auto; - height: 100vh; -} - -.content-body { - min-height: 100%; -} - -.empty-state { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: #999; - font-size: 15px; -} diff --git a/store/NebulaShell/webui/frontend/assets/js/main.js b/store/NebulaShell/webui/frontend/assets/js/main.js deleted file mode 100644 index ff5e885..0000000 --- a/store/NebulaShell/webui/frontend/assets/js/main.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * NebulaShell WebUI 主脚本 - * 提供基础框架功能 - */ - -window.WebUI = { - /** - * 打开设置面板 - * 其他插件可以扩展此功能 - */ - openSettings: function() { - console.log('[WebUI] 打开设置面板'); - // 设置面板逻辑 - 其他插件可以扩展 - alert('设置功能需要其他插件支持'); - }, - - /** - * 注册导航项 - * 其他插件可以调用此方法添加导航 - */ - registerNavItem: function(item) { - console.log('[WebUI] 注册导航项:', item); - // 实际实现需要与后端通信 - }, - - /** - * 加载内容到主内容区 - * 其他插件可以调用此方法加载内容 - */ - loadContent: function(url) { - console.log('[WebUI] 加载内容:', url); - // 实际实现需要 AJAX 请求 - } -}; - -console.log('NebulaShell WebUI 框架已加载'); diff --git a/store/NebulaShell/webui/frontend/views/index.html b/store/NebulaShell/webui/frontend/views/index.html deleted file mode 100644 index 5e8e956..0000000 --- a/store/NebulaShell/webui/frontend/views/index.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - NebulaShell - 首页 - - - - - -
- - -
-
-
-
-

👋 欢迎使用 NebulaShell

-

一切皆为插件的轻量级框架

-
- -
-
-

插件化架构

-

所有功能皆可通过插件扩展,灵活定制您的系统

-
-
-

安全隔离

-

进程级沙箱保护,确保插件运行安全

-
-
-

多语言支持

-

内置国际化框架,支持全球多种语言

-
-
-

轻松部署

-

Docker 容器化部署,一键启动服务

-
-
-
-
-
-
- - - - diff --git a/store/NebulaShell/webui/frontend/views/layout.html b/store/NebulaShell/webui/frontend/views/layout.html deleted file mode 100644 index 7274d7f..0000000 --- a/store/NebulaShell/webui/frontend/views/layout.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - {{ pageTitle }} - - - - - -
- - -
-
- {{ content }} -
-
-
- - - - diff --git a/store/NebulaShell/webui/main.py b/store/NebulaShell/webui/main.py deleted file mode 100644 index f0ddea9..0000000 --- a/store/NebulaShell/webui/main.py +++ /dev/null @@ -1,67 +0,0 @@ -class WebUIPlugin: - def __init__(self): - self.http_api = None - self.server = None - self.tui = None - self.config = {} - - def meta(self): - from oss.plugin.types import Metadata, PluginConfig, Manifest - config = get_config() - return Manifest( - metadata=Metadata( - name="webui", - version="2.1.0", - author="NebulaShell", - description="Web 控制台容器 + TUI 双启动 - 供其他插件注册页面" - ), - config=PluginConfig( - enabled=True, - args={ - "port": config.get("HTTP_API_PORT", 8080), - "theme": "dark", - "title": "NebulaShell", - "tui_enabled": True } - ), - dependencies=["http-api"] - ) - - def set_http_api(self, http_api): - self.http_api = http_api - - def init(self, deps: dict = None): - if not self.http_api: - try: - from store.NebulaShell.plugin_bridge.main import use - self.http_api = use("http-api") - except Exception: - pass - if self.server: - self._setup_home_page() - - self.server.start() - Log.info("webui", f"WebUI 容器已启动:http://localhost:{self.config['port']}") - - if self.config.get("tui_enabled"): - Log.info("webui", "TUI 双启动中...") - - def _setup_home_page(self): - return Response( - status=302, - headers={"Location": "/dashboard", "Content-Type": "text/html"}, - body="" - ) - - def stop(self): - Log.error("webui", "WebUI 容器已停止") - - - def register_page(self, path: str, content_provider, nav_item: dict = None): - """其他插件调用此方法注册页面。""" - if self.server: - self.server.register_page(path, content_provider, nav_item) - else: - Log.warn("webui", f"警告:试图注册页面 {path},但服务器未初始化") - - def add_nav_item(self, item: dict): - pass diff --git a/store/NebulaShell/webui/manifest.json b/store/NebulaShell/webui/manifest.json deleted file mode 100644 index 0eddf22..0000000 --- a/store/NebulaShell/webui/manifest.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "metadata": { - "name": "webui", - "version": "2.1.0", - "author": "NebulaShell", - "description": "Web 控制台 + TUI 双启动 - 多语言支持/插件管理/安全配置/系统监控", - "type": "webui" - }, - "config": { - "enabled": true, - "args": { - "port": 8080, - "theme": "dark", - "title": "NebulaShell", - "language": "zh-CN", - "supported_languages": ["zh-CN", "en-US", "zh-TW", "ja-JP", "ko-KR", "fr-FR", "de-DE", "es-ES"], - "session_timeout": 3600, - "enable_2fa": false, - "show_plugins": true, - "show_security": true, - "show_deployments": true, - "tui_enabled": true - } - }, - "dependencies": ["http-api", "i18n"], - "permissions": ["*"], - "frontend": "php", - "database": { - "type": "mysql", - "name": "nebulashell", - "init_script": "config/database.sql" - } -} diff --git a/store/NebulaShell/webui/static/__init__.py b/store/NebulaShell/webui/static/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/webui/static/assets.py b/store/NebulaShell/webui/static/assets.py deleted file mode 100644 index 991bbc1..0000000 --- a/store/NebulaShell/webui/static/assets.py +++ /dev/null @@ -1,8 +0,0 @@ -class Assets: - @staticmethod - def get_css() -> str: - return "" - - @staticmethod - def get_js() -> str: - return diff --git a/store/NebulaShell/webui/templates/__init__.py b/store/NebulaShell/webui/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/webui/templates/layout.py b/store/NebulaShell/webui/templates/layout.py deleted file mode 100644 index 410803e..0000000 --- a/store/NebulaShell/webui/templates/layout.py +++ /dev/null @@ -1,42 +0,0 @@ -class Layout: - def __init__(self, config: dict): - self.config = config - - def render(self) -> str: - return """ - - - - {self.config.get('title', 'NebulaShell')} - - - -
- -
-
-

欢迎使用 NebulaShell

-
-
-
-

暂无内容

-
-
-
-
- - -""" diff --git a/store/NebulaShell/webui/tui/README.md b/store/NebulaShell/webui/tui/README.md deleted file mode 100644 index d4d21ec..0000000 --- a/store/NebulaShell/webui/tui/README.md +++ /dev/null @@ -1,187 +0,0 @@ -# NebulaShell TUI - 终端用户界面 - -## 概述 - -TUI(Terminal User Interface)插件为 NebulaShell 提供终端界面,与 WebUI 双启动运行。 - -## 核心特性 - -### 1. 转换层架构 - -TUI 本身只有一个转换层,它只会访问 WebUI 所开放的 `/tui` 接口: - -- **`/tui/index.html`** - TUI 入口页面 -- **`/tui/page`** - 获取任意页面的 TUI 版本 -- **`/tui/css`** - 终端兼容的 CSS -- **`/tui/interact`** - 处理交互事件 - -### 2. HTML 标记规范 - -WebUI 开放的 `.html` 文件中不含有任何给用户看的内容,但包含 TUI 可解析的特殊标记: - -```html - - - - - -
- - -[1] 首页 - - - - - - -``` - -### 3. 支持的 CSS 属性 - -TUI 只支持终端能够渲染的样式: - -| CSS 属性 | TUI 转换 | 说明 | -|---------|---------|------| -| `font-weight: bold` | ANSI 加粗 | `\x1b[1m` | -| `font-style: italic` | ANSI 斜体 | `\x1b[3m` | -| `text-decoration: underline` | ANSI 下划线 | `\x1b[4m` | -| `background-color` | ANSI 背景色 | 仅支持基础 8 色 | -| `color` | ANSI 前景色 | 仅支持基础 8 色 | -| `text-align` | 文本对齐 | left/center/right | - -### 4. 支持的 JS 交互 - -TUI 只支持基础的终端交互: - -- **鼠标位置** - 通过 ANSI 鼠标协议获取 -- **点击事件** - 转换为选择操作 -- **按键输入** - 完整的键盘支持 - -```javascript -// TUI 配置中的键盘映射 -{ - "keyboard": { - "1": {"action": "navigate", "target": "/"}, - "ArrowUp": {"action": "navigate_up"}, - "Enter": {"action": "select"}, - "q": {"action": "quit"} - } -} -``` - -## 文件结构 - -``` -webui/tui/ -├── __init__.py # 包初始化 -├── main.py # TUI 插件主程序 -├── converter.py # HTML 到 TUI 转换层 -├── index.html # TUI 入口页面(含特殊标记) -├── manifest.json # 插件清单 -└── README.md # 本文档 -``` - -## 使用方式 - -### 启动 NebulaShell - -```bash -# 正常启动,WebUI 和 TUI 会同时运行 -python main.py serve - -# 或通过 CLI -python -m oss.cli serve -``` - -### TUI 快捷键 - -| 按键 | 功能 | -|-----|------| -| `1` | 首页 | -| `2` | 仪表盘 | -| `3` | 日志 | -| `4` | 终端 | -| `5` | 插件 | -| `6` | 设置 | -| `r` | 刷新 | -| `h` | 帮助 | -| `↑/↓` | 上下导航 | -| `Enter` | 确认 | -| `q` | 退出 TUI | - -## 开发指南 - -### 创建 TUI 兼容页面 - -1. 在 WebUI 插件中创建页面时,添加 TUI 标记 -2. 使用 `data-tui-*` 属性定义交互行为 -3. 在 ` - - - ''', - nav_item={'icon': 'ri-star-line', 'text': '我的页面'} - ) -``` - -## 技术细节 - -### 转换流程 - -1. TUI 插件启动时访问 `/tui/index.html` -2. `HTMLToTUIConverter` 解析 HTML 提取: - - 文本内容 - - 按钮和链接 - - TUI 配置(键盘映射、样式) -3. `TUIRenderer` 将元素渲染为 ANSI 转义序列 -4. `TUICanvas` 管理终端显示缓冲区 -5. `TUIInputHandler` 处理键盘/鼠标输入 - -### ANSI 颜色映射 - -```python -COLOR_MAP = { - '#000000': '\x1b[30m', # black - '#ff0000': '\x1b[31m', # red - '#00ff00': '\x1b[32m', # green - '#ffff00': '\x1b[33m', # yellow - '#0000ff': '\x1b[34m', # blue - '#ff00ff': '\x1b[35m', # magenta - '#00ffff': '\x1b[36m', # cyan - '#ffffff': '\x1b[37m', # white -} -``` - -## 许可证 - -MIT License - NebulaShell Project diff --git a/store/NebulaShell/webui/tui/__init__.py b/store/NebulaShell/webui/tui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/store/NebulaShell/webui/tui/converter.py b/store/NebulaShell/webui/tui/converter.py deleted file mode 100644 index df2d08c..0000000 --- a/store/NebulaShell/webui/tui/converter.py +++ /dev/null @@ -1,481 +0,0 @@ -import re -import json -import html -from pathlib import Path -from typing import Dict, List, Any, Optional, Callable, Tuple -from dataclasses import dataclass, field -from enum import Enum -import os -import sys - - -class TUIElementType(Enum): - RESET = '\x1b[0m' - BOLD = '\x1b[1m' - DIM = '\x1b[2m' - ITALIC = '\x1b[3m' - UNDERLINE = '\x1b[4m' - BLINK = '\x1b[5m' - REVERSE = '\x1b[7m' - HIDDEN = '\x1b[8m' - - FG_BLACK = '\x1b[30m' - FG_RED = '\x1b[31m' - FG_GREEN = '\x1b[32m' - FG_YELLOW = '\x1b[33m' - FG_BLUE = '\x1b[34m' - FG_MAGENTA = '\x1b[35m' - FG_CYAN = '\x1b[36m' - FG_WHITE = '\x1b[37m' - FG_DEFAULT = '\x1b[39m' - - BG_BLACK = '\x1b[40m' - BG_RED = '\x1b[41m' - BG_GREEN = '\x1b[42m' - BG_YELLOW = '\x1b[43m' - BG_BLUE = '\x1b[44m' - BG_MAGENTA = '\x1b[45m' - BG_CYAN = '\x1b[46m' - BG_WHITE = '\x1b[47m' - BG_DEFAULT = '\x1b[49m' - - @staticmethod - def fg_256(color: int) -> str: - return f'\x1b[38;5;{color}m' - - @staticmethod - def bg_256(color: int) -> str: - return f'\x1b[48;5;{color}m' - - -class BorderStyle: - fg_color: str = "" - bg_color: str = "" - bold: bool = False - dim: bool = False - underline: bool = False - italic: bool = False - reverse: bool = False - - def apply(self, text: str) -> str: - return text - - -@dataclass -class TUIElement: - id: str = "" - element_type: TUIElementType = TUIElementType.CONTAINER - classes: List[str] = field(default_factory=list) - text: str = "" - x: int = 0 - y: int = 0 - width: int = 80 - height: int = 1 - style: TUIStyle = field(default_factory=TUIStyle) - children: List['TUIElement'] = field(default_factory=list) - attributes: Dict[str, Any] = field(default_factory=dict) - parent: Optional['TUIElement'] = None - - def render(self) -> str: - return (self.x, self.y, self.width, self.height) - - -@dataclass -class TUIButton(TUIElement): - alignment: str = "left" - def render(self) -> str: - text = self.style.apply(self.text) - - if self.alignment == "center": - padding = (self.width - len(self.text)) // 2 - text = " " * padding + text - elif self.alignment == "right": - padding = self.width - len(self.text) - text = " " * padding + text - - remaining = self.width - len(self.text) - if remaining > 0 and self.alignment == "left": - text += " " * remaining - - return text - - -@dataclass -class TUIPanel(TUIElement): - layout_type: str = "vertical" - gap: int = 1 - - def render(self, width: int = 80, height: int = 24) -> str: - if self.layout_type == "vertical": - rendered = [] - for i, child in enumerate(self.children): - child.y = self.y + sum(len(r.render().split('\n')) for r in rendered) + (i * self.gap) - rendered.append(child) - return "\n".join(el.render() for el in rendered) - - elif self.layout_type == "horizontal": - rendered = [] - current_x = self.x - for child in self.children: - child.x = current_x - rendered.append(child) - current_x += child.width + self.gap - return " ".join(el.render() for el in rendered) - - else: - return "\n".join(el.render() for el in self.children) - - -@dataclass -class TUIList(TUIElement): - char: str = "─" - - def render(self) -> str: - return self.char * self.width - - -@dataclass -class TUIProgressBar(TUIElement): - frames: List[str] = field(default_factory=lambda: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) - current_frame: int = 0 - - def render(self) -> str: - frame = self.frames[self.current_frame % len(self.frames)] - return f"{frame} {self.text}" - - def next_frame(self): - self.current_frame += 1 - - -class HTMLToTUIConverter: - - COLOR_MAP = { - ' ' ' ' ' ' ' ' ' ' 'black': ANSIStyle.FG_BLACK, - 'blue': ANSIStyle.FG_BLUE, - 'green': ANSIStyle.FG_GREEN, - 'cyan': ANSIStyle.FG_CYAN, - 'red': ANSIStyle.FG_RED, - 'magenta': ANSIStyle.FG_MAGENTA, - 'yellow': ANSIStyle.FG_YELLOW, - 'white': ANSIStyle.FG_WHITE, - 'gray': ANSIStyle.DIM, - 'grey': ANSIStyle.DIM, - } - - BG_COLOR_MAP = { - ' ' ' ' ' ' ' ' 'black': ANSIStyle.BG_BLACK, - 'blue': ANSIStyle.BG_BLUE, - 'green': ANSIStyle.BG_GREEN, - 'cyan': ANSIStyle.BG_CYAN, - 'red': ANSIStyle.BG_RED, - 'magenta': ANSIStyle.BG_MAGENTA, - 'yellow': ANSIStyle.BG_YELLOW, - 'white': ANSIStyle.BG_WHITE, - } - - def __init__(self, width: int = 80, height: int = 24): - self.width = width - self.height = height - self.keyboard_bindings: Dict[str, Dict] = {} - self.mouse_handlers: Dict[str, Callable] = {} - self.css_styles: Dict[str, TUIStyle] = {} - - def parse(self, html_content: str) -> TUILayout: - for match in re.finditer(r']*type=["\']application/x-tui-config["\'][^>]*>(.*?)', html, re.DOTALL): - try: - config = json.loads(match.group(1).strip()) - if 'keyboard' in config: - self.keyboard_bindings = config['keyboard'] - except json.JSONDecodeError: - pass - return html - - def _parse_tui_config(self, html: str): - for match in re.finditer(r']*type=["\']text/x-tui-css["\'][^>]*>(.*?)', html, re.DOTALL): - css = match.group(1) - for rule_match in re.finditer(r'([.\w#\s>:\[\]()=~|$^*]+)\s*\{([^}]*)\}', css): - selector = rule_match.group(1) - properties = rule_match.group(2) - style = self._parse_css_properties(properties) - self.css_styles[selector] = style - - def _parse_css_properties(self, css_text: str) -> TUIStyle: - elements = [] - - for match in re.finditer(r'<(\w+)([^>]*)>(.*?)', html, re.DOTALL): - tag = match.group(1) - attrs_str = match.group(2) - content = match.group(3) - - attrs = self._parse_attributes(attrs_str) - - if 'data-tui-type' in attrs or self._is_tui_element(tag, attrs): - element = self._create_tui_element(tag, attrs, content) - if element: - elements.append(element) - - for match in re.finditer(r'<(\w+)([^/]*)/>', html): - tag = match.group(1) - attrs_str = match.group(2) - attrs = self._parse_attributes(attrs_str) - - if 'data-tui-type' in attrs or self._is_tui_element(tag, attrs): - element = self._create_tui_element(tag, attrs, "") - if element: - elements.append(element) - - return elements - - def _parse_attributes(self, attrs_str: str) -> Dict[str, Any]: - tui_tags = ['header', 'footer', 'nav', 'section', 'article', 'aside', 'main'] - tui_attrs = ['data-tui-type', 'data-tui-action', 'data-tui-key', 'data-tui-layout'] - - return tag in tui_tags or any(attr in attrs for attr in tui_attrs) - - def _create_tui_element(self, tag: str, attrs: Dict, content: str) -> Optional[TUIElement]: - style = TUIStyle() - - classes = attrs.get('class', '').split() - for cls in classes: - selector = f".{cls}" - if selector in self.css_styles: - base_style = self.css_styles[selector] - style.fg_color = base_style.fg_color or style.fg_color - style.bg_color = base_style.bg_color or style.bg_color - style.bold = style.bold or base_style.bold - style.dim = style.dim or base_style.dim - style.underline = style.underline or base_style.underline - - tui_style = attrs.get('data-tui-style', '') - if 'bold' in tui_style: - style.bold = True - if 'dim' in tui_style: - style.dim = True - if 'underline' in tui_style: - style.underline = True - if 'reverse' in tui_style: - style.reverse = True - - return style - - def _extract_nav(self, html: str) -> List[TUIElement]: - elements = [] - - for match in re.finditer(r']*>(.*?)', html, re.DOTALL | re.IGNORECASE): - attrs_str = match.group(0) - text = re.sub(r'<[^>]+>', '', match.group(1)).strip() - text = html.unescape(text) - - onclick = "" - onclick_match = re.search(r'onclick=["\']([^"\']*)["\']', attrs_str) - if onclick_match: - onclick = onclick_match.group(1) - - btn = TUIButton( - text=text or "Button", - action=onclick, - width=self.width - ) - elements.append(btn) - - return elements - - def get_keyboard_bindings(self) -> Dict[str, Dict]: - return self.keyboard_bindings - - def __init__(self, width: int = 80, height: int = 24): - raise NotImplementedError("Use HTMLToTUIConverter instead") - - -class TUIRenderer: - def __init__(self, width: int = 80, height: int = 24): - self.converter = HTMLToTUIConverter(width, height) - self.screen_buffer: List[List[str]] = [] - - def render(self, html: str) -> str: - self._init_buffer() - self._render_element(layout, 0, 0) - return self._buffer_to_string() - - def _init_buffer(self): - rendered = element.render() - lines = rendered.split('\n') - - for i, line in enumerate(lines): - if y + i >= self.height: - break - - clean_line = re.sub(r'\x1b\[[0-9;]*m', '', line) - - for j, char in enumerate(line): - if x + j >= self.width: - break - self.screen_buffer[y + i][x + j] = char - - def _buffer_to_string(self) -> str: - content = self.render(html) - lines = content.split('\n') - - max_content_width = max(len(re.sub(r'\x1b\[[0-9;]*m', '', line)) for line in lines) if lines else 0 - frame_width = min(max_content_width + 2, self.width) - - result = [] - - top = "╔" + "═" * (frame_width - 2) + "╗" - if title: - title_text = f" {title} " - padding = (frame_width - 2 - len(title_text)) // 2 - top = "╔" + "═" * padding + title_text + "═" * (frame_width - 2 - padding - len(title_text)) + "╗" - result.append(top) - - for line in lines: - clean_len = len(re.sub(r'\x1b\[[0-9;]*m', '', line)) - padding = frame_width - 2 - clean_len - if padding > 0: - line = line + " " * padding - result.append(f"║ {line} ║") - - result.append("╚" + "═" * (frame_width - 2) + "╝") - - return '\n'.join(result) - - -class TUIInputHandler: - - def __init__(self): - self.key_bindings: Dict[str, Callable] = {} - self.mouse_handlers: Dict[str, Callable] = {} - self.mouse_x = 0 - self.mouse_y = 0 - self.running = True - - def bind_key(self, key: str, handler: Callable): - self.mouse_handlers[event] = handler - - def handle_key(self, key: str) -> bool: - self.mouse_x = x - self.mouse_y = y - - handler_key = f"{button}" - if handler_key in self.mouse_handlers: - self.mouse_handlers[handler_key](x, y) - return True - return False - - def read_key(self) -> str: - return "" - -class TUICanvas: - def __init__(self, width: int = 80, height: int = 24): - self.width = width - self.height = height - self.buffer = [[' ' for _ in range(width)] for _ in range(height)] - self.renderer = TUIRenderer(width, height) - - def clear(self): - if style: - text = style.apply(text) - - lines = text.split('\n') - for i, line in enumerate(lines): - if y + i >= self.height: - break - for j, char in enumerate(line): - if x + j >= self.width: - break - self.buffer[y + i][x + j] = char - - def draw_box(self, x: int, y: int, width: int, height: int, style: str = "single"): - return '\n'.join(''.join(row) for row in self.buffer) - - def display(self): - pass - - -class TUIEventManager: - def __init__(self): - self.events: Dict[str, List[Callable]] = {} - - def on(self, event: str, handler: Callable): - if event in self.events: - for handler in self.events[event]: - handler(*args, **kwargs) - - -class TUIManager: - - _instance: Optional['TUIManager'] = None - - def __init__(self, width: int = 80, height: int = 24): - self.width = width - self.height = height - self.canvas = TUICanvas(width, height) - self.renderer = TUIRenderer(width, height) - self.converter = HTMLToTUIConverter(width, height) - self.input_handler = TUIInputHandler() - self.event_manager = TUIEventManager() - - self.pages: Dict[str, str] = {} - self.current_page = "" - self.running = False - self.selected_index = 0 - self.nav_items: List[Dict] = [] - - @classmethod - def get_instance(cls, width: int = 80, height: int = 24) -> 'TUIManager': - self.pages[path] = html_content - self.current_page = path - - def navigate(self, path: str): - if not self.current_page or self.current_page not in self.pages: - return - - html = self.pages[self.current_page] - output = self.renderer.render_with_frame(html, title=f"NebulaShell - {self.current_page}") - - self.canvas.clear() - self.canvas.draw_text(output, 0, 0) - self.canvas.display() - - def show_error(self, message: str): - error_html = f""" - -

错误

-

{message}

-

按任意键返回

- - """ - self.load_page("/error", error_html) - self.render_current() - - def setup_default_bindings(self): - if self.current_page not in self.pages: - return - - html = self.pages[self.current_page] - converter = HTMLToTUIConverter(self.width, self.height) - converter.parse(html) - - for key, config in converter.get_keyboard_bindings().items(): - action = config.get('action', '') - target = config.get('target', '') - - if action == 'navigate' and target: - self.input_handler.bind_key(key, lambda t=target: self.navigate(t)) - elif action == 'quit': - self.input_handler.bind_key(key, self.quit) - elif action == 'refresh': - self.input_handler.bind_key(key, self.render_current) - - def run_event_loop(self): - self.running = False - - def start(self): - pass - - -def create_tui_manager(width: int = 80, height: int = 24): - global _tui_manager_instance - if _tui_manager_instance is None: - _tui_manager_instance = TUIManager(width, height) - return _tui_manager_instance diff --git a/store/NebulaShell/webui/tui/index.html b/store/NebulaShell/webui/tui/index.html deleted file mode 100644 index e0b2df7..0000000 --- a/store/NebulaShell/webui/tui/index.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - NebulaShell TUI - - - - - -
- -
- NebulaShell TUI - 终端用户界面 -
- - -
- WebUI: http://localhost:8080 | TUI: 双启动模式 -
- - - - - -
- - -
-

快捷键说明:

-
    -
  • q - 退出 TUI
  • -
  • r - 刷新当前页
  • -
  • h - 显示帮助
  • -
  • ↑/↓ - 上下导航
  • -
  • Enter - 确认选择
  • -
-
-
- - - - - - - - diff --git a/store/NebulaShell/webui/tui/main.py b/store/NebulaShell/webui/tui/main.py deleted file mode 100644 index 091baa0..0000000 --- a/store/NebulaShell/webui/tui/main.py +++ /dev/null @@ -1,95 +0,0 @@ -class TUIPlugin: - def __init__(self): - self.webui = None - self.http_api = None - self.tui_manager = None - self.running = False - self.tui_thread = None - - def meta(self): - from oss.plugin.types import Metadata, PluginConfig, Manifest - return Manifest( - metadata=Metadata( - name="tui", - version="1.0.0", - author="NebulaShell", - description="终端用户界面 - 与 WebUI 双启动" - ), - config=PluginConfig(enabled=True, args={}), - dependencies=["http-api", "webui"] - ) - - def set_webui(self, webui): - self.webui = webui - - def set_http_api(self, http_api): - self.http_api = http_api - - def init(self, deps: dict = None): - if not self.webui or not self.http_api: - try: - from store.NebulaShell.plugin_bridge.main import use - if not self.webui: self.webui = use("webui") - if not self.http_api: self.http_api = use("http-api") - except Exception: - pass - default_pages = ["/", "/dashboard", "/logs", "/terminal"] - - for path in default_pages: - try: - html = self._fetch_webui_page(path) - if html: - self.tui_manager.load_page(path, html) - Log.info("tui", f"已加载页面:{path}") - except Exception as e: - Log.warn("tui", f"加载页面 {path} 失败:{e}") - - def _fetch_webui_page(self, path: str) -> str: - Log.info("tui", "TUI 启动中...") - self.running = True - - self.tui_thread = threading.Thread(target=self._tui_loop, daemon=True) - self.tui_thread.start() - - Log.ok("tui", "TUI 已启动(后台模式)") - Log.info("tui", "提示:按 'q' 退出 TUI,WebUI 仍在运行") - - def _tui_loop(self): - pass - - def _handle_tui_page(self, request): - css = """/* TUI 兼容 CSS */ -.tui-page { - background-color: transparent; - color: inherit; -} -.tui-body { - font-family: monospace; - font-weight: normal; -} -.bold { font-weight: bold; } -.underline { text-decoration: underline; } -[data-tui-action] { - cursor: pointer; -}""" - return Response( - status=200, - headers={"Content-Type": "text/css"}, - body=css - ) - - def _handle_tui_interact(self, request): - Log.info("tui", "TUI 停止中...") - self.running = False - - if self.tui_thread: - self.tui_thread.join(timeout=2) - - Log.ok("tui", "TUI 已停止") - - -register_plugin_type("TUIPlugin", TUIPlugin) - - -def New(): - return TUIPlugin() diff --git a/store/NebulaShell/webui/tui/manifest.json b/store/NebulaShell/webui/tui/manifest.json deleted file mode 100644 index 3605d3d..0000000 --- a/store/NebulaShell/webui/tui/manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "metadata": { - "name": "tui", - "version": "1.0.0", - "author": "NebulaShell", - "description": "终端用户界面 - 与 WebUI 双启动,通过访问 /tui 接口获取 HTML 并转换为终端显示", - "type": "tui" - }, - "config": { - "enabled": true, - "args": { - "width": 80, - "height": 24, - "theme": "dark", - "enable_mouse": false, - "keyboard_shortcuts": { - "1": "/", - "2": "/dashboard", - "3": "/logs", - "4": "/terminal", - "q": "quit", - "r": "refresh" - } - } - }, - "dependencies": ["http-api", "webui"], - "permissions": ["read:pages", "execute:commands"] -} diff --git a/store/NebulaShell/ws-api/README.md b/store/NebulaShell/ws-api/README.md deleted file mode 100644 index a136378..0000000 --- a/store/NebulaShell/ws-api/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# ws-api WebSocket API - -提供 WebSocket 实时双向通信服务。 - -## 功能 - -- WebSocket 服务器 -- 路由匹配 -- 中间件链(认证/日志) -- 广播消息 -- 连接/断开/消息事件 -- 与 HTTP 插件集成 - -## 使用 - -```python -ws = plugin_mgr.get("ws-api") - -# 注册消息路由 -ws.router.on_message("/chat", lambda client, msg: client.send({"echo": msg})) - -# 广播 -ws.server.broadcast({"type": "announcement", "data": "服务器维护通知"}) - -# 获取客户端列表 -clients = ws.server.get_clients() -``` - -## 事件 - -```python -# 通过 plugin-bridge 订阅 WS 事件 -bridge = plugin_mgr.get("plugin-bridge") -bridge.event_bus.on("ws.connect", lambda event: print(f"新连接: {event.client.path}")) -bridge.event_bus.on("ws.message", lambda event: print(f"消息: {event.message}")) -bridge.event_bus.on("ws.disconnect", lambda event: print(f"断开: {event.client.id}")) -``` - -## 配置 - -```json -{ - "config": { - "args": { - "host": "0.0.0.0", - "port": 8081 - } - } -} -``` diff --git a/store/NebulaShell/ws-api/SIGNATURE b/store/NebulaShell/ws-api/SIGNATURE deleted file mode 100644 index eb2ce20..0000000 --- a/store/NebulaShell/ws-api/SIGNATURE +++ /dev/null @@ -1,8 +0,0 @@ -{ - "signature": "jCGTXt+uLD/djjYQIxogWXFztNVz4Y/Xyzsj3JaXtvrvNppdUm7Ei8/N08l+dZxqU7Lpjg2UIMakX2jVXSE2rlZlTZyL5cfvxr3j5mdDXP4VhXScdVwEaeScDz0qUn23e6qOqEdS+KbjK6pZCk34GjwBoV2/ze8DteK+whSMCSwqxYASHNTTrNhELKoDhTBnnlCEAAlEM6y95EIpdL6FsvJRP8w8xgg1Ah2gRvQdyC1785zbGdFJ3Oib6mw1fuKaV90Phqb8qCjD8qePqocdSArNJ/Jz+073lVH0IZMUw45cIF1uBgpXErnJrnY2KWtSLpC9AGK62vNLMjTYnKa7HZ0J2xwmg89skW2w3YSaiZweijelFgut7tmgdof52Xvb6hdnzNWTAKorT6C8d6jZWzNv0BrJKGtThzCoyBhTQOSiEfgz+QO2yFpbWxCtMjX1SfVEOaWWJq5H5fgTu6YqCJlwSU3ur3pdBLaBNYH3PMYW4aIOUs5mOzpnFBq1FTJmfz9BT9ZEIK/7bbVjCfifMMH1Xkq2gudQDfElok7WZQ3CaHNzms4wbwbS6yIzzRggDo1s7tTXR0i5AazoUOCKj6cxTqwPnBiO19J0Okjhj8TkApbNHAoezcgYHuq1HxxZ/ckGxs/5yVuN0qkBGESwfWPRmXd/CS8ddhIowSOds9c=", - "signer": "NebulaShell", - "algorithm": "RSA-SHA256", - "timestamp": 1775964952.9217672, - "plugin_hash": "b056cef4d2a2aceeeb199ef47fb709b021f9d2a7198109fa4023e3f2ded4b108", - "author": "NebulaShell" -} \ No newline at end of file diff --git a/store/NebulaShell/ws-api/events.py b/store/NebulaShell/ws-api/events.py deleted file mode 100644 index 0fc1d92..0000000 --- a/store/NebulaShell/ws-api/events.py +++ /dev/null @@ -1,15 +0,0 @@ -class WsEvent: - type: str - client: Any = None - path: str = "" - message: str = "" - context: dict[str, Any] = field(default_factory=dict) - - -EVENT_CONNECT = "ws.connect" -EVENT_DISCONNECT = "ws.disconnect" -EVENT_MESSAGE = "ws.message" -EVENT_ERROR = "ws.error" -EVENT_SUBSCRIBE = "ws.subscribe" -EVENT_UNSUBSCRIBE = "ws.unsubscribe" -EVENT_BROADCAST = "ws.broadcast" diff --git a/store/NebulaShell/ws-api/main.py b/store/NebulaShell/ws-api/main.py deleted file mode 100644 index d9a7227..0000000 --- a/store/NebulaShell/ws-api/main.py +++ /dev/null @@ -1,10 +0,0 @@ -class WsApiPlugin: - def __init__(self): - self._running = False - - def init(self, deps: dict = None): - self._running = True - Log.info("ws-api", "已启动") - - def stop(self): - pass diff --git a/store/NebulaShell/ws-api/manifest.json b/store/NebulaShell/ws-api/manifest.json deleted file mode 100644 index c9fd2b8..0000000 --- a/store/NebulaShell/ws-api/manifest.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "metadata": { - "name": "ws-api", - "version": "1.1.0", - "author": "NebulaShell", - "description": "WebSocket API 服务 - 实时双向通信/多语言支持/安全认证", - "type": "protocol" - }, - "config": { - "enabled": true, - "args": { - "host": "0.0.0.0", - "port": 8081, - "ssl_enabled": false, - "heartbeat_interval": 30, - "max_connections": 1000, - "auth_enabled": true - } - }, - "dependencies": ["i18n"], - "permissions": ["lifecycle"] -} diff --git a/store/NebulaShell/ws-api/middleware.py b/store/NebulaShell/ws-api/middleware.py deleted file mode 100644 index 0aaa15f..0000000 --- a/store/NebulaShell/ws-api/middleware.py +++ /dev/null @@ -1,14 +0,0 @@ -class WsMiddleware: - async def process(self, client: Any, message: str, next_fn: Callable) -> Optional[str]: - pass - - -class WsMiddlewareChain: - def __init__(self): - self.middlewares: list[WsMiddleware] = [] - - def add(self, middleware: WsMiddleware): - self.middlewares.append(middleware) - - async def run(self, client, message) -> Optional[str]: - pass diff --git a/store/NebulaShell/ws-api/router.py b/store/NebulaShell/ws-api/router.py deleted file mode 100644 index 3eb6d76..0000000 --- a/store/NebulaShell/ws-api/router.py +++ /dev/null @@ -1,15 +0,0 @@ -class WsRoute: - def __init__(self, path: str, handler: Callable): - self.path = path - self.handler = handler - - -class WsRouter: - def __init__(self): - self.routes: dict[str, WsRoute] = {} - - def add(self, path: str, handler: Callable): - self.routes[path] = WsRoute(path, handler) - - async def handle(self, client: WsClient, path: str, message: str): - pass diff --git a/store/NebulaShell/ws-api/server.py b/store/NebulaShell/ws-api/server.py deleted file mode 100644 index 54c7a05..0000000 --- a/store/NebulaShell/ws-api/server.py +++ /dev/null @@ -1,67 +0,0 @@ -class WsClient: - def __init__(self, websocket, path: str): - self.websocket = websocket - self.path = path - self.id = id(websocket) - self.closed = False - - async def send(self, message: Any): - self.closed = True - await self.websocket.close() - - -class WsServer: - def __init__(self): - self._loop = asyncio.new_event_loop() - self._thread = threading.Thread(target=self._run_loop, daemon=True) - self._thread.start() - - async def _run_loop(self): - if path is None: - try: - path = websocket.request.path - except AttributeError: - path = "/" - - client = WsClient(websocket, path) - self._clients[client.id] = client - - self.event_bus.emit(WsEvent( - type=EVENT_CONNECT, - client=client, - path=path - )) - - try: - async for message in websocket: - self.event_bus.emit(WsEvent( - type=EVENT_MESSAGE, - client=client, - path=path, - message=message - )) - - await self.router.handle(client, path, message) - - except websockets.exceptions.ConnectionClosed: - pass - finally: - del self._clients[client.id] - self.event_bus.emit(WsEvent( - type=EVENT_DISCONNECT, - client=client, - path=path - )) - - def stop(self): - async def _broadcast(): - for client_id, client in self._clients.items(): - if exclude_client and client_id == exclude_client: - continue - await client.send(message) - - if self._loop and self._loop.is_running(): - asyncio.run_coroutine_threadsafe(_broadcast(), self._loop) - - def get_clients(self) -> list[WsClient]: - pass diff --git a/tests/test_nbpf.py b/tests/test_nbpf.py new file mode 100644 index 0000000..16279a3 --- /dev/null +++ b/tests/test_nbpf.py @@ -0,0 +1,481 @@ +"""NBPF 模块测试""" +import sys +import os +import json +import tempfile +import shutil +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from oss.core.nbpf import ( + NBPCrypto, NBPCryptoError, + NIRCompiler, NIRCompileError, + NBPFPacker, NBPFUnpacker, NBPFFormatter, + NBPFLoader, NBPFLoadError, +) + + +# ═══════════════════════════════════════════════════════════════ +# 辅助函数 +# ═══════════════════════════════════════════════════════════════ + +def _create_test_plugin(tmp_dir: Path) -> Path: + """创建测试插件目录""" + plugin_dir = tmp_dir / "test-plugin" + plugin_dir.mkdir(parents=True) + + # manifest.json + manifest = { + "metadata": { + "name": "test-plugin", + "version": "1.0.0", + "author": "test", + "description": "测试插件", + }, + "dependencies": [], + } + (plugin_dir / "manifest.json").write_text(json.dumps(manifest, indent=2)) + + # main.py + main_py = '''"""测试插件""" +class New: + def __init__(self): + self.name = "test-plugin" + self.started = False + + def start(self): + self.started = True + return True + + def stop(self): + self.started = False + return True + + def greet(self, name: str) -> str: + return f"Hello, {name}!" +''' + (plugin_dir / "main.py").write_text(main_py) + + return plugin_dir + + +def _generate_keys(tmp_dir: Path) -> dict: + """生成测试密钥,返回路径字典""" + keys = {} + + # Ed25519 + ed25519_private, ed25519_public = NBPCrypto.generate_ed25519_keypair() + keys["ed25519_private"] = ed25519_private + keys["ed25519_public"] = ed25519_public + + # RSA + rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=2048) # 测试用 2048 + keys["rsa_private"] = rsa_private + keys["rsa_public"] = rsa_public + + return keys + + +# ═══════════════════════════════════════════════════════════════ +# 测试:NBPCrypto +# ═══════════════════════════════════════════════════════════════ + +def test_crypto_aes_encrypt_decrypt(): + """测试 AES-256-GCM 加密解密""" + data = b"Hello, NebulaShell!" + key = NBPCrypto.generate_aes_key() + assert len(key) == 32 + + nonce, ct, tag = NBPCrypto._aes_encrypt(data, key) + assert len(nonce) == 12 + assert len(tag) == 16 + assert isinstance(ct, bytes) + + dec = NBPCrypto._aes_decrypt(ct, key, nonce, tag) + assert dec == data + + +def test_crypto_outer_encrypt_decrypt(): + """测试外层加密解密""" + data = b"Secret plugin data" + key = NBPCrypto.generate_aes_key() + + enc = NBPCrypto.outer_encrypt(data, key) + assert isinstance(enc, dict) + + dec = NBPCrypto.outer_decrypt(enc, key) + assert dec == data + + +def test_crypto_inner_encrypt_decrypt(): + """测试中层加密解密""" + data = b"NIR compiled data" + key = NBPCrypto.generate_aes_key() + + enc = NBPCrypto.inner_encrypt(data, key) + assert isinstance(enc, dict) + + dec = NBPCrypto.inner_decrypt(enc, key) + assert dec == data + + +def test_crypto_ed25519_sign_verify(): + """测试 Ed25519 签名验证""" + data = b"Package content hash" + private, public = NBPCrypto.generate_ed25519_keypair() + + signature = NBPCrypto.outer_sign(data, private) + assert isinstance(signature, bytes) + + assert NBPCrypto.outer_verify(data, signature, public) is True + assert NBPCrypto.outer_verify(b"tampered", signature, public) is False + + +def test_crypto_rsa_sign_verify(): + """测试 RSA-PSS 签名验证""" + data = b"NIR content hash" + private, public = NBPCrypto.generate_rsa_keypair(key_size=2048) + + signature = NBPCrypto.inner_sign(data, private) + assert isinstance(signature, bytes) + + assert NBPCrypto.inner_verify(data, signature, public) is True + assert NBPCrypto.inner_verify(b"tampered", signature, public) is False + + +def test_crypto_hmac_sign_verify(): + """测试 HMAC-SHA256 签名验证""" + data = b"Module content" + key1 = NBPCrypto.generate_aes_key() + key2 = NBPCrypto.generate_aes_key() + hmac_key = NBPCrypto.derive_hmac_key(key1, key2) + + signature = NBPCrypto.module_sign(data, hmac_key) + assert isinstance(signature, str) + + assert NBPCrypto.module_verify(data, signature, hmac_key) is True + assert NBPCrypto.module_verify(b"tampered", signature, hmac_key) is False + + +def test_crypto_rsa_oaep_key_encapsulation(): + """测试 RSA-OAEP 密钥封装""" + aes_key = NBPCrypto.generate_aes_key() + rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=2048) + + encrypted = NBPCrypto.encrypt_key(aes_key, rsa_public) + assert isinstance(encrypted, str) + + decrypted = NBPCrypto.decrypt_key(encrypted, rsa_private) + assert decrypted == aes_key + + +def test_crypto_anti_debug(): + """测试反调试检测(不触发,仅验证函数存在)""" + assert hasattr(NBPCrypto, "_anti_debug_check") + assert callable(NBPCrypto._anti_debug_check) + + +def test_crypto_secure_wipe(): + """测试内存擦除""" + buf = bytearray(b"secret_key_1234567890") + NBPCrypto._secure_wipe(buf) + assert all(b == 0 for b in buf) + + +# ═══════════════════════════════════════════════════════════════ +# 测试:NIRCompiler +# ═══════════════════════════════════════════════════════════════ + +def test_compiler_compile_source(): + """测试编译源码为 NIR""" + compiler = NIRCompiler() + source = "x = 1\ny = x + 2\nresult = y * 3\n" + nir_data = compiler.compile_source(source, "test.py") + assert isinstance(nir_data, bytes) + + # 反序列化 + code = NIRCompiler.deserialize_nir(nir_data) + assert code is not None + + # 执行 + ns = {} + exec(code, ns) + assert ns["result"] == 9 + + +def test_compiler_create_function(): + """测试从 code object 创建函数""" + compiler = NIRCompiler() + source = "def add(a, b): return a + b" + nir_data = compiler.compile_source(source, "test.py") + code = NIRCompiler.deserialize_nir(nir_data) + + ns = {} + exec(code, ns) + assert ns["add"](2, 3) == 5 + + +def test_compiler_forbidden_modules(): + """测试禁止导入危险模块""" + compiler = NIRCompiler() + dangerous_sources = [ + "import os", + "from os import path", + "import subprocess", + "import sys", + "import socket", + "import ctypes", + ] + for source in dangerous_sources: + try: + compiler.compile_source(source, "test.py") + assert False, f"应该拒绝: {source}" + except NIRCompileError: + pass # 预期行为 + + +def test_compiler_compile_plugin(): + """测试编译整个插件目录""" + compiler = NIRCompiler() + with tempfile.TemporaryDirectory() as tmp: + plugin_dir = _create_test_plugin(Path(tmp)) + nir_data = compiler.compile_plugin(plugin_dir) + assert "main" in nir_data + assert isinstance(nir_data["main"], bytes) + + +# ═══════════════════════════════════════════════════════════════ +# 测试:NBPFPacker + NBPFUnpacker +# ═══════════════════════════════════════════════════════════════ + +def _create_packer(): + """创建 NBPFPacker 实例""" + return NBPFPacker(crypto=NBPCrypto(), compiler=NIRCompiler()) + + +def test_pack_unpack(): + """测试打包和解包完整流程""" + packer = _create_packer() + unpacker = NBPFUnpacker() + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + plugin_dir = _create_test_plugin(tmp_path) + keys = _generate_keys(tmp_path) + + # 打包 + nbpf_path = tmp_path / "test-plugin.nbpf" + result = packer.pack( + plugin_dir=plugin_dir, + output_path=nbpf_path, + ed25519_private_key=keys["ed25519_private"], + rsa_private_key_pem=keys["rsa_private"], + rsa_public_key_pem=keys["rsa_public"], + signer_name="test", + ) + assert result.exists() + assert result.suffix == ".nbpf" + + # 解包 + unpack_dir = tmp_path / "unpacked" + result_dir = unpacker.unpack(nbpf_path, unpack_dir) + assert result_dir.exists() + # 解包后 manifest 在 META-INF/MANIFEST.MF + assert (result_dir / "META-INF" / "MANIFEST.MF").exists() + + +def test_extract_manifest(): + """测试提取 manifest""" + packer = _create_packer() + unpacker = NBPFUnpacker() + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + plugin_dir = _create_test_plugin(tmp_path) + keys = _generate_keys(tmp_path) + + nbpf_path = tmp_path / "test-plugin.nbpf" + packer.pack( + plugin_dir=plugin_dir, + output_path=nbpf_path, + ed25519_private_key=keys["ed25519_private"], + rsa_private_key_pem=keys["rsa_private"], + rsa_public_key_pem=keys["rsa_public"], + signer_name="test", + ) + + manifest = unpacker.extract_manifest(nbpf_path) + assert manifest["metadata"]["name"] == "test-plugin" + assert manifest["metadata"]["version"] == "1.0.0" + + +def test_verify_signature(): + """测试签名验证""" + packer = _create_packer() + unpacker = NBPFUnpacker() + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + plugin_dir = _create_test_plugin(tmp_path) + keys = _generate_keys(tmp_path) + + nbpf_path = tmp_path / "test-plugin.nbpf" + packer.pack( + plugin_dir=plugin_dir, + output_path=nbpf_path, + ed25519_private_key=keys["ed25519_private"], + rsa_private_key_pem=keys["rsa_private"], + rsa_public_key_pem=keys["rsa_public"], + signer_name="test", + ) + + # 用正确的公钥验证 + trusted = {"test": keys["ed25519_public"]} + valid, msg = unpacker.verify_signature(nbpf_path, trusted) + assert valid, f"验证失败: {msg}" + + # 用错误的公钥验证 + wrong_private, wrong_public = NBPCrypto.generate_ed25519_keypair() + wrong_trusted = {"wrong": wrong_public} + valid, msg = unpacker.verify_signature(nbpf_path, wrong_trusted) + assert not valid, "应该验证失败" + + +# ═══════════════════════════════════════════════════════════════ +# 测试:NBPFLoader +# ═══════════════════════════════════════════════════════════════ + +def test_loader_full_flow(): + """测试加载器完整流程""" + packer = _create_packer() + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + plugin_dir = _create_test_plugin(tmp_path) + keys = _generate_keys(tmp_path) + + # 打包 + nbpf_path = tmp_path / "test-plugin.nbpf" + packer.pack( + plugin_dir=plugin_dir, + output_path=nbpf_path, + ed25519_private_key=keys["ed25519_private"], + rsa_private_key_pem=keys["rsa_private"], + rsa_public_key_pem=keys["rsa_public"], + signer_name="test", + ) + + # 加载 + loader = NBPFLoader( + crypto=NBPCrypto(), + compiler=NIRCompiler(), + trusted_ed25519_keys={"test": keys["ed25519_public"]}, + trusted_rsa_keys={"test": keys["rsa_public"]}, + rsa_private_key=keys["rsa_private"], + ) + + instance, info = loader.load(nbpf_path) + assert instance is not None + assert info["name"] == "test-plugin" + assert info["version"] == "1.0.0" + + # 验证插件功能 + assert instance.name == "test-plugin" + assert instance.greet("World") == "Hello, World!" + + # 验证启动/停止 + assert instance.start() is True + assert instance.started is True + assert instance.stop() is True + assert instance.started is False + + +def test_loader_wrong_signature(): + """测试加载器拒绝错误签名""" + packer = _create_packer() + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + plugin_dir = _create_test_plugin(tmp_path) + keys = _generate_keys(tmp_path) + + nbpf_path = tmp_path / "test-plugin.nbpf" + packer.pack( + plugin_dir=plugin_dir, + output_path=nbpf_path, + ed25519_private_key=keys["ed25519_private"], + rsa_private_key_pem=keys["rsa_private"], + rsa_public_key_pem=keys["rsa_public"], + signer_name="test", + ) + + # 用错误的 Ed25519 公钥 + _, wrong_public = NBPCrypto.generate_ed25519_keypair() + loader = NBPFLoader( + trusted_ed25519_keys={"wrong": wrong_public}, + trusted_rsa_keys={"test": keys["rsa_public"]}, + rsa_private_key=keys["rsa_private"], + ) + + try: + loader.load(nbpf_path) + assert False, "应该抛出 NBPFLoadError" + except NBPFLoadError: + pass + + +# ═══════════════════════════════════════════════════════════════ +# 测试:PluginManager 集成 +# ═══════════════════════════════════════════════════════════════ + +def test_plugin_manager_nbpf_methods(): + """测试 PluginManager 的 NBPF 方法""" + from oss.core.engine import PluginManager + + pm = PluginManager() + assert hasattr(pm, "load_nbpf") + assert hasattr(pm, "_init_nbpf") + assert pm._nbpf_initialized is False + + # 初始化 + pm._init_nbpf() + # 没有密钥文件时,加载器可能为 None + # 但方法应该存在且不崩溃 + + +# ═══════════════════════════════════════════════════════════════ +# 运行 +# ═══════════════════════════════════════════════════════════════ + +if __name__ == "__main__": + tests = [ + ("NBPCrypto AES", test_crypto_aes_encrypt_decrypt), + ("NBPCrypto 外层加密", test_crypto_outer_encrypt_decrypt), + ("NBPCrypto 中层加密", test_crypto_inner_encrypt_decrypt), + ("NBPCrypto Ed25519", test_crypto_ed25519_sign_verify), + ("NBPCrypto RSA-PSS", test_crypto_rsa_sign_verify), + ("NBPCrypto HMAC", test_crypto_hmac_sign_verify), + ("NBPCrypto RSA-OAEP", test_crypto_rsa_oaep_key_encapsulation), + ("NBPCrypto 反调试", test_crypto_anti_debug), + ("NBPCrypto 内存擦除", test_crypto_secure_wipe), + ("NIRCompiler 编译", test_compiler_compile_source), + ("NIRCompiler 函数创建", test_compiler_create_function), + ("NIRCompiler 禁止模块", test_compiler_forbidden_modules), + ("NIRCompiler 编译目录", test_compiler_compile_plugin), + ("NBPF 打包解包", test_pack_unpack), + ("NBPF 提取 manifest", test_extract_manifest), + ("NBPF 签名验证", test_verify_signature), + ("NBPF 加载器完整流程", test_loader_full_flow), + ("NBPF 加载器错误签名", test_loader_wrong_signature), + ("PluginManager 集成", test_plugin_manager_nbpf_methods), + ] + + passed = 0 + failed = 0 + for name, test_fn in tests: + try: + test_fn() + print(f" ✓ {name}") + passed += 1 + except Exception as e: + print(f" ✗ {name}: {type(e).__name__}: {e}") + failed += 1 + + print(f"\n结果: {passed} 通过, {failed} 失败, {passed + failed} 总计") + sys.exit(1 if failed > 0 else 0)