初始提交 - FutureOSS v1.0 插件化运行时框架

一切皆为插件的开发者工具运行时框架

🧩 核心特性:
  - 插件热插拔 (importlib 动态加载)
  - 依赖自动解析 (拓扑排序 + 循环检测)
  - 企业级稳定 (熔断/降级/重试/隔离)
  - 事件驱动 (发布/订阅事件总线)
  - 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
Falck
2026-04-06 09:57:10 +08:00
commit 76147bae94
174 changed files with 15626 additions and 0 deletions

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
__pycache__/
*.pyc
*.pyo
.venv/
*.egg-info/
.pytest_cache/
.git/
.vscode/
.cache/
logs/
config.yaml
data/pkg/
data/DCIM/
data/html-render/*.json
data/web-toolkit/*.json
data/plugin-storage/*.json
*.so
*.dll
*.dylib

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
.qwen/
QWEN.md
.vscode/
bin/
logs/
*.log
config.yaml
package.json
.cache/
data/
# Web 运行时文件
start-web.sh
website/router.php
admin/
# 数据库配置(含密码)
website/community/config.php
website/community/.env
admin/includes/config.php
# 日志
logs/
*.log

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
FROM python:3.12-slim AS builder
WORKDIR /app
# 构建依赖
COPY pyproject.toml ./
COPY README.md ./
# 安装 Python 依赖
RUN pip install --no-cache-dir --prefix=/install . 2>/dev/null || \
pip install --no-cache-dir --prefix=/install --break-system-packages . 2>/dev/null || true
# 兜底核心依赖
RUN pip install --no-cache-dir --prefix=/install click pyyaml websockets 2>/dev/null || true
# ─────────────────────────────────────────────────────────
FROM python:3.12-slim
LABEL maintainer="Falck <https://gitee.com/starlight-apk>"
LABEL description="FutureOSS — 一切皆为插件的开发者工具运行时框架"
WORKDIR /app
# 复制依赖
COPY --from=builder /install /usr/local
# 复制项目文件
COPY oss/ ./oss/
COPY store/ ./store/
COPY data/ ./data/
COPY start.sh ./start.sh
COPY pyproject.toml ./pyproject.toml
COPY README.md ./README.md
# 创建必要目录
RUN mkdir -p /app/data/html-render /app/data/web-toolkit /app/data/plugin-storage /app/data/DCIM /app/data/pkg /app/logs
# 暴露端口
EXPOSE 8080 8081 8082
# 健康检查
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动
CMD ["python", "-m", "oss.cli", "serve"]

190
LICENSE Normal file
View File

@@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026 Falck
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

106
README.md Normal file
View File

@@ -0,0 +1,106 @@
<div align="center">
<img src="static/banner.svg" alt="FutureOSS Banner" width="100%" />
</div>
---
<div align="center">
## 📂 项目结构
</div>
```
FutureOSS/
├── 🚀 pyproject.toml # Python 项目配置
├── 📋 oss/ # 核心框架包
│ ├── cli.py # CLI 命令入口
│ ├── config/ # 配置系统
│ ├── logger/ # 日志系统
│ ├── plugin/ # 插件框架 (接口/加载器/管理器)
│ └── server/ # HTTP 服务器
├── 🧩 store/ # 本地插件仓库
│ └── @{作者/插件名}/ # 插件目录 (含 manifest.json)
├── 📦 data/ # 运行时数据目录
│ ├── html-render/ # 网站文件 (通过 config.json 配置)
│ ├── web-toolkit/ # Web 工具配置
│ ├── plugin-storage/ # 插件存储配置
│ └── DCIM/ # 共享存储
├── 🌐 website/ # 官网 + 社区 (PHP)
└── 📖 static/ # 静态资源 (SVG 背景等)
```
<div align="center">
## 📖 文档
</div>
所有文档都在本地 `dock/` 目录中:
| 📘 页面 | 📝 内容 |
|:---:|:---|
| [🎯 项目介绍](./dock/00-项目介绍/) | 什么是 FutureOSS、架构设计、核心概念 |
| [🚀 快速开始](./dock/01-快速开始/) | 安装、配置、第一次运行 |
| [🔌 插件开发](./dock/02-插件开发/) | 编写你的第一个插件、事件系统 |
| [📄 插件文档](./dock/03-插件文档/) | http-api、ws-api、file 插件详解 |
| [📦 包管理](./dock/04-包管理/) | 安装/卸载/搜索/发布插件 |
| [⚙️ 配置参考](./dock/05-配置参考/) | 配置参数详解 |
| [🚢 部署运维](./dock/06-部署运维/) | 本地运行、Docker、生产环境 |
| [🌟 社区与贡献](./dock/07-社区与贡献/) | 贡献指南、行为准则 |
<div align="center">
## 🔗 远程仓库
</div>
<div align="center">
| 📦 代码仓库 | 📚 包仓库 |
|:---:|:---:|
| [Gitee](https://gitee.com/starlight-apk/feature-oss) | [Gitee Pkg](https://gitee.com/starlight-apk/future-oss-pkg) |
</div>
---
<div align="center">
## 📜 许可证
**[Apache License 2.0](LICENSE)**
Copyright 2026 Falck
本项目采用 Apache 2.0 许可证 — 你可以自由使用、修改和分发,需保留版权和许可证声明。
---
### 📝 作者声明
> 本项目采用 Apache 2.0 开源许可证,此为独立于许可证的补充说明:
- 🚫 **禁止未经作者Falck明确书面许可的二次转发、搬运、转载**
- 🚫 **禁止冒充原作者或声称与官方项目存在关联**
- 🚫 **禁止移除、修改或遮盖版权声明、许可证和 NOTICE 文件**
-**允许个人学习、研究、商业使用(需保留版权和许可证信息)**
> 此声明不改变 Apache 2.0 许可证的法律效力,仅表达作者的合理期望。
> 如需特殊授权,请联系作者。
</div>
<div align="center">
---
<p>
<strong>⚡ FutureOSS</strong> — 一切皆为插件
</p>
<p>
Made with ❤️ by <a href="https://gitee.com/starlight-apk">Falck</a>
</p>
</div>

43
docker-compose.yml Normal file
View File

@@ -0,0 +1,43 @@
services:
futureoss:
build:
context: .
dockerfile: Dockerfile
container_name: futureoss
restart: unless-stopped
ports:
- "8080:8080" # HTTP API + 网站
- "8081:8081" # WebSocket
- "8082:8082" # HTTP TCP
volumes:
# 插件热更新(无需重建镜像)
- ./store/@{FutureOSS}:/app/store/@{FutureOSS}:ro
- ./store/@{Falck}:/app/store/@{Falck}:ro
# 数据持久化
- futureoss-data:/app/data
# 配置可覆盖
- ./config.yaml:/app/config.yaml:ro
environment:
- TZ=Asia/Shanghai
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
reservations:
memory: 128M
volumes:
futureoss-data:
driver: local

2
oss/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
"""Future OSS"""
__version__ = "1.0.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

56
oss/cli.py Normal file
View File

@@ -0,0 +1,56 @@
"""CLI 入口"""
import click
import signal
from oss import __version__
from oss.logger.logger import Logger
from oss.plugin.manager import PluginManager
@click.group()
def cli():
"""Future OSS - 一切皆为插件"""
pass
@cli.command()
def serve():
"""启动 Future OSS"""
log = Logger()
log.info(f"Future OSS {__version__} 启动")
plugin_mgr = PluginManager()
plugin_mgr.load()
plugin_mgr.start()
log.info("就绪")
def shutdown(sig, frame):
log.info("停止中...")
plugin_mgr.stop()
log.info("已停止")
raise SystemExit(0)
signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)
try:
signal.pause()
except AttributeError:
import time
while True:
time.sleep(1)
@cli.command()
def version():
"""显示版本"""
click.echo(f"Future OSS {__version__}")
def main():
cli()
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
oss/logger/logger.py Normal file
View File

@@ -0,0 +1,15 @@
"""日志系统 - 空壳,由日志插件提供实际功能"""
class Logger:
"""日志记录器(空壳)"""
def info(self, msg: str, **kwargs):
print(f"[INFO] {msg}")
def warn(self, msg: str, **kwargs):
print(f"[WARN] {msg}")
def error(self, msg: str, **kwargs):
print(f"[ERROR] {msg}")
def debug(self, msg: str, **kwargs):
print(f"[DEBUG] {msg}")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,73 @@
"""能力扫描器 - 自动扫描插件支持的能力"""
import ast
from pathlib import Path
from typing import Any
def scan_capabilities(plugin_dir: Path) -> Any:
"""扫描插件目录,自动发现支持的能力"""
capabilities: set[str] = set()
main_file = plugin_dir / "main.py"
if not main_file.exists():
return capabilities
with open(main_file, "r", encoding="utf-8") as f:
source = f.read()
tree = ast.parse(source)
# 扫描规则:
# 1. 检查是否导出了特定的类或函数
# 2. 检查是否有特定的装饰器或标记
# 3. 检查 import 语句(表示依赖了某个能力)
for node in ast.walk(tree):
# 检查类定义
if isinstance(node, ast.ClassDef):
class_name = node.name
# 如果类名包含特定后缀,认为是能力提供者
if class_name.endswith("Provider"):
cap_name = class_name.replace("Provider", "").lower()
capabilities.add(cap_name)
elif class_name.endswith("Mixin"):
cap_name = class_name.replace("Mixin", "").lower()
capabilities.add(cap_name)
elif class_name.endswith("Support"):
cap_name = class_name.replace("Support", "").lower()
capabilities.add(cap_name)
# 检查函数定义
elif isinstance(node, ast.FunctionDef):
func_name = node.name
# 检查是否有能力相关的装饰器
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name):
if decorator.id.startswith("provides_"):
cap_name = decorator.id.replace("provides_", "")
capabilities.add(cap_name)
elif isinstance(decorator, ast.Attribute):
if decorator.attr.startswith("provides_"):
cap_name = decorator.attr.replace("provides_", "")
capabilities.add(cap_name)
# 检查 import 语句(表示使用了某个能力)
elif isinstance(node, ast.Import):
for alias in node.names:
if "circuit" in alias.name.lower() or "breaker" in alias.name.lower():
capabilities.add("circuit_breaker")
elif "retry" in alias.name.lower():
capabilities.add("retry")
elif "cache" in alias.name.lower():
capabilities.add("cache")
elif isinstance(node, ast.ImportFrom):
if node.module:
if "circuit" in node.module.lower() or "breaker" in node.module.lower():
capabilities.add("circuit_breaker")
elif "retry" in node.module.lower():
capabilities.add("retry")
elif "cache" in node.module.lower():
capabilities.add("cache")
return capabilities

125
oss/plugin/loader.py Normal file
View File

@@ -0,0 +1,125 @@
"""插件加载器 - 加载基础插件(带沙箱隔离)"""
import sys
import importlib.util
from pathlib import Path
from typing import Any, Optional
from oss.plugin.types import Plugin
class Sandbox:
"""沙箱隔离"""
def __init__(self):
self._restricted_builtins = {
"__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,
"str": str,
"int": int,
"float": float,
"list": list,
"dict": dict,
"set": set,
"tuple": tuple,
"print": print,
}
}
def get_safe_globals(self) -> dict:
"""获取安全的 globals"""
return dict(self._restricted_builtins)
class PluginLoader:
"""插件加载器(带沙箱隔离)"""
def __init__(self, enable_sandbox: bool = True):
self.loaded: dict[str, Any] = {}
self.sandbox = Sandbox() if enable_sandbox else None
def load_core_plugin(self, plugin_name: str, store_dir: str = "store") -> Optional[dict[str, Any]]:
"""加载核心插件(不受沙箱限制)"""
plugin_dir = Path(store_dir) / "@{FutureOSS}" / plugin_name
return self._load_plugin(plugin_name, plugin_dir, use_sandbox=False, allow_relative=True)
def load_sandbox_plugin(self, plugin_dir: Path) -> Optional[dict[str, Any]]:
"""加载沙箱插件"""
plugin_name = plugin_dir.name
result = self._load_plugin(plugin_name, plugin_dir, use_sandbox=True, allow_relative=True)
return result
def _load_plugin(self, plugin_name: str, plugin_dir: Path, use_sandbox: bool = True, allow_relative: bool = False) -> Optional[dict[str, Any]]:
"""加载插件"""
if not (plugin_dir / "main.py").exists():
return None
# 清理插件名(去掉 } 等)
clean_name = plugin_name.rstrip("}")
module_name = f"plugin.{clean_name}"
spec = importlib.util.spec_from_file_location(module_name, str(plugin_dir / "main.py"))
module = importlib.util.module_from_spec(spec)
module.__package__ = module_name
module.__path__ = [str(plugin_dir)] # 启用相对导入子模块
sys.modules[spec.name] = module
# 沙箱模式:限制内置函数
if use_sandbox and self.sandbox:
safe_globals = self.sandbox.get_safe_globals()
# 允许导入框架模块
safe_globals["__builtins__"]["__import__"] = self._safe_import
spec.loader.exec_module(module)
else:
spec.loader.exec_module(module)
if not hasattr(module, "New"):
return None
instance = module.New()
self.loaded[clean_name] = {
"instance": instance,
"module": module,
"path": str(plugin_dir),
"name": clean_name, # 存储清理后的名称
}
return self.loaded[clean_name]
@staticmethod
def _safe_import(name: str, globals: dict = None, locals: dict = None, fromlist: tuple = (), level: int = 0):
"""安全导入:只允许导入框架模块、标准库子集和插件自身模块"""
allowed_prefixes = ("oss.", "json.", "time.", "datetime.", "enum.", "typing.", "dataclasses.", "pathlib.", "mimetypes.", "http.", "threading.", "socket.", "asyncio.", "websockets.", "re.", "urllib.", "shutil.", "string.", "io.", "base64.", "hashlib.", "hmac.", "secrets.", "math.", "random.", "collections.", "functools.", "itertools.", "operator.", "copy.", "pprint.", "textwrap.", "unicodedata.", "struct.", "codecs.", "locale.", "gettext.", "logging.", "warnings.", "contextlib.", "abc.", "atexit.", "traceback.", "linecache.", "tokenize.", "keyword.", "ast.", "dis.", "inspect.", "types.", "__future__.", "importlib.", "pkgutil.", "sys.", "os.", "stat.", "glob.", "tempfile.", "fnmatch.", "csv.", "configparser.", "argparse.", "html.", "xml.", "email.", "mailbox.", "mimetypes.", "binascii.", "zlib.", "gzip.", "bz2.", "lzma.", "zipfile.", "tarfile.", "sqlite3.", "zlib.")
if any(name.startswith(p) for p in allowed_prefixes):
return __import__(name, globals, locals, fromlist, level)
# 允许相对导入(插件自身模块)
if level > 0:
return __import__(name, globals, locals, fromlist, level)
raise ImportError(f"插件不允许导入模块: {name}")

33
oss/plugin/manager.py Normal file
View File

@@ -0,0 +1,33 @@
"""插件管理器 - 只加载 plugin-loader"""
from typing import Any, Optional
from oss.plugin.loader import PluginLoader
class PluginManager:
"""管理基础插件"""
def __init__(self):
self.loader = PluginLoader()
self.plugin_loader: Optional[Any] = None
def load(self):
"""加载基础插件"""
# 只加载 plugin-loader其他都是可选的
pl_info = self.loader.load_core_plugin("plugin-loader")
if pl_info:
self.plugin_loader = pl_info["instance"]
def start(self):
"""启动基础插件"""
if self.plugin_loader:
self.plugin_loader.init()
self.plugin_loader.start()
def stop(self):
"""停止基础插件"""
if self.plugin_loader:
try:
self.plugin_loader.stop()
except Exception:
pass

92
oss/plugin/types.py Normal file
View File

@@ -0,0 +1,92 @@
"""插件类型定义"""
from abc import ABC, abstractmethod
from typing import Any, Optional
# ========== 插件自定义类型注册 ==========
_plugin_types: dict[str, Any] = {}
def register_plugin_type(type_name: str, type_class: Any):
"""注册插件自定义类型"""
_plugin_types[type_name] = type_class
def get_plugin_type(type_name: str) -> Optional[Any]:
"""获取已注册的插件类型"""
return _plugin_types.get(type_name)
def list_plugin_types() -> dict[str, Any]:
"""列出所有已注册的插件类型"""
return _plugin_types.copy()
# ========== HTTP 响应类型 ==========
class Response:
"""HTTP 响应对象"""
def __init__(self, status: int = 200, headers: Optional[dict[str, str]] = None, body: str = ""):
self.status = status
self.headers = headers or {}
self.body = body
# ========== 插件数据类型 ==========
class Metadata:
"""插件元数据"""
def __init__(self, name: str = "", version: str = "", author: str = "", description: str = ""):
self.name = name
self.version = version
self.author = author
self.description = description
class PluginConfig:
"""插件配置"""
def __init__(self, enabled: bool = True, args: Optional[dict[str, Any]] = None):
self.enabled = enabled
self.args = args or {}
class Manifest:
"""插件清单"""
def __init__(self, metadata: Optional[Metadata] = None, config: Optional[PluginConfig] = None, dependencies: Optional[list[str]] = None):
self.metadata = metadata or Metadata()
self.config = config or PluginConfig()
self.dependencies = dependencies or []
# ========== 插件基类 ==========
class Plugin(ABC):
"""插件基类"""
@abstractmethod
def init(self, deps: Optional[dict[str, Any]] = None):
"""初始化插件"""
...
@abstractmethod
def start(self):
"""启动插件"""
...
@abstractmethod
def stop(self):
"""停止插件"""
...
def meta(self) -> Manifest:
"""获取插件元数据"""
return Manifest()
def reload(self, config: Optional[dict[str, Any]] = None):
"""热重载插件配置"""
pass
def health(self) -> bool:
"""健康检查"""
return True
def stats(self) -> dict[str, Any]:
"""获取插件统计信息"""
return {}

17
pyproject.toml Normal file
View File

@@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "future-oss"
version = "1.0.0"
description = "Future OSS - 一切皆为插件的开发者工具运行时框架"
requires-python = ">=3.10"
dependencies = [
"click>=8.0",
"pyyaml>=6.0",
"websockets>=12.0",
]
[project.scripts]
oss = "oss.cli:main"

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
click>=8.0
pyyaml>=6.0
websockets>=12.0

146
start.bat Normal file
View File

@@ -0,0 +1,146 @@
@echo off
chcp 65001 >nul 2>&1
setlocal enabledelayedexpansion
:: ═══════════════════════════════════════════════════════════
:: FutureOSS 启动脚本 — Windows
:: 自动检测 Python / 依赖 / 守护 / 崩溃重启
:: ═══════════════════════════════════════════════════════════
set "RED=[31m"
set "GREEN=[32m"
set "YELLOW=[33m"
set "CYAN=[36m"
set "WHITE=[37m"
set "BOLD=[1m"
set "NC=[0m"
call :color_echo "BOLD" "CYAN" ""
echo ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗
echo ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝
echo █████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗
echo ██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║
echo ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝
echo ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝
call :color_echo "BOLD" "WHITE" "" 一切皆为插件 · 零编译热插拔
call :color_echo "" "WHITE" "" https://gitee.com/starlight-apk/feature-oss
echo.
:: ── 目录 ──
cd /d "%~dp0"
set "PYTHON_CMD="
set "PIP_CMD="
:: ═══════════════════════════════════════════════════════════
:: 1. 检查 Python
:: ═══════════════════════════════════════════════════════════
call :section "环境检测"
where python 2>nul && set "PYTHON_CMD=python" || (
where python3 2>nul && set "PYTHON_CMD=python3" || (
where py 2>nul && set "PYTHON_CMD=py" || (
call :color_echo "" "YELLOW" "" [!] 未检测到 Python
echo.
echo 请安装 Python 3.10+ :
echo ^> https://www.python.org/downloads/
echo.
echo 安装时请勾选 "Add Python to PATH"
echo.
pause
exit /b 1
)
)
)
for /f "tokens=*" %%i in ('%PYTHON_CMD% --version 2^>^&1') do set "PY_VER=%%i"
call :color_echo "" "GREEN" "" [✓] Python: %PY_VER%
:: ═══════════════════════════════════════════════════════════
:: 2. 虚拟环境 & 依赖
:: ═══════════════════════════════════════════════════════════
call :section "依赖安装"
if not exist ".venv" (
call :color_echo "" "CYAN" "" [i] 创建虚拟环境...
%PYTHON_CMD% -m venv .venv
)
set "VENV_PYTHON=.venv\Scripts\python.exe"
set "VENV_PIP=.venv\Scripts\pip.exe"
if exist "pyproject.toml" (
call :color_echo "" "CYAN" "" [i] 安装项目依赖...
%VENV_PIP% install -e . -q 2>nul
)
if exist "requirements.txt" (
call :color_echo "" "CYAN" "" [i] 安装 requirements.txt...
%VENV_PIP% install -r requirements.txt -q 2>nul
)
:: 核心依赖兜底
for %%p in (click pyyaml websockets) do (
%VENV_PYTHON% -c "import %%p" 2>nul || (
call :color_echo "" "CYAN" "" [i] 安装 %%p ...
%VENV_PIP% install %%p -q 2>nul
)
)
call :color_echo "" "GREEN" "" [✓] 依赖就绪
:: ═══════════════════════════════════════════════════════════
:: 3. 确保 data 目录
:: ═══════════════════════════════════════════════════════════
if not exist "data\html-render" mkdir "data\html-render"
if not exist "data\web-toolkit" mkdir "data\web-toolkit"
if not exist "data\plugin-storage" mkdir "data\plugin-storage"
if not exist "data\DCIM" mkdir "data\DCIM"
if not exist "data\pkg" mkdir "data\pkg"
:: ═══════════════════════════════════════════════════════════
:: 4. 启动
:: ═══════════════════════════════════════════════════════════
call :section "启动 FutureOSS"
set "RESTART_DELAY=3"
set "RESTART_COUNT=0"
:LOOP
echo.
call :color_echo "" "CYAN" "" [i] 启动服务...
echo.
%VENV_PYTHON% -m oss.cli serve
set "EXIT_CODE=!ERRORLEVEL!"
if !EXIT_CODE! equ 0 (
echo.
call :color_echo "" "GREEN" "" [✓] 服务正常退出
goto :END
)
set /a RESTART_COUNT+=1
echo.
call :color_echo "" "YELLOW" "" [!] 服务异常退出 (code: !EXIT_CODE!)!RESTART_DELAY!s 后重启... (第 !RESTART_COUNT! 次)
timeout /t !RESTART_DELAY! /nobreak >nul
if !RESTART_DELAY! lss 30 set /a RESTART_DELAY=!RESTART_DELAY!*2
goto :LOOP
:END
echo.
pause
exit /b 0
:: ── 辅助函数 ──
:color_echo
if "%~1" neq "" set /p "=^<ESC>%BOLD%%~2%<ESC>%NC%" <nul
echo.
goto :eof
:section
echo.
call :color_echo "BOLD" "WHITE" "══════════════════════════════════════"
call :color_echo "BOLD" "WHITE" " %~1"
call :color_echo "BOLD" "WHITE" "══════════════════════════════════════"
goto :eof

180
start.sh Executable file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════
# FutureOSS 启动脚本 — Linux / macOS
# 自动检测 Python / 依赖 / 守护 / 崩溃重启
# ═══════════════════════════════════════════════════════════
set -e
# ── 颜色 ──
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; WHITE='\033[1;37m'; BOLD='\033[1m'; NC='\033[0m'
LOGO="
███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗
██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝
█████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗
██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║
██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝"
info() { echo -e "${CYAN} $1${NC}"; }
ok() { echo -e "${GREEN}$1${NC}"; }
warn() { echo -e "${YELLOW}$1${NC}"; }
err() { echo -e "${RED}$1${NC}"; }
title() { echo -e "\n${BOLD}$1${NC}"; }
# ── 守护参数 ──
DAEMON=false
if [[ "$1" == "--daemon" || "$1" == "-d" ]]; then
DAEMON=true
fi
title "$LOGO"
echo -e "${WHITE} 一切皆为插件 · 零编译热插拔${NC}"
echo -e "${WHITE} https://gitee.com/starlight-apk/feature-oss${NC}"
echo ""
# ── 目录 ──
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$PROJECT_DIR"
# ═══════════════════════════════════════════════════════════
# 1. 检查 Python
# ═══════════════════════════════════════════════════════════
title "📦 环境检测"
find_python() {
for cmd in python3 python python3.12 python3.11 python3.10; do
if command -v "$cmd" &>/dev/null; then
echo "$cmd"
return
fi
done
return 1
}
PYTHON_CMD=$(find_python || true)
if [[ -z "$PYTHON_CMD" ]]; then
warn "未检测到 Python正在自动安装..."
if command -v apt-get &>/dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-pip python3-venv
elif command -v yum &>/dev/null; then
sudo yum install -y python3 python3-pip
elif command -v pacman &>/dev/null; then
sudo pacman -Sy --noconfirm python python-pip
elif command -v brew &>/dev/null; then
brew install python
elif command -v apk &>/dev/null; then
apk add python3 py3-pip
else
err "无法自动安装 Python请手动安装 Python 3.10+"
exit 1
fi
PYTHON_CMD=$(find_python || true)
[[ -z "$PYTHON_CMD" ]] && { err "Python 安装失败"; exit 1; }
fi
PY_VER=$($PYTHON_CMD --version 2>&1)
ok "Python: $PY_VER ($PYTHON_CMD)"
# ═══════════════════════════════════════════════════════════
# 2. 虚拟环境 & 依赖
# ═══════════════════════════════════════════════════════════
title "📚 依赖安装"
VENV_DIR=".venv"
if [[ ! -d "$VENV_DIR" ]]; then
info "创建虚拟环境..."
$PYTHON_CMD -m venv "$VENV_DIR"
fi
source "$VENV_DIR/bin/activate"
PIP_CMD="$VENV_DIR/bin/pip"
if [[ -f "pyproject.toml" ]]; then
info "安装项目依赖 (pyproject.toml)..."
$PIP_CMD install -e . -q 2>/dev/null || $PIP_CMD install -e . --break-system-packages -q 2>/dev/null || true
fi
if [[ -f "requirements.txt" ]]; then
info "安装 requirements.txt..."
$PIP_CMD install -r requirements.txt -q 2>/dev/null || true
fi
# 核心依赖兜底
for pkg in click pyyaml websockets; do
$PYTHON_CMD -c "import $pkg" 2>/dev/null || {
info "安装 $pkg ..."
$PIP_CMD install "$pkg" -q 2>/dev/null || $PIP_CMD install "$pkg" --break-system-packages -q 2>/dev/null || true
}
done
ok "依赖就绪"
# ═══════════════════════════════════════════════════════════
# 3. 确保 data 目录
# ═══════════════════════════════════════════════════════════
mkdir -p data/html-render data/web-toolkit data/plugin-storage data/DCIM data/pkg
# ═══════════════════════════════════════════════════════════
# 4. 启动
# ═══════════════════════════════════════════════════════════
title "🚀 启动 FutureOSS"
if $DAEMON; then
title "🔒 守护模式"
LOG_FILE="logs/futureoss.log"
mkdir -p logs
PID_FILE="logs/futureoss.pid"
if [[ -f "$PID_FILE" ]]; then
OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
warn "已有进程运行 (PID: $OLD_PID),正在停止..."
kill "$OLD_PID" 2>/dev/null || true
sleep 2
fi
fi
nohup $PYTHON_CMD -m oss.cli serve > "$LOG_FILE" 2>&1 &
NEW_PID=$!
echo "$NEW_PID" > "$PID_FILE"
ok "已启动守护进程 (PID: $NEW_PID)"
info "日志: $LOG_FILE"
info "停止: kill $(cat $PID_FILE) 或 bash start.sh stop"
sleep 2
curl -s http://localhost:8080/health &>/dev/null && ok "服务就绪: http://localhost:8080" || warn "服务启动中,请稍候..."
exit 0
fi
# ── 前台模式 + 崩溃自动重启 ──
RESTART_DELAY=3
MAX_RESTARTS=0
RESTART_COUNT=0
run_server() {
$PYTHON_CMD -m oss.cli serve
}
while true; do
run_server
EXIT_CODE=$?
if [[ $EXIT_CODE -eq 0 ]]; then
ok "服务正常退出"
break
fi
RESTART_COUNT=$((RESTART_COUNT + 1))
warn "服务异常退出 (code: $EXIT_CODE)${RESTART_DELAY}s 后重启... (第 $RESTART_COUNT 次)"
sleep $RESTART_DELAY
# 指数退避,最大 30s
if [[ $RESTART_DELAY -lt 30 ]]; then
RESTART_DELAY=$((RESTART_DELAY * 2))
fi
done
deactivate 2>/dev/null || true

421
static/banner.svg Normal file
View File

@@ -0,0 +1,421 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 3300" width="1400" height="3300">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0a0a1a"/>
<stop offset="20%" stop-color="#0d1033"/>
<stop offset="50%" stop-color="#110824"/>
<stop offset="80%" stop-color="#0d1033"/>
<stop offset="100%" stop-color="#0a0a1a"/>
</linearGradient>
<radialGradient id="glow-purple" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.2"/>
<stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow-cyan" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#06b6d4" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#06b6d4" stop-opacity="0"/>
</radialGradient>
<linearGradient id="cube-purple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#8b5cf6" stop-opacity="0.2"/>
</linearGradient>
<linearGradient id="cube-cyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.2"/>
</linearGradient>
<linearGradient id="cube-pink" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ec4899" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#f43f5e" stop-opacity="0.2"/>
</linearGradient>
<pattern id="grid" width="60" height="60" patternUnits="userSpaceOnUse" patternTransform="rotate(12)">
<path d="M 60 0 L 0 0 0 60" fill="none" stroke="#6366f1" stroke-width="0.25" opacity="0.12"/>
</pattern>
<filter id="blur-sm"><feGaussianBlur stdDeviation="6"/></filter>
<filter id="blur-md"><feGaussianBlur stdDeviation="18"/></filter>
<filter id="blur-lg"><feGaussianBlur stdDeviation="40"/></filter>
<filter id="glow"><feGaussianBlur stdDeviation="5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="glow-strong"><feGaussianBlur stdDeviation="8" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="shadow"><feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.5"/></filter>
<!-- 统一图标样式 (基于 Feather Icons) -->
<g id="icon-plugin"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16.88 3.549L7.12 20.451"/></g>
<g id="icon-deps"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></g>
<g id="icon-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></g>
<g id="icon-shield-check"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></g>
<g id="icon-bolt"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></g>
<g id="icon-rocket"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.48-.56.9-1.23 1.23-2l-4.23-1z"/><path d="M2 2l20 20"/></g>
<g id="icon-features"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></g>
<g id="icon-arch"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></g>
<g id="icon-docs"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></g>
<g id="icon-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></g>
<g id="icon-config"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></g>
<g id="icon-deploy"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></g>
<g id="icon-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></g>
<g id="icon-dev"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></g>
<g id="icon-check"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></g>
<g id="icon-wip"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></g>
<g id="icon-license"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></g>
<g id="icon-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></g>
</defs>
<rect width="1400" height="3300" fill="url(#bg)"/>
<rect width="1400" height="3300" fill="url(#grid)"/>
<ellipse cx="700" cy="600" rx="800" ry="500" fill="url(#glow-purple)" filter="url(#blur-lg)" opacity="0.8"/>
<ellipse cx="300" cy="1200" rx="600" ry="400" fill="url(#glow-cyan)" filter="url(#blur-lg)" opacity="0.7"/>
<ellipse cx="1100" cy="1800" rx="700" ry="450" fill="url(#glow-purple)" filter="url(#blur-lg)" opacity="0.75"/>
<ellipse cx="500" cy="2500" rx="650" ry="400" fill="url(#glow-cyan)" filter="url(#blur-lg)" opacity="0.7"/>
<g transform="translate(200, 500) rotate(-25) scale(1.2)" filter="url(#glow-strong)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-purple)" opacity="0.5" stroke="#6366f1" stroke-width="0.8"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-purple)" opacity="0.4" stroke="#6366f1" stroke-width="0.8"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-purple)" opacity="0.35" stroke="#6366f1" stroke-width="0.8"/>
</g>
<g transform="translate(1100, 700) rotate(15) scale(1.1)" filter="url(#glow-strong)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-cyan)" opacity="0.5" stroke="#06b6d4" stroke-width="0.8"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-cyan)" opacity="0.4" stroke="#06b6d4" stroke-width="0.8"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-cyan)" opacity="0.35" stroke="#06b6d4" stroke-width="0.8"/>
</g>
<g transform="translate(700, 1000) rotate(35) scale(0.9)" filter="url(#glow)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-pink)" opacity="0.45" stroke="#ec4899" stroke-width="0.6"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-pink)" opacity="0.35" stroke="#ec4899" stroke-width="0.6"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-pink)" opacity="0.3" stroke="#ec4899" stroke-width="0.6"/>
</g>
<g transform="translate(300, 1500) rotate(-40) scale(0.85)" filter="url(#glow)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-cyan)" opacity="0.4" stroke="#06b6d4" stroke-width="0.6"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-cyan)" opacity="0.3" stroke="#06b6d4" stroke-width="0.6"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-cyan)" opacity="0.25" stroke="#06b6d4" stroke-width="0.6"/>
</g>
<g transform="translate(1050, 1800) rotate(-20) scale(1.05)" filter="url(#glow-strong)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-purple)" opacity="0.5" stroke="#8b5cf6" stroke-width="0.7"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-purple)" opacity="0.4" stroke="#8b5cf6" stroke-width="0.7"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-purple)" opacity="0.35" stroke="#8b5cf6" stroke-width="0.7"/>
</g>
<g transform="translate(500, 2200) rotate(28) scale(0.75)" filter="url(#glow)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-pink)" opacity="0.4" stroke="#f43f5e" stroke-width="0.5"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-pink)" opacity="0.3" stroke="#f43f5e" stroke-width="0.5"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-pink)" opacity="0.25" stroke="#f43f5e" stroke-width="0.5"/>
</g>
<g transform="translate(900, 2600) rotate(-32) scale(0.95)" filter="url(#glow)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-cyan)" opacity="0.45" stroke="#06b6d4" stroke-width="0.6"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-cyan)" opacity="0.35" stroke="#06b6d4" stroke-width="0.6"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-cyan)" opacity="0.3" stroke="#06b6d4" stroke-width="0.6"/>
</g>
<g transform="translate(250, 3000) rotate(18) scale(0.8)" filter="url(#glow)">
<polygon points="0,-40 60,-70 120,-40 60,-10" fill="url(#cube-purple)" opacity="0.4" stroke="#6366f1" stroke-width="0.5"/>
<polygon points="0,-40 60,-10 60,50 0,20" fill="url(#cube-purple)" opacity="0.3" stroke="#6366f1" stroke-width="0.5"/>
<polygon points="60,-10 120,-40 120,20 60,50" fill="url(#cube-purple)" opacity="0.25" stroke="#6366f1" stroke-width="0.5"/>
</g>
<g transform="translate(700, 150)" text-anchor="middle">
<rect x="-230" y="-25" width="460" height="50" rx="25" fill="#6366f1" opacity="0.15" filter="url(#glow)"/>
<rect x="-228" y="-23" width="456" height="46" rx="23" fill="none" stroke="#6366f1" stroke-width="1" opacity="0.4"/>
<text font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#a5b4fc" letter-spacing="3" font-weight="400">Python 3.10+ Apache 2.0 Plugin Driven</text>
</g>
<g transform="translate(700, 380)" text-anchor="middle" filter="url(#shadow)">
<text font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="96" fill="url(#cube-purple)" font-weight="800" letter-spacing="4">FutureOSS</text>
<text y="75" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="32" fill="#e0e7ff" font-weight="300" letter-spacing="8">一切皆为插件的开发者工具运行时框架</text>
</g>
<g transform="translate(700, 520)" text-anchor="middle">
<text font-family="system-ui, -apple-system, sans-serif" font-size="20" fill="#94a3b8" font-style="italic">一个空壳框架,通过插件获得无限能力</text>
</g>
<g transform="translate(0, 650)">
<g transform="translate(100, 0)">
<rect width="280" height="160" rx="12" fill="#1e1b4b" opacity="0.6" filter="url(#shadow)"/>
<rect width="280" height="160" rx="12" fill="none" stroke="#6366f1" stroke-width="1.5" opacity="0.4"/>
<use href="#icon-plugin" transform="translate(128, 38) scale(1)" color="#6366f1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="140" y="95" text-anchor="middle" font-family="system-ui, sans-serif" font-size="18" fill="#e0e7ff" font-weight="600">一切皆插件</text>
<text x="140" y="125" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">协议、中间件、工具</text>
<text x="140" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">所有功能以插件加载</text>
</g>
<g transform="translate(420, 0)">
<rect width="280" height="160" rx="12" fill="#1e1b4b" opacity="0.6" filter="url(#shadow)"/>
<rect width="280" height="160" rx="12" fill="none" stroke="#06b6d4" stroke-width="1.5" opacity="0.4"/>
<use href="#icon-deps" transform="translate(128, 38) scale(1)" color="#06b6d4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="140" y="95" text-anchor="middle" font-family="system-ui, sans-serif" font-size="18" fill="#e0e7ff" font-weight="600">依赖自动解析</text>
<text x="140" y="125" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">拓扑排序 + 循环检测</text>
<text x="140" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">自动识别多级依赖</text>
</g>
<g transform="translate(740, 0)">
<rect width="280" height="160" rx="12" fill="#1e1b4b" opacity="0.6" filter="url(#shadow)"/>
<rect width="280" height="160" rx="12" fill="none" stroke="#ec4899" stroke-width="1.5" opacity="0.4"/>
<use href="#icon-shield" transform="translate(128, 38) scale(1)" color="#ec4899" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="140" y="95" text-anchor="middle" font-family="system-ui, sans-serif" font-size="18" fill="#e0e7ff" font-weight="600">企业级稳定</text>
<text x="140" y="125" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">熔断、降级、重试</text>
<text x="140" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">隔离 + 资源限制</text>
</g>
<g transform="translate(1060, 0)">
<rect width="280" height="160" rx="12" fill="#1e1b4b" opacity="0.6" filter="url(#shadow)"/>
<rect width="280" height="160" rx="12" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.4"/>
<use href="#icon-bolt" transform="translate(128, 38) scale(1)" color="#10b981" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="140" y="95" text-anchor="middle" font-family="system-ui, sans-serif" font-size="18" fill="#e0e7ff" font-weight="600">事件驱动</text>
<text x="140" y="125" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">发布/订阅事件总线</text>
<text x="140" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">40+ 种系统事件</text>
</g>
</g>
<g transform="translate(700, 920)" text-anchor="middle">
<text font-family="system-ui, sans-serif" font-size="36" fill="#e0e7ff" font-weight="700" letter-spacing="3">快速开始</text>
</g>
<g transform="translate(200, 980)">
<rect width="1000" height="180" rx="10" fill="#0f172a" opacity="0.85" filter="url(#shadow)"/>
<rect width="1000" height="180" rx="10" fill="none" stroke="#334155" stroke-width="1"/>
<circle cx="30" cy="30" r="6" fill="#ef4444"/>
<circle cx="55" cy="30" r="6" fill="#f59e0b"/>
<circle cx="80" cy="30" r="6" fill="#10b981"/>
<text x="50" y="75" font-family="'Fira Code', 'Consolas', 'Monaco', monospace" font-size="18" fill="#22d3ee">$ git clone https://gitee.com/starlight-apk/feature-oss.git</text>
<text x="50" y="105" font-family="'Fira Code', 'Consolas', 'Monaco', monospace" font-size="18" fill="#22d3ee">$ cd feature-oss &amp;&amp; bash start.sh</text>
<text x="50" y="145" font-family="'Fira Code', 'Consolas', 'Monaco', monospace" font-size="16" fill="#10b981">✓ 服务已启动 → http://localhost:8080/</text>
</g>
<g transform="translate(700, 1250)" text-anchor="middle">
<text font-family="system-ui, sans-serif" font-size="36" fill="#e0e7ff" font-weight="700" letter-spacing="3">核心特性</text>
</g>
<g transform="translate(150, 1320)">
<g transform="translate(0, 0)">
<rect width="540" height="60" rx="8" fill="#1e1b4b" opacity="0.5"/>
<use href="#icon-plugin" transform="translate(38, 26) scale(1)" color="#6366f1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="85" y="38" font-family="system-ui, sans-serif" font-size="20" fill="#e0e7ff">插件热插拔</text>
<use href="#icon-check" transform="translate(490, 26) scale(1)" color="#10b981" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g transform="translate(580, 0)">
<rect width="540" height="60" rx="8" fill="#1e1b4b" opacity="0.5"/>
<use href="#icon-deps" transform="translate(38, 26) scale(1)" color="#06b6d4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="85" y="38" font-family="system-ui, sans-serif" font-size="20" fill="#e0e7ff">依赖自动解析</text>
<use href="#icon-check" transform="translate(490, 26) scale(1)" color="#10b981" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g transform="translate(0, 80)">
<rect width="540" height="60" rx="8" fill="#1e1b4b" opacity="0.5"/>
<use href="#icon-bolt" transform="translate(38, 26) scale(1)" color="#10b981" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="85" y="38" font-family="system-ui, sans-serif" font-size="20" fill="#e0e7ff">事件总线</text>
<use href="#icon-check" transform="translate(490, 26) scale(1)" color="#10b981" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g transform="translate(580, 80)">
<rect width="540" height="60" rx="8" fill="#1e1b4b" opacity="0.5"/>
<use href="#icon-config" transform="translate(38, 26) scale(1)" color="#f59e0b" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="85" y="38" font-family="system-ui, sans-serif" font-size="20" fill="#e0e7ff">完整配置</text>
<use href="#icon-check" transform="translate(490, 26) scale(1)" color="#10b981" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g transform="translate(0, 160)">
<rect width="540" height="60" rx="8" fill="#1e1b4b" opacity="0.5"/>
<use href="#icon-deploy" transform="translate(38, 26) scale(1)" color="#6366f1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="85" y="38" font-family="system-ui, sans-serif" font-size="20" fill="#e0e7ff">协议适配</text>
<use href="#icon-wip" transform="translate(490, 26) scale(1)" color="#f59e0b" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g transform="translate(580, 160)">
<rect width="540" height="60" rx="8" fill="#1e1b4b" opacity="0.5"/>
<use href="#icon-shield-check" transform="translate(38, 26) scale(1)" color="#ec4899" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="85" y="38" font-family="system-ui, sans-serif" font-size="20" fill="#e0e7ff">熔断降级</text>
<use href="#icon-wip" transform="translate(490, 26) scale(1)" color="#f59e0b" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<g transform="translate(700, 1700)" text-anchor="middle">
<text font-family="system-ui, sans-serif" font-size="36" fill="#e0e7ff" font-weight="700" letter-spacing="3">🏗️ 架构设计</text>
</g>
<g transform="translate(250, 1760)">
<rect width="900" height="300" rx="12" fill="#0f172a" opacity="0.7" filter="url(#shadow)"/>
<rect width="900" height="300" rx="12" fill="none" stroke="#334155" stroke-width="1.5"/>
<text x="450" y="50" text-anchor="middle" font-family="system-ui, sans-serif" font-size="16" fill="#6366f1" font-weight="600">用户层</text>
<rect x="150" y="65" width="600" height="35" rx="6" fill="#1e1b4b" opacity="0.6"/>
<text x="450" y="88" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8">CLI │ HTTP API │ WebSocket │ 社区论坛</text>
<line x1="450" y1="100" x2="450" y2="125" stroke="#6366f1" stroke-width="2"/>
<text x="450" y="155" text-anchor="middle" font-family="system-ui, sans-serif" font-size="16" fill="#06b6d4" font-weight="600">插件层</text>
<rect x="175" y="165" width="170" height="35" rx="6" fill="#1e1b4b" opacity="0.6"/>
<text x="260" y="188" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">http-api</text>
<rect x="365" y="165" width="170" height="35" rx="6" fill="#1e1b4b" opacity="0.6"/>
<text x="450" y="188" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">ws-api</text>
<rect x="555" y="165" width="170" height="35" rx="6" fill="#1e1b4b" opacity="0.6"/>
<text x="640" y="188" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">plugin-storage</text>
<line x1="450" y1="200" x2="450" y2="225" stroke="#06b6d4" stroke-width="2"/>
<text x="450" y="255" text-anchor="middle" font-family="system-ui, sans-serif" font-size="16" fill="#ec4899" font-weight="600">核心层</text>
<rect x="100" y="265" width="700" height="25" rx="6" fill="#1e1b4b" opacity="0.6"/>
<text x="450" y="283" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8">PluginManager | EventBus | Config | Logger | Loader</text>
</g>
<g transform="translate(700, 2150)" text-anchor="middle">
<text font-family="system-ui, sans-serif" font-size="36" fill="#e0e7ff" font-weight="700" letter-spacing="3">文档</text>
</g>
<g transform="translate(450, 2220)">
<!-- 书影 -->
<rect x="8" y="8" width="500" height="310" rx="8" fill="#000" opacity="0.3" filter="url(#blur-md)"/>
<!-- 书封面 -->
<rect x="0" y="0" width="500" height="310" rx="8" fill="#1e1b4b" opacity="0.9"/>
<rect x="0" y="0" width="500" height="310" rx="8" fill="none" stroke="#6366f1" stroke-width="2" opacity="0.6"/>
<!-- 书脊 -->
<rect x="0" y="0" width="25" height="310" rx="8" fill="#0f172a" opacity="0.8"/>
<line x1="25" y1="0" x2="25" y2="310" stroke="#6366f1" stroke-width="1" opacity="0.4"/>
<!-- 书页 -->
<rect x="30" y="10" width="460" height="290" rx="4" fill="#0f172a" opacity="0.4"/>
<!-- 书签 -->
<polygon points="480,0 500,0 500,25 490,18 480,25" fill="#ec4899" opacity="0.8"/>
<!-- 标题 -->
<text x="250" y="40" text-anchor="middle" font-family="system-ui, sans-serif" font-size="16" fill="#6366f1" font-weight="600" letter-spacing="2">所有文档都在本地 dock/ 目录中</text>
<!-- 文档列表 (单列 8 项) -->
<use href="#icon-book" transform="translate(100, 53) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="65" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">项目介绍</text>
<use href="#icon-rocket" transform="translate(100, 83) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="95" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">快速开始</text>
<use href="#icon-dev" transform="translate(100, 113) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="125" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">插件开发</text>
<use href="#icon-docs" transform="translate(100, 143) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="155" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">插件文档</text>
<use href="#icon-config" transform="translate(100, 173) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="185" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">包管理</text>
<use href="#icon-features" transform="translate(100, 203) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="215" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">配置参考</text>
<use href="#icon-deploy" transform="translate(100, 233) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="245" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">部署运维</text>
<use href="#icon-star" transform="translate(100, 263) scale(0.75)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="125" y="275" font-family="system-ui, sans-serif" font-size="15" fill="#a5b4fc">社区与贡献</text>
</g>
<line x1="200" y1="2580" x2="1200" y2="2580" stroke="#334155" stroke-width="1"/>
<g transform="translate(700, 2680)" text-anchor="middle">
<use href="#icon-license" transform="translate(-18, -2) scale(1.33)" color="#e0e7ff" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text font-family="system-ui, sans-serif" font-size="32" fill="#e0e7ff" font-weight="700">许可证</text>
<text y="45" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8">Apache License 2.0</text>
<text y="80" font-family="system-ui, sans-serif" font-size="16" fill="#64748b">Copyright 2026 Falck</text>
</g>
<g transform="translate(700, 2830)" text-anchor="middle">
<use href="#icon-heart" transform="translate(-16, -6) scale(1)" color="#a5b4fc" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="18" font-family="system-ui, sans-serif" font-size="24" fill="#a5b4fc" font-weight="600">Made with by Falck</text>
<text y="40" font-family="system-ui, sans-serif" font-size="16" fill="#64748b">https://gitee.com/starlight-apk</text>
</g>
<text x="700" y="3000" font-family="system-ui, -apple-system, sans-serif" font-size="32" fill="#6366f1" opacity="0.08" text-anchor="middle" letter-spacing="15" font-weight="300">FUTURE OSS</text>
<text x="700" y="3050" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6366f1" opacity="0.06" text-anchor="middle" letter-spacing="10" font-weight="300">EVERYTHING IS A PLUGIN</text>
<!-- 视差效果脚本 -->
<script type="text/ecmascript"><![CDATA[
// 视差层配置
const layers = [
{ selector: '.parallax-grid', speed: 0.05 },
{ selector: '.parallax-cubes', speed: 0.12 },
{ selector: '.parallax-title', speed: 0.25 },
{ selector: '.parallax-content', speed: 0.08 }
];
let mouseX = 700, mouseY = 500;
let currentX = 700, currentY = 500;
const centerX = 700, centerY = 1650;
// 获取SVG元素
function getLayerElements(selector) {
const elements = [];
const all = document.querySelectorAll('*');
for (let i = 0; i < all.length; i++) {
const classes = all[i].getAttribute('class') || '';
if (classes.includes(selector.replace('.', ''))) {
elements.push(all[i]);
}
}
return elements;
}
// 鼠标移动事件
document.addEventListener('mousemove', function(e) {
const svg = document.querySelector('svg');
const rect = svg.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width * 1400;
mouseY = (e.clientY - rect.top) / rect.height * 3300;
});
// 动画循环
function animate() {
// 平滑插值
currentX += (mouseX - currentX) * 0.15;
currentY += (mouseY - currentY) * 0.15;
const deltaX = currentX - centerX;
const deltaY = currentY - centerY;
layers.forEach(layer => {
const elements = getLayerElements(layer.selector);
elements.forEach(el => {
const moveX = deltaX * layer.speed;
const moveY = deltaY * layer.speed;
const currentTransform = el.getAttribute('data-base-transform') || '';
el.setAttribute('transform', `${currentTransform} translate(${moveX}, ${moveY})`);
});
});
requestAnimationFrame(animate);
}
// 初始化:为各层添加 class
function init() {
// 网格层
document.querySelectorAll('rect').forEach(rect => {
if (rect.getAttribute('fill') && rect.getAttribute('fill').includes('grid')) {
rect.setAttribute('class', 'parallax-grid');
}
});
// 3D立方体层
const cubeGroups = [
'translate(200, 500)', 'translate(1100, 700)', 'translate(700, 1000)',
'translate(300, 1500)', 'translate(1050, 1800)', 'translate(500, 2200)',
'translate(900, 2600)', 'translate(250, 3000)'
];
document.querySelectorAll('g').forEach(g => {
const transform = g.getAttribute('transform') || '';
if (cubeGroups.some(t => transform.includes(t))) {
g.setAttribute('data-base-transform', transform);
g.setAttribute('class', 'parallax-cubes');
}
});
// 标题层
document.querySelectorAll('g').forEach(g => {
const transform = g.getAttribute('transform') || '';
if (transform.includes('translate(700, 380)') || transform.includes('translate(700, 150)') || transform.includes('translate(700, 520)')) {
g.setAttribute('data-base-transform', transform);
g.setAttribute('class', 'parallax-title');
}
});
// 内容层
document.querySelectorAll('g').forEach(g => {
const transform = g.getAttribute('transform') || '';
if (transform.includes('translate(0, 650)') || transform.includes('translate(700, 920)') || transform.includes('translate(200, 980)') || transform.includes('translate(700, 1250)') || transform.includes('translate(150, 1320)')) {
g.setAttribute('data-base-transform', transform);
g.setAttribute('class', 'parallax-content');
}
});
animate();
}
init();
]]></script>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,41 @@
# HTML 渲染服务
将存储在 plugin-storage 中的 HTML 页面映射到 8080 端口。
## 功能
- 从 plugin-storage 读取 HTML
- 自动注册路由到 web-toolkit
- 支持动态页面访问
- 页面管理(存储/获取/删除/列出)
## 使用
```python
html_render = plugin_mgr.get("html-render")
# 存储 HTML 页面
html_render.store_html("index", "<h1>Hello World</h1>")
html_render.store_html("about", "<h1>About</h1>")
# 获取页面
html = html_render.get_html("index")
# 列出所有页面
pages = html_render.list_pages() # ["index", "about"]
# 删除页面
html_render.delete_page("about")
```
## 访问
```
http://localhost:8080/ → index 页面
http://localhost:8080/about → about 页面
```
## 依赖
- web-toolkitWeb 服务
- plugin-storageHTML 存储

View File

@@ -0,0 +1,99 @@
"""HTML 渲染服务 - 通过 config.json 配置,统一文件入口"""
import json
from pathlib import Path
from oss.plugin.types import Plugin, register_plugin_type, Response
class HtmlRenderPlugin(Plugin):
"""HTML 渲染插件 - 渲染服务由 html-render 提供"""
def __init__(self):
self.http_api = None
self.storage = None # plugin-storage 入口
self.config = {}
self.root_dir = None # 解析后的网站根目录
def init(self, deps: dict = None):
"""初始化 - 读取 config.json 并解析网站根目录"""
self._load_config()
print(f"[html-render] 配置加载完成: root_dir={self.root_dir}")
def start(self):
"""启动 - 注册路由到 http-api共享配置给 web-toolkit"""
# 注册首页路由
if self.http_api and hasattr(self.http_api, 'router'):
self.http_api.router.get("/", self._serve_html)
print("[html-render] 已注册路由到 http-api")
else:
print("[html-render] http-api 未加载")
# 将配置共享给 web-toolkit通过 plugin-storage 的 DCIM 共享存储)
if self.storage:
shared = self.storage.get_shared()
shared.set_shared("html-render-config", {
"root_dir": str(self.root_dir),
"index_file": self.config.get("index_file", "index.html"),
"static_prefix": self.config.get("static_prefix", "/static"),
})
print("[html-render] 配置已共享到 DCIM")
def stop(self):
"""停止"""
pass
def set_http_api(self, instance):
"""设置 http-api 实例"""
self.http_api = instance
def set_plugin_storage(self, instance):
"""设置 plugin-storage 实例(唯一文件读写入口)"""
self.storage = instance
def _load_config(self):
"""读取 config.json解析根目录"""
config_path = Path("./data/html-render/config.json")
if not config_path.exists():
print("[html-render] 警告: config.json 不存在,使用默认配置")
self.config = {"root_dir": "../website", "index_file": "index.html"}
else:
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
# 解析根目录(相对于 config.json 的路径)
root_relative = self.config.get("root_dir", "../website")
self.root_dir = (config_path.parent / root_relative).resolve()
def _serve_html(self, request):
"""提供 HTML 页面 - 通过 plugin-storage 读取并注入静态资源路径"""
index_file = self.config.get("index_file", "index.html")
if self.storage:
storage = self.storage.get_storage("html-render")
if storage.file_exists(index_file):
content = storage.read_file(index_file)
if content:
# 注入静态资源路径(相对路径 → /website/ 前缀)
content = self._inject_static_paths(content)
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=content
)
return Response(status=404, body="Not Found")
def _inject_static_paths(self, html: str) -> str:
"""将相对静态资源路径替换为 /website/ 前缀"""
import re
# href="css/xxx" → href="/website/css/xxx"
html = re.sub(r'(href\s*=\s*["\'])css/', r'\1/website/css/', html)
# src="js/xxx" → src="/website/js/xxx"
html = re.sub(r'(src\s*=\s*["\'])js/', r'\1/website/js/', html)
# src="logo.svg" → src="/website/logo.svg"
html = re.sub(r'(src\s*=\s*["\'])(?!https?://|/)([\w.-]+\.(svg|png|jpg|gif|ico|webp))', r'\1/website/\2', html)
return html
register_plugin_type("HtmlRenderPlugin", HtmlRenderPlugin)
def New():
return HtmlRenderPlugin()

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"name": "html-render",
"version": "1.0.0",
"author": "Falck",
"description": "HTML 渲染服务 - 提供 8080 端口的 HTML 页面服务",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"html_dir": "./data/html-render"
}
},
"dependencies": ["http-api", "plugin-storage"],
"permissions": ["http-api", "plugin-storage"]
}

View File

@@ -0,0 +1,71 @@
# web-toolkit Web 工具包
提供静态文件服务、模板渲染、路由等 Web 开发工具。
## 功能
- **静态文件服务**:提供 HTML/CSS/JS/图片等静态文件
- **模板引擎**:支持变量替换、条件判断、循环
- **路由管理**:为 HTTP 和 TCP 服务器注册路由
- **自动首页**:自动查找 index.html
## 使用
```python
web = plugin_mgr.get("web-toolkit")
# 设置目录
web.set_static_dir("./public")
web.set_template_dir("./templates")
# 添加自定义路由
web.add_route("GET", "/api/hello", lambda req: {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"message": "Hello"}'
})
# 渲染模板
html = web.render_template("page.html", {"title": "My Page", "items": [1, 2, 3]})
```
## 模板语法
```html
<!-- 变量 -->
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<!-- 条件 -->
{% if show_content %}
<div>{{ content }}</div>
{% endif %}
<!-- 循环 -->
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8080,
"static_dir": "./static",
"template_dir": "./templates",
"index_files": ["index.html", "index.htm"]
}
}
}
```
## 依赖
- http-apiHTTP 服务
- http-tcpTCP HTTP 服务

View File

@@ -0,0 +1,158 @@
"""Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)"""
import json
from pathlib import Path
from oss.plugin.types import Plugin, register_plugin_type, Response
from .router import WebRouter
from .static import StaticFileHandler
from .template import TemplateEngine
class WebToolkitPlugin(Plugin):
"""Web 工具包插件 - 提供网站前端所有服务"""
def __init__(self):
self.router = None
self.static_handler = None
self.template_engine = None
self.http_api = None
self.http_tcp = None
self.storage = None
self.config = {} # 从 config.json 读取
self.root_dir = None
def init(self, deps: dict = None):
"""初始化 - 读取 config.json 配置"""
self.router = WebRouter()
self.template_engine = TemplateEngine()
self._load_config()
self.static_handler = StaticFileHandler(root=str(self.root_dir))
print(f"[web-toolkit] 配置加载完成: root_dir={self.root_dir}")
def start(self):
"""启动"""
# 注册路由到 http-api
if self.http_api:
http_instance = self.http_api
if hasattr(http_instance, "router"):
# 精确路由先注册,参数化路由后注册
http_instance.router.get(
self.config.get("website_prefix", "/website") + "/",
self._serve_website_index
)
http_instance.router.get(
self.config.get("website_prefix", "/website") + "/:path",
self._serve_static
)
http_instance.router.get(
self.config.get("static_prefix", "/static") + "/:path",
self._serve_static
)
# 注册路由到 http-tcp
if self.http_tcp:
tcp_instance = self.http_tcp
if hasattr(tcp_instance, "router"):
tcp_instance.router.get(
self.config.get("website_prefix", "/website") + "/",
self._serve_website_index
)
tcp_instance.router.get(
self.config.get("website_prefix", "/website") + "/:path",
self._serve_static
)
tcp_instance.router.get(
self.config.get("static_prefix", "/static") + "/:path",
self._serve_static
)
print("[web-toolkit] Web 工具包已启动")
def stop(self):
"""停止"""
pass
def set_http_api(self, instance):
"""设置 HTTP API 实例"""
self.http_api = instance
def set_http_tcp(self, instance):
"""设置 HTTP TCP 实例"""
self.http_tcp = instance
def set_plugin_storage(self, instance):
"""设置 plugin-storage 实例(唯一文件读写入口)"""
self.storage = instance
def set_static_dir(self, path: str):
"""设置静态文件目录"""
self.static_handler.set_root(path)
def set_template_dir(self, path: str):
"""设置模板目录"""
template_root = Path(path)
if template_root.exists():
self.template_engine.set_root(str(template_root))
def _load_config(self):
"""读取 config.json解析网站根目录"""
config_path = Path("./data/web-toolkit/config.json")
if not config_path.exists():
print("[web-toolkit] 警告: config.json 不存在,使用默认配置")
self.config = {
"root_dir": "../website",
"index_file": "index.html",
"static_prefix": "/static",
"website_prefix": "/website",
}
else:
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
# 解析根目录(相对于 config.json 的路径)
root_relative = self.config.get("root_dir", "../website")
self.root_dir = (config_path.parent / root_relative).resolve()
# 初始化模板引擎
template_dir = self.config.get("template_dir", "")
if template_dir:
template_path = self.root_dir / template_dir
if template_path.exists():
self.template_engine.set_root(str(template_path))
def _serve_website_index(self, request):
"""提供 website 目录首页"""
index_file = self.config.get("index_file", "index.html")
if self.root_dir:
path = self.root_dir / index_file
if path.exists():
content = path.read_text(encoding="utf-8")
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=content
)
return Response(status=404, body="Index file not found")
def _serve_static(self, request):
"""提供静态文件"""
path = request.path
website_prefix = self.config.get("website_prefix", "/website")
static_prefix = self.config.get("static_prefix", "/static")
if path.startswith(website_prefix + "/"):
filename = path[len(website_prefix) + 1:]
elif path.startswith(static_prefix + "/"):
filename = path[len(static_prefix) + 1:]
else:
filename = path.lstrip("/")
if not filename:
return self._serve_website_index(request)
return self.static_handler.serve(filename)
register_plugin_type("WebToolkitPlugin", WebToolkitPlugin)
def New():
return WebToolkitPlugin()

View File

@@ -0,0 +1,21 @@
{
"metadata": {
"name": "web-toolkit",
"version": "1.0.0",
"author": "Falck",
"description": "Web 工具包 - 提供静态文件服务、模板渲染、路由等 Web 开发工具",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8080,
"static_dir": "./static",
"template_dir": "./templates",
"index_files": ["index.html", "index.htm"]
}
},
"dependencies": ["http-api", "http-tcp", "plugin-storage"],
"permissions": ["http-api", "http-tcp", "json-codec", "plugin-storage"]
}

View File

@@ -0,0 +1,63 @@
"""Web 路由器"""
from typing import Callable, Optional, Any
class WebRoute:
"""Web 路由"""
def __init__(self, method: str, path: str, handler: Callable):
self.method = method
self.path = path
self.handler = handler
class WebRouter:
"""Web 路由器"""
def __init__(self):
self.routes: list[WebRoute] = []
def add_route(self, method: str, path: str, handler: Callable):
"""添加路由"""
self.routes.append(WebRoute(method, path, handler))
def get(self, path: str, handler: Callable):
"""GET 路由"""
self.add_route("GET", path, handler)
def post(self, path: str, handler: Callable):
"""POST 路由"""
self.add_route("POST", path, handler)
def put(self, path: str, handler: Callable):
"""PUT 路由"""
self.add_route("PUT", path, handler)
def delete(self, path: str, handler: Callable):
"""DELETE 路由"""
self.add_route("DELETE", path, handler)
def handle(self, request: dict) -> Optional[Any]:
"""处理请求"""
method = request.get("method", "GET")
path = request.get("path", "/")
for route in self.routes:
if route.method == method and self._match(route.path, path):
return route.handler(request)
return None
def _match(self, pattern: str, path: str) -> bool:
"""路径匹配"""
if pattern == path:
return True
if ":" in pattern:
pattern_parts = pattern.strip("/").split("/")
path_parts = path.strip("/").split("/")
if len(pattern_parts) != len(path_parts):
return False
for p, a in zip(pattern_parts, path_parts):
if not p.startswith(":") and p != a:
return False
return True
return False

View File

@@ -0,0 +1,69 @@
"""静态文件处理器"""
import os
import mimetypes
from pathlib import Path
from typing import Optional, Any
from oss.plugin.types import Response
class StaticFileHandler:
"""静态文件处理器"""
def __init__(self, root: str = "./static"):
self.root = root
self._ensure_root()
def _ensure_root(self):
"""确保静态目录存在"""
Path(self.root).mkdir(parents=True, exist_ok=True)
def set_root(self, path: str):
"""设置静态文件根目录"""
self.root = path
self._ensure_root()
def serve(self, filename: str) -> Optional[Response]:
"""提供静态文件"""
file_path = Path(self.root) / filename
# 安全检查:防止目录遍历
try:
file_path.resolve().relative_to(Path(self.root).resolve())
except ValueError:
return Response(status=403, body="Forbidden")
if not file_path.exists() or not file_path.is_file():
return Response(status=404, body="File not found")
# 检测 MIME 类型
content_type, _ = mimetypes.guess_type(str(file_path))
if not content_type:
content_type = "application/octet-stream"
# 读取文件内容
try:
if content_type.startswith("text/") or content_type in (
"application/json", "application/javascript", "application/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 reading file: {e}")
def list_files(self) -> list[str]:
"""列出静态文件"""
root_path = Path(self.root)
if not root_path.exists():
return []
return [f.name for f in root_path.iterdir() if f.is_file()]

View File

@@ -0,0 +1,144 @@
"""模板引擎"""
import re
import ast
from pathlib import Path
from typing import Any, Optional
class TemplateEngine:
"""简单模板引擎"""
def __init__(self, root: str = "./templates"):
self.root = root
self._cache: dict[str, str] = {}
self._ensure_root()
def _ensure_root(self):
"""确保模板目录存在"""
Path(self.root).mkdir(parents=True, exist_ok=True)
def set_root(self, path: str):
"""设置模板根目录"""
self.root = path
self._ensure_root()
self._cache.clear()
def render(self, name: str, context: dict[str, Any]) -> str:
"""渲染模板"""
template = self._load_template(name)
return self._render_template(template, context)
def _load_template(self, name: str) -> str:
"""加载模板"""
if name in self._cache:
return self._cache[name]
template_path = Path(self.root) / name
if not template_path.exists():
raise FileNotFoundError(f"模板不存在: {name}")
content = template_path.read_text(encoding="utf-8")
self._cache[name] = content
return content
def _safe_eval(self, expression: str, context: dict) -> Any:
"""安全评估表达式(仅允许简单的属性访问和比较)"""
# 只允许访问 context 中的变量
# 支持的运算符: and, or, not, ==, !=, <, >, <=, >=, in
# 不允许函数调用、导入、属性访问等
# 使用 AST 解析并验证
try:
tree = ast.parse(expression, mode='eval')
except SyntaxError:
return False
# 验证 AST 节点
if not self._validate_ast(tree.body[0].value, set(context.keys())):
return False
# 在受限环境中评估
try:
return eval(expression, {"__builtins__": {}}, context)
except Exception:
return False
def _validate_ast(self, node: ast.AST, allowed_names: set) -> bool:
"""验证 AST 只包含安全的操作"""
if isinstance(node, ast.Name):
return node.id in allowed_names or node.id in ('True', 'False', 'None')
elif isinstance(node, ast.Constant):
return True
elif isinstance(node, ast.BoolOp):
return all(self._validate_ast(v, allowed_names) for v in node.values)
elif isinstance(node, ast.Compare):
return (self._validate_ast(node.left, allowed_names) and
all(self._validate_ast(c, allowed_names) for c in node.comparators))
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
return self._validate_ast(node.operand, allowed_names)
elif isinstance(node, ast.Attribute):
# 不允许属性访问(防止绕过安全限制)
return False
elif isinstance(node, ast.Call):
# 不允许函数调用
return False
elif isinstance(node, ast.Subscript):
# 允许简单的索引访问
return (self._validate_ast(node.value, allowed_names) and
self._validate_ast(node.slice, allowed_names))
return False
def _render_template(self, template: str, context: dict[str, Any]) -> str:
"""渲染模板内容"""
# 替换 {{ variable }}
def replace_var(match):
var_name = match.group(1).strip()
value = context.get(var_name, "")
if isinstance(value, (dict, list)):
import json
return json.dumps(value, ensure_ascii=False)
return str(value)
result = re.sub(r'\{\{(.*?)\}\}', replace_var, template)
# 处理 {% if condition %} ... {% endif %}
result = self._process_if(result, context)
# 处理 {% for item in list %} ... {% endfor %}
result = self._process_for(result, context)
return result
def _process_if(self, template: str, context: dict) -> str:
"""处理 if 条件"""
pattern = r'\{%\s*if\s+(.*?)\s*%\}(.*?){%\s*endif\s*%\}'
def replace_if(match):
condition = match.group(1).strip()
content = match.group(2)
# 安全条件评估
value = self._safe_eval(condition, context)
return content if value else ""
return re.sub(pattern, replace_if, template, flags=re.DOTALL)
def _process_for(self, template: str, context: dict) -> str:
"""处理 for 循环"""
pattern = r'\{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%\}(.*?){%\s*endfor\s*%\}'
def replace_for(match):
item_name = match.group(1)
list_name = match.group(2)
content = match.group(3)
items = context.get(list_name, [])
if not isinstance(items, list):
return ""
result = ""
for item in items:
loop_context = {**context, item_name: item}
result += self._render_template(content, loop_context)
return result
return re.sub(pattern, replace_for, template, flags=re.DOTALL)

View File

@@ -0,0 +1,30 @@
# circuit-breaker 熔断器
为插件提供熔断能力,防止级联失败。
## 功能
- 失败计数熔断
- 状态:`closed``open``half-open`
- 可配置失败阈值
- 自动恢复机制
## 状态机
```
closed (正常) → open (熔断) → half-open (半开) → closed (恢复)
```
## 使用
```python
# 检查是否有熔断能力
if "circuit_breaker" in capabilities:
breaker = extensions["_circuit_breaker_provider"]
cb = breaker.create("my-plugin", threshold=5)
try:
result = cb.call(risky_function, arg1, arg2)
except Exception:
print("熔断器已触发")
```

View File

@@ -0,0 +1,70 @@
"""熔断插件 - 为插件提供熔断能力"""
from oss.plugin.types import Plugin, register_plugin_type
class CircuitBreakerProvider:
"""熔断能力提供者"""
def __init__(self):
self.breakers: dict[str, "CircuitBreaker"] = {}
def create(self, name: str, threshold: int = 5) -> "CircuitBreaker":
breaker = CircuitBreaker(name, threshold)
self.breakers[name] = breaker
return breaker
def get(self, name: str):
return self.breakers.get(name)
class CircuitBreaker:
"""熔断器"""
def __init__(self, name: str, threshold: int = 5):
self.name = name
self.threshold = threshold
self.failures = 0
self.state = "closed" # closed, open, half-open
def call(self, func, *args, **kwargs):
if self.state == "open":
raise Exception(f"熔断器 '{self.name}' 已打开")
try:
result = func(*args, **kwargs)
self.failures = 0
self.state = "closed"
return result
except Exception as e:
self.failures += 1
if self.failures >= self.threshold:
self.state = "open"
raise e
class CircuitBreakerPlugin(Plugin):
"""熔断插件"""
def __init__(self):
self.provider = CircuitBreakerProvider()
def init(self, deps: dict = None):
pass
def start(self):
pass
def stop(self):
pass
def get_provider(self):
return self.provider
# 注册类型
register_plugin_type("CircuitBreakerProvider", CircuitBreakerProvider)
register_plugin_type("CircuitBreaker", CircuitBreaker)
def New():
return CircuitBreakerPlugin()

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"name": "circuit-breaker",
"version": "1.0.0",
"author": "FutureOSS",
"description": "熔断器 - 为插件提供熔断能力",
"type": "extension"
},
"config": {
"enabled": true,
"args": {
"default_threshold": 5
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,39 @@
# dependency 依赖解析
插件依赖关系管理,使用拓扑排序确定加载顺序。
## 功能
- 拓扑排序Kahn 算法)
- 循环依赖检测DFS
- 缺失依赖检测
- 自动按依赖顺序加载插件
## 使用
```python
dep = dependency_plugin
# 添加插件及其依赖
dep.add_plugin("plugin-a", ["plugin-b", "plugin-c"])
dep.add_plugin("plugin-b", [])
dep.add_plugin("plugin-c", ["plugin-b"])
# 解析依赖顺序
order = dep.resolve() # 返回 ["plugin-b", "plugin-c", "plugin-a"]
# 检查缺失依赖
missing = dep.get_missing_deps()
# 获取加载顺序
order = dep.get_order()
```
## manifest.json 声明
```json
{
"metadata": {...},
"dependencies": ["lifecycle", "circuit-breaker"]
}
```

View File

@@ -0,0 +1,138 @@
"""依赖解析插件 - 拓扑排序 + 循环依赖检测"""
from typing import Any, Optional
from oss.plugin.types import Plugin, register_plugin_type
class DependencyError(Exception):
"""依赖错误"""
pass
class DependencyResolver:
"""依赖解析器"""
def __init__(self):
self.graph: dict[str, list[str]] = {} # 插件名 -> 依赖列表
def add_plugin(self, name: str, dependencies: list[str]):
"""添加插件及其依赖"""
self.graph[name] = dependencies
def resolve(self) -> list[str]:
"""解析依赖,返回拓扑排序后的插件列表
例如A 依赖 BB 依赖 C
图: A -> [B], B -> [C], C -> []
结果: [C, B, A] (先启动没有依赖的,再启动依赖它们的)
"""
# 检测循环依赖
self._detect_cycles()
# 拓扑排序 (Kahn 算法 - 反向)
# in_degree[name] = name 依赖的插件数量
in_degree: dict[str, int] = {name: 0 for name in self.graph}
# 反向图: who_depends_on[dep] = [name1, name2, ...] (谁依赖 dep)
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 # name 依赖 dep所以 name 的入度 +1
who_depends_on[dep].append(name) # dep 被 name 依赖
# 从没有依赖的插件开始
queue = [name for name, degree in in_degree.items() if degree == 0]
result = []
while queue:
node = queue.pop(0)
result.append(node)
# 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):
"""检测循环依赖"""
visited = set()
rec_stack = set()
def dfs(node: str) -> bool:
visited.add(node)
rec_stack.add(node)
for dep in self.graph.get(node, []):
if dep not in visited:
if dfs(dep):
return True
elif dep in rec_stack:
raise DependencyError(f"检测到循环依赖: {node} -> {dep}")
rec_stack.remove(node)
return False
for node in self.graph:
if node not in visited:
if dfs(node):
raise DependencyError(f"检测到循环依赖涉及: {node}")
def get_missing(self) -> list[str]:
"""获取缺失的依赖"""
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):
self.resolver = DependencyResolver()
self.plugin_deps: dict[str, list[str]] = {}
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动"""
pass
def stop(self):
"""停止"""
pass
def add_plugin(self, name: str, dependencies: list[str]):
"""添加插件及其依赖"""
self.plugin_deps[name] = dependencies
self.resolver.add_plugin(name, dependencies)
def resolve(self) -> list[str]:
"""解析依赖顺序"""
return self.resolver.resolve()
def get_missing_deps(self) -> list[str]:
"""获取缺失的依赖"""
return self.resolver.get_missing()
def get_order(self) -> list[str]:
"""获取插件加载顺序"""
return self.resolve()
# 注册类型
register_plugin_type("DependencyResolver", DependencyResolver)
register_plugin_type("DependencyError", DependencyError)
def New():
return DependencyPlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "dependency",
"version": "1.0.0",
"author": "FutureOSS",
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,32 @@
# hot-reload 热插拔
运行时加载、卸载、更新插件,无需重启服务。
## 功能
- 运行时加载新插件
- 运行时卸载插件
- 运行时更新插件(热重载)
- 自动监听文件变化(可选)
- 模块缓存清理
## 使用
```python
from pathlib import Path
# 加载新插件
hot_reload.load_plugin(Path("store/@{Author/new-plugin"))
# 卸载插件
hot_reload.unload_plugin("plugin-name")
# 更新插件
hot_reload.reload_plugin("plugin-name", Path("store/@{Author/plugin-name"))
```
## 注意事项
- 插件必须实现 `init()`, `start()`, `stop()`
- 卸载时会调用 `stop()`
- 更新时先 `stop()``init()` + `start()`

View File

@@ -0,0 +1,196 @@
"""热插拔插件 - 运行时加载/卸载/更新插件"""
import sys
import time
import threading
from pathlib import Path
from typing import Any, Optional, Callable
from oss.plugin.types import Plugin, register_plugin_type
class HotReloadError(Exception):
"""热插拔错误"""
pass
class FileWatcher:
"""文件监听器"""
def __init__(self, watch_dirs: list[str], extensions: list[str], on_change: Callable):
self.watch_dirs = [Path(d) for d in watch_dirs]
self.extensions = extensions
self.on_change = on_change
self._running = False
self._thread: Optional[threading.Thread] = None
self._file_times: dict[str, float] = {}
self._scan_files()
def _scan_files(self):
"""扫描当前文件及其修改时间"""
for watch_dir in self.watch_dirs:
if watch_dir.exists():
for f in watch_dir.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()
def stop(self):
"""停止监听"""
self._running = False
if self._thread:
self._thread.join(timeout=5)
def _watch_loop(self):
"""监听循环"""
while self._running:
changed = []
current_files = {}
for watch_dir in self.watch_dirs:
if watch_dir.exists():
for f in watch_dir.rglob("*"):
if f.is_file() and f.suffix in self.extensions:
fpath = str(f)
mtime = f.stat().st_mtime
current_files[fpath] = mtime
# 新文件或修改过
if fpath not in self._file_times:
changed.append(("new", f))
elif mtime > self._file_times[fpath]:
changed.append(("modified", f))
# 检查删除的文件
for fpath in self._file_times:
if fpath not in current_files:
changed.append(("deleted", Path(fpath)))
if changed:
self._file_times = current_files
self.on_change(changed)
time.sleep(1)
class HotReloadPlugin(Plugin):
"""热插拔插件"""
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):
"""初始化"""
pass
def start(self):
"""启动 - 自动开始监听默认目录"""
if not self.watch_dirs:
# 默认监听 store 目录
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, fpath in changes:
# 只关心 main.py 和 manifest.json 的变化
if fpath.name not in ("main.py", "manifest.json"):
continue
plugin_dir = fpath.parent
plugin_name = plugin_dir.name
try:
if change_type == "new":
self.load_plugin(plugin_dir)
elif change_type == "modified":
self.reload_plugin(plugin_name, plugin_dir)
elif change_type == "deleted":
self.unload_plugin(plugin_name)
except Exception as e:
print(f"[hot-reload] 处理变化失败: {e}")
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_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"加载插件失败: {e}")
def unload_plugin(self, plugin_name: str) -> bool:
"""运行时卸载插件"""
try:
if plugin_name not in self.plugin_loader_instance.plugins:
raise HotReloadError(f"插件不存在: {plugin_name}")
info = self.plugin_loader_instance.plugins[plugin_name]
instance = info["instance"]
instance.stop()
# 从模块缓存中移除
module = info.get("module")
if module and module.__name__ in sys.modules:
del sys.modules[module.__name__]
del self.plugin_loader_instance.plugins[plugin_name]
return True
except Exception as e:
raise HotReloadError(f"卸载插件失败: {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"更新插件失败: {e}")
# 注册类型
register_plugin_type("HotReloadError", HotReloadError)
register_plugin_type("FileWatcher", FileWatcher)
def New():
return HotReloadPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "hot-reload",
"version": "1.0.0",
"author": "FutureOSS",
"description": "热插拔 - 运行时加载/卸载/更新插件",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"watch_dirs": ["store", "./data/pkg"],
"watch_extensions": [".py", ".json"]
}
},
"dependencies": [],
"permissions": ["plugin-loader"]
}

View File

@@ -0,0 +1,53 @@
# http-api HTTP API 服务
提供 HTTP RESTful API 服务,支持路由、中间件等功能。
## 功能
- HTTP 服务器GET/POST/PUT/DELETE
- 路由匹配(支持参数路由 `:id`
- 中间件链CORS/日志/限流)
- 分散式布局(每个文件 < 200 行)
## 路由使用
```python
# 在插件中获取 router
http_plugin = plugin_mgr.get("http-api")
router = http_plugin.router
# 添加路由
router.get("/health", lambda req: Response(status=200, body='{"status": "ok"}'))
router.get("/api/users", handle_users)
router.post("/api/users", handle_create_user)
router.get("/api/users/:id", handle_user_by_id)
```
## 中间件
```python
middleware = http_plugin.middleware
# 添加自定义中间件
class MyMiddleware(Middleware):
def process(self, ctx, next_fn):
# 前置处理
resp = next_fn() # 继续执行
# 后置处理
return resp
middleware.add(MyMiddleware())
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8080
}
}
}
```

View File

@@ -0,0 +1,58 @@
"""HTTP 事件系统 - 请求/响应生命周期事件"""
from typing import Callable, Any, Optional
from dataclasses import dataclass, field
@dataclass
class HttpEvent:
"""HTTP 事件"""
type: str # request, response, error, etc
request: Any = None
response: Any = None
error: Exception = None
context: dict[str, Any] = field(default_factory=dict)
class HttpEventBus:
"""HTTP 事件总线"""
def __init__(self):
self._handlers: dict[str, list[Callable]] = {}
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 emit(self, event: HttpEvent):
"""发布事件"""
handlers = self._handlers.get(event.type, [])
for handler in handlers:
try:
handler(event)
except Exception:
pass
def clear(self):
"""清空所有订阅"""
self._handlers.clear()
# 事件类型常量
EVENT_REQUEST = "http.request"
EVENT_BEFORE_ROUTE = "http.before_route"
EVENT_AFTER_ROUTE = "http.after_route"
EVENT_BEFORE_HANDLER = "http.before_handler"
EVENT_AFTER_HANDLER = "http.after_handler"
EVENT_RESPONSE = "http.response"
EVENT_ERROR = "http.error"
EVENT_COMPLETE = "http.complete"

View File

@@ -0,0 +1,68 @@
"""HTTP API 插件 - 分散式布局"""
import json
from oss.plugin.types import Plugin, register_plugin_type
from .server import HttpServer, Response
from .router import Router
from .middleware import MiddlewareChain
class HttpApiPlugin(Plugin):
"""HTTP API 插件"""
def __init__(self):
self.server = None
self.router = Router()
self.middleware = MiddlewareChain()
def init(self, deps: dict = None):
"""初始化"""
# 注册基础路由
self.router.get("/health", self._health_handler)
self.router.get("/api/server/info", self._server_info_handler)
self.router.get("/api/status", self._status_handler)
self.server = HttpServer(self.router, self.middleware)
def start(self):
"""启动"""
self.server.start()
def stop(self):
"""停止"""
if self.server:
self.server.stop()
def _health_handler(self, request):
"""健康检查"""
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({
"name": "FutureOSS HTTP API",
"version": "1.0.0",
"endpoints": ["/health", "/api/server/info", "/api/status"]
}),
headers={"Content-Type": "application/json"}
)
def _status_handler(self, request):
"""状态检查"""
return Response(
status=200,
body=json.dumps({"status": "running", "plugins_loaded": True}),
headers={"Content-Type": "application/json"}
)
register_plugin_type("HttpApiPlugin", HttpApiPlugin)
def New():
return HttpApiPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "http-api",
"version": "1.0.0",
"author": "FutureOSS",
"description": "HTTP API 服务 - 提供 RESTful API 和路由功能",
"type": "protocol"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8080
}
},
"dependencies": [],
"permissions": ["lifecycle", "circuit-breaker"]
}

View File

@@ -0,0 +1,57 @@
"""中间件链 - CORS/日志/限流等"""
from typing import Callable, Optional, Any
from .server import Request, Response
class Middleware:
"""中间件基类"""
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
"""处理请求"""
return None
class CorsMiddleware(Middleware):
"""CORS 中间件"""
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
ctx["response_headers"] = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}
return None
class LoggerMiddleware(Middleware):
"""日志中间件"""
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
req = ctx.get("request")
if req:
print(f"[http-api] {req.method} {req.path}")
return None
class MiddlewareChain:
"""中间件链"""
def __init__(self):
self.middlewares: list[Middleware] = []
self.add(LoggerMiddleware())
self.add(CorsMiddleware())
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
return next_fn()

View File

@@ -0,0 +1,72 @@
"""路由器 - 路径匹配和处理器分发"""
from typing import Callable, Optional
from .server import Request, Response
class Route:
"""路由定义"""
def __init__(self, method: str, path: str, handler: Callable):
self.method = method
self.path = path
self.handler = handler
class Router:
"""路由器"""
def __init__(self):
self.routes: list[Route] = []
def add(self, method: str, path: str, handler: Callable):
"""添加路由"""
self.routes.append(Route(method, path, handler))
def get(self, path: str, handler: Callable):
"""GET 路由"""
self.add("GET", path, handler)
def post(self, path: str, handler: Callable):
"""POST 路由"""
self.add("POST", path, handler)
def put(self, path: str, handler: Callable):
"""PUT 路由"""
self.add("PUT", path, handler)
def delete(self, path: str, handler: Callable):
"""DELETE 路由"""
self.add("DELETE", path, handler)
def handle(self, request: Request) -> Response:
"""处理请求"""
for route in self.routes:
if route.method == request.method and self._match(route.path, request.path):
return route.handler(request)
return Response(status=404, body='{"error": "Not Found"}')
def _match(self, pattern: str, path: str) -> bool:
"""路径匹配"""
if pattern == path:
return True
if ":" in pattern:
pattern_parts = pattern.strip("/").split("/")
path_parts = path.strip("/").split("/")
# 检查前缀是否匹配
for i, p in enumerate(pattern_parts):
if i >= len(path_parts):
return False
if not p.startswith(":") and p != path_parts[i]:
return False
# 如果最后一个 pattern 是 :path通配符允许更多路径段
last_pattern = pattern_parts[-1]
if last_pattern.startswith(":") and len(path_parts) >= len(pattern_parts):
return True
# 否则必须精确匹配段数
if len(pattern_parts) != len(path_parts):
return False
return True
return False

View File

@@ -0,0 +1,110 @@
"""HTTP 服务器核心"""
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Any
class Request:
"""请求对象"""
def __init__(self, method, path, headers, body):
self.method = method
self.path = path
self.headers = headers
self.body = body
class Response:
"""响应对象"""
def __init__(self, status=200, headers=None, body=""):
self.status = status
self.headers = headers or {}
self.body = body
class HttpServer:
"""HTTP 服务器"""
def __init__(self, router, middleware, host="0.0.0.0", port=8080):
self.host = host
self.port = port
self.router = router
self.middleware = middleware
self._server = None
self._thread = None
def start(self):
"""启动服务器"""
handler = self._create_handler()
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}")
def stop(self):
"""停止服务器"""
if self._server:
self._server.shutdown()
print("[http-api] 服务器已停止")
def _create_handler(self):
"""创建请求处理器"""
router = self.router
middleware = self.middleware
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self._handle("GET")
def do_POST(self):
self._handle("POST")
def do_PUT(self):
self._handle("PUT")
def do_DELETE(self):
self._handle("DELETE")
def do_OPTIONS(self):
"""处理 CORS 预检请求"""
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def _handle(self, method):
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length) if content_length else b""
req = Request(
method=method,
path=self.path,
headers=dict(self.headers),
body=body.decode("utf-8")
)
# 执行中间件
ctx = {"request": req, "response": None}
result = middleware.run(ctx)
if result:
self._send_response(result)
return
# 路由匹配
resp = router.handle(req)
self._send_response(resp)
def _send_response(self, resp: Response):
self.send_response(resp.status)
for k, v in resp.headers.items():
self.send_header(k, v)
self.end_headers()
if isinstance(resp.body, str):
self.wfile.write(resp.body.encode("utf-8"))
else:
self.wfile.write(resp.body)
def log_message(self, format, *args):
pass
return Handler

View File

@@ -0,0 +1,51 @@
# http-tcp HTTP TCP 服务
提供基于 TCP 的 HTTP 协议实现。
## 功能
- 原始 TCP HTTP 服务器
- 路由匹配
- 中间件链(日志/CORS
- 连接管理
- 事件发布(通过 plugin-bridge
## 使用
```python
tcp = plugin_mgr.get("http-tcp")
# 注册路由
tcp.router.get("/api/status", lambda req: {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"status": "ok"}'
})
# 获取客户端
clients = tcp.server.get_clients()
```
## 事件
```python
bridge = plugin_mgr.get("plugin-bridge")
bus = bridge.event_bus
bus.on("tcp.connect", lambda e: print(f"连接: {e.client.id}"))
bus.on("tcp.http.request", lambda e: print(f"请求: {e.context['request']['path']}"))
bus.on("tcp.disconnect", lambda e: print(f"断开: {e.client.id}"))
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8082
}
}
}
```

View File

@@ -0,0 +1,21 @@
"""HTTP TCP 事件定义"""
from dataclasses import dataclass, field
from typing import Any
@dataclass
class TcpEvent:
"""TCP 事件"""
type: str
client: Any = None
data: bytes = b""
context: dict[str, Any] = field(default_factory=dict)
# 事件类型常量
EVENT_CONNECT = "tcp.connect"
EVENT_DISCONNECT = "tcp.disconnect"
EVENT_DATA = "tcp.data"
EVENT_REQUEST = "tcp.http.request"
EVENT_RESPONSE = "tcp.http.response"
EVENT_ERROR = "tcp.error"

View File

@@ -0,0 +1,34 @@
"""HTTP TCP 插件入口"""
from oss.plugin.types import Plugin, register_plugin_type
from .server import TcpHttpServer
from .router import TcpRouter
from .middleware import TcpMiddlewareChain
class HttpTcpPlugin(Plugin):
"""HTTP TCP 插件"""
def __init__(self):
self.server = None
self.router = TcpRouter()
self.middleware = TcpMiddlewareChain()
def init(self, deps: dict = None):
"""初始化"""
self.server = TcpHttpServer(self.router, self.middleware)
def start(self):
"""启动"""
self.server.start()
def stop(self):
"""停止"""
if self.server:
self.server.stop()
register_plugin_type("HttpTcpPlugin", HttpTcpPlugin)
def New():
return HttpTcpPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "http-tcp",
"version": "1.0.0",
"author": "FutureOSS",
"description": "HTTP TCP 服务 - 基于 TCP 的 HTTP 协议实现",
"type": "protocol"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8082
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,53 @@
"""TCP HTTP 中间件链"""
from typing import Callable, Optional, Any
class TcpMiddleware:
"""TCP 中间件基类"""
def process(self, request: dict, next_fn: Callable) -> Optional[dict]:
"""处理请求"""
return next_fn()
class TcpLogMiddleware(TcpMiddleware):
"""日志中间件"""
def process(self, request, next_fn):
print(f"[http-tcp] {request.get('method')} {request.get('path')}")
return next_fn()
class TcpCorsMiddleware(TcpMiddleware):
"""CORS 中间件"""
def process(self, request, next_fn):
response = next_fn()
if response:
response.setdefault("headers", {})
response["headers"]["Access-Control-Allow-Origin"] = "*"
return response
class TcpMiddlewareChain:
"""TCP 中间件链"""
def __init__(self):
self.middlewares: list[TcpMiddleware] = []
self.add(TcpLogMiddleware())
self.add(TcpCorsMiddleware())
def add(self, middleware: TcpMiddleware):
"""添加中间件"""
self.middlewares.append(middleware)
def run(self, request: dict) -> Optional[dict]:
"""执行中间件链"""
idx = 0
def next_fn():
nonlocal idx
if idx < len(self.middlewares):
mw = self.middlewares[idx]
idx += 1
return mw.process(request, next_fn)
return None
return next_fn()

View File

@@ -0,0 +1,63 @@
"""TCP HTTP 路由器"""
from typing import Callable, Optional, Any
class TcpRoute:
"""TCP HTTP 路由"""
def __init__(self, method: str, path: str, handler: Callable):
self.method = method
self.path = path
self.handler = handler
class TcpRouter:
"""TCP HTTP 路由器"""
def __init__(self):
self.routes: list[TcpRoute] = []
def add(self, method: str, path: str, handler: Callable):
"""添加路由"""
self.routes.append(TcpRoute(method, path, handler))
def get(self, path: str, handler: Callable):
"""GET 路由"""
self.add("GET", path, handler)
def post(self, path: str, handler: Callable):
"""POST 路由"""
self.add("POST", path, handler)
def put(self, path: str, handler: Callable):
"""PUT 路由"""
self.add("PUT", path, handler)
def delete(self, path: str, handler: Callable):
"""DELETE 路由"""
self.add("DELETE", path, handler)
def handle(self, request: dict) -> dict:
"""处理请求"""
method = request.get("method", "GET")
path = request.get("path", "/")
for route in self.routes:
if route.method == method and self._match(route.path, path):
return route.handler(request)
return {"status": 404, "headers": {}, "body": "Not Found"}
def _match(self, pattern: str, path: str) -> bool:
"""路径匹配"""
if pattern == path:
return True
if ":" in pattern:
pattern_parts = pattern.strip("/").split("/")
path_parts = path.strip("/").split("/")
if len(pattern_parts) != len(path_parts):
return False
for p, a in zip(pattern_parts, path_parts):
if not p.startswith(":") and p != a:
return False
return True
return False

View File

@@ -0,0 +1,193 @@
"""TCP HTTP 服务器核心"""
import socket
import threading
import re
from typing import Any, Callable, Optional
from .events import TcpEvent, EVENT_CONNECT, EVENT_DISCONNECT, EVENT_DATA, EVENT_REQUEST, EVENT_RESPONSE
class TcpClient:
"""TCP 客户端连接"""
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.sendall(data)
def close(self):
"""关闭连接"""
self.conn.close()
class TcpHttpServer:
"""TCP HTTP 服务器"""
def __init__(self, router, middleware, event_bus=None, host="0.0.0.0", port=8082):
self.host = host
self.port = port
self.router = router
self.middleware = middleware
self.event_bus = event_bus
self._server = None
self._thread = None
self._running = False
self._clients: dict[str, TcpClient] = {}
def start(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):
"""接受连接循环"""
while self._running:
try:
conn, address = self._server.accept()
client = TcpClient(conn, address)
self._clients[client.id] = client
# 触发连接事件
if self.event_bus:
self.event_bus.emit(TcpEvent(type=EVENT_CONNECT, client=client))
# 启动处理线程
t = threading.Thread(target=self._handle_client, args=(client,), daemon=True)
t.start()
except Exception as e:
if self._running:
print(f"[http-tcp] 接受连接失败: {e}")
def _handle_client(self, client: TcpClient):
"""处理客户端请求"""
buffer = b""
try:
while self._running:
data = client.conn.recv(4096)
if not data:
break
buffer += data
# 检查 HTTP 请求是否完整
if b"\r\n\r\n" in buffer:
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 Exception as e:
if self.event_bus:
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": str(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]:
"""解析 HTTP 请求"""
try:
text = data.decode("utf-8", errors="replace")
lines = text.split("\r\n")
if not lines:
return None
# 解析请求行
match = re.match(r'(\w+)\s+(\S+)\s+HTTP/(\d\.\d)', lines[0])
if not match:
return None
method, path, version = match.groups()
# 解析头
headers = {}
body_start = 0
for i, line in enumerate(lines[1:], 1):
if line == "":
body_start = i + 1
break
if ":" in line:
key, value = line.split(":", 1)
headers[key.strip()] = value.strip()
# 解析体
content_length = int(headers.get("Content-Length", 0))
body = "\r\n".join(lines[body_start:]) if body_start else ""
return {
"method": method,
"path": path,
"version": version,
"headers": headers,
"body": body,
}
except Exception:
return None
def _format_response(self, response: dict) -> bytes:
"""格式化 HTTP 响应"""
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):
"""停止服务器"""
self._running = False
for client in self._clients.values():
client.close()
if self._server:
self._server.close()
print("[http-tcp] 服务器已停止")
def get_clients(self) -> list[TcpClient]:
"""获取所有客户端"""
return list(self._clients.values())

View File

@@ -0,0 +1,83 @@
# json-codec JSON 编解码器
提供插件间 JSON 数据的编码、解码和验证功能。
## 功能
- **JSON 编码**: Python 对象 → JSON 字符串
- **JSON 解码**: JSON 字符串 → Python 对象
- **Schema 验证**: 验证 JSON 数据结构
- **自定义类型**: 支持注册自定义类型编解码器
## 基本使用
```python
codec = plugin_mgr.get("json-codec")
# 编码
data = {"name": "test", "count": 42}
json_str = codec.encode(data)
# '{"name": "test", "count": 42}'
# 编码(格式化)
json_pretty = codec.encode(data, pretty=True)
# '{\n "name": "test",\n "count": 42\n}'
# 解码
parsed = codec.decode(json_str)
# {"name": "test", "count": 42}
```
## HTTP 响应处理
```python
# 在 http-api 插件中使用
router.get("/api/users", lambda req: Response(
status=200,
headers={"Content-Type": "application/json"},
body=codec.encode({"users": [...]})
))
```
## Schema 验证
```python
# 注册 schema
codec.register_schema("user", {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "number"}
}
})
# 验证数据
user_data = {"name": "test", "email": "test@example.com"}
is_valid = codec.validate(user_data, "user")
```
## 自定义类型
```python
from datetime import datetime
# 注册自定义编码器
codec.serializer.register_encoder(datetime, lambda dt: dt.isoformat())
# 使用
data = {"created_at": datetime.now()}
json_str = codec.encode(data)
```
## 错误处理
```python
from oss.plugin.types import JsonCodecError
try:
result = codec.decode("invalid json")
except JsonCodecError as e:
print(f"解码失败: {e}")
```

View File

@@ -0,0 +1,161 @@
"""JSON 编解码器 - 插件间 JSON 数据处理"""
import json
from typing import Any, Callable, Optional
from datetime import datetime
from oss.plugin.types import Plugin, register_plugin_type
class JsonCodecError(Exception):
"""JSON 编解码错误"""
pass
class JsonSerializer:
"""JSON 序列化器"""
def __init__(self):
self._custom_encoders: dict[type, Callable] = {}
def register_encoder(self, type_class: type, encoder: Callable):
"""注册自定义类型编码器"""
self._custom_encoders[type_class] = encoder
def encode(self, data: Any, pretty: bool = False) -> str:
"""编码为 JSON 字符串"""
def default_handler(obj):
if isinstance(obj, datetime):
return obj.isoformat()
for type_class, encoder in self._custom_encoders.items():
if isinstance(obj, type_class):
return encoder(obj)
raise TypeError(f"无法序列化类型: {type(obj).__name__}")
if pretty:
return json.dumps(data, ensure_ascii=False, indent=2, default=default_handler)
return json.dumps(data, ensure_ascii=False, default=default_handler)
def encode_to_bytes(self, data: Any) -> bytes:
"""编码为字节"""
return self.encode(data).encode("utf-8")
class JsonDeserializer:
"""JSON 反序列化器"""
def __init__(self):
self._custom_decoders: dict[str, Callable] = {}
def register_decoder(self, type_name: str, decoder: Callable):
"""注册自定义类型解码器"""
self._custom_decoders[type_name] = decoder
def decode(self, text: str) -> Any:
"""解码 JSON 字符串"""
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise JsonCodecError(f"JSON 解码失败: {e}")
def decode_bytes(self, data: bytes) -> Any:
"""解码字节"""
return self.decode(data.decode("utf-8"))
def decode_file(self, path: str) -> Any:
"""解码 JSON 文件"""
with open(path, "r", encoding="utf-8") as f:
return self.decode(f.read())
class JsonValidator:
"""JSON 验证器"""
def __init__(self):
self._schemas: dict[str, dict] = {}
def register_schema(self, name: str, schema: dict):
"""注册 schema"""
self._schemas[name] = schema
def validate(self, data: Any, schema_name: str) -> bool:
"""验证数据是否符合 schema"""
if schema_name not in self._schemas:
raise JsonCodecError(f"未知的 schema: {schema_name}")
return self._check_schema(data, self._schemas[schema_name])
def _check_schema(self, data: Any, schema: dict) -> bool:
"""检查 schema 匹配"""
schema_type = schema.get("type")
if schema_type == "object":
if not isinstance(data, dict):
return False
required = schema.get("required", [])
for field in required:
if field not in data:
return False
properties = schema.get("properties", {})
for key, value in data.items():
if key in properties:
if not self._check_schema(value, properties[key]):
return False
return True
elif schema_type == "array":
if not isinstance(data, list):
return False
items_schema = schema.get("items", {})
return all(self._check_schema(item, items_schema) for item in data)
elif schema_type == "string":
return isinstance(data, str)
elif schema_type == "number":
return isinstance(data, (int, float))
elif schema_type == "boolean":
return isinstance(data, bool)
return True
class JsonCodecPlugin(Plugin):
"""JSON 编解码器插件"""
def __init__(self):
self.serializer = JsonSerializer()
self.deserializer = JsonDeserializer()
self.validator = JsonValidator()
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动"""
print("[json-codec] JSON 编解码器已启动")
def stop(self):
"""停止"""
pass
def encode(self, data: Any, pretty: bool = False) -> str:
"""编码 JSON"""
return self.serializer.encode(data, pretty)
def decode(self, text: str) -> Any:
"""解码 JSON"""
return self.deserializer.decode(text)
def validate(self, data: Any, schema_name: str) -> bool:
"""验证 JSON schema"""
return self.validator.validate(data, schema_name)
def register_schema(self, name: str, schema: dict):
"""注册 schema"""
self.validator.register_schema(name, schema)
# 注册类型
register_plugin_type("JsonSerializer", JsonSerializer)
register_plugin_type("JsonDeserializer", JsonDeserializer)
register_plugin_type("JsonValidator", JsonValidator)
register_plugin_type("JsonCodecError", JsonCodecError)
def New():
return JsonCodecPlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "json-codec",
"version": "1.0.0",
"author": "FutureOSS",
"description": "JSON 编解码器 - 插件间 JSON 数据处理和验证",
"type": "utility"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,30 @@
# lifecycle 生命周期管理
管理插件的状态转换和钩子函数。
## 功能
- 状态机:`pending``running``stopped`
- 支持状态转换验证
- 提供生命周期钩子:
- `before_start`
- `after_start`
- `before_stop`
- `after_stop`
- 支持扩展能力注入
## 状态转换
```
pending → running → stopped
(可重启)
```
## 使用
```python
lc = lifecycle_plugin.create("my-plugin")
lc.on("after_start", lambda: print("started"))
lc.start()
```

View File

@@ -0,0 +1,150 @@
"""生命周期插件 - 管理插件生命周期状态"""
from enum import Enum
from typing import Optional, Callable, Any
from oss.plugin.types import Plugin, register_plugin_type
class LifecycleState(str, Enum):
"""生命周期状态"""
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) -> Optional[Any]:
"""获取扩展能力"""
return self._extensions.get(name)
def transition(self, target_state: LifecycleState):
"""状态转换"""
if target_state not in self.VALID_TRANSITIONS.get(self.state, []):
raise LifecycleError(
f"插件 '{self.name}' 无法从 {self.state.value} 转换到 {target_state.value}"
)
old_state = self.state
self.state = target_state
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):
"""停止"""
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):
"""重启"""
if self.state == LifecycleState.RUNNING:
self.stop()
self.start()
def on(self, event: str, hook: Callable):
"""注册钩子"""
if event in self._hooks:
self._hooks[event].append(hook)
def is_running(self) -> bool:
return self.state == LifecycleState.RUNNING
def is_stopped(self) -> bool:
return self.state == LifecycleState.STOPPED
def is_pending(self) -> bool:
return self.state == LifecycleState.PENDING
def __repr__(self):
return f"Lifecycle({self.name}, state={self.state.value})"
class LifecyclePlugin(Plugin):
"""生命周期插件"""
def __init__(self):
self.lifecycles: dict[str, Lifecycle] = {}
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动"""
pass
def stop(self):
"""停止"""
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
# 注册类型
register_plugin_type("Lifecycle", Lifecycle)
register_plugin_type("LifecycleState", LifecycleState)
register_plugin_type("LifecycleError", LifecycleError)
def New():
return LifecyclePlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "lifecycle",
"version": "1.0.0",
"author": "FutureOSS",
"description": "生命周期管理 - 管理插件的状态转换和钩子",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,43 @@
# pkg 包管理
插件的搜索、安装、卸载和更新功能。
## 功能
- 从远程仓库搜索插件
- 下载并安装到 `./data/pkg/` 目录
- 卸载已安装的插件
- 更新单个或所有插件
- 维护已安装插件列表
## 使用
```python
pm = pkg_plugin.manager
# 搜索
results = pm.search("keyword")
# 安装
pm.install("plugin-name")
pm.install("plugin-name", version="1.0.0")
# 卸载
pm.uninstall("plugin-name")
# 更新
pm.update() # 更新所有
pm.update("plugin-name") # 更新单个
# 列出已安装
installed = pm.list_installed()
```
## 安装位置
```
./data/pkg/
└── <插件名>/
├── main.py
└── manifest.json
```

View File

@@ -0,0 +1,201 @@
"""包管理插件 - 搜索、安装、卸载、更新插件"""
import os
import json
import shutil
import urllib.request
import urllib.parse
from pathlib import Path
from typing import Any, Optional
from oss.plugin.types import Plugin, register_plugin_type
# 远程仓库地址(可配置)
DEFAULT_REGISTRY = "https://gitee.com/starlight-apk/future-oss-pkg/raw/main"
# 插件安装目录
PKG_DIR = Path("./data/pkg")
class PackageInfo:
"""包信息"""
def __init__(self):
self.name: str = ""
self.version: str = ""
self.author: str = ""
self.description: str = ""
self.download_url: str = ""
self.dependencies: list[str] = []
class PackageManager:
"""包管理器"""
def __init__(self):
self.registry = DEFAULT_REGISTRY
self.index_cache: dict[str, PackageInfo] = {}
self.installed: dict[str, dict[str, Any]] = {}
self._load_installed()
def _load_installed(self):
"""加载已安装的包"""
if not PKG_DIR.exists():
return
for pkg_dir in PKG_DIR.iterdir():
if pkg_dir.is_dir():
manifest = pkg_dir / "manifest.json"
if manifest.exists():
with open(manifest, "r", encoding="utf-8") as f:
self.installed[pkg_dir.name] = json.load(f)
def search(self, query: str = "") -> list[PackageInfo]:
"""搜索可用的包"""
# 从远程仓库获取包索引
index_url = f"{self.registry}/index.json"
try:
with urllib.request.urlopen(index_url, timeout=10) as resp:
index = json.loads(resp.read().decode("utf-8"))
except Exception:
# 本地缓存
return list(self.index_cache.values())
results = []
for pkg_name, pkg_info in index.items():
if not query or query.lower() in pkg_name.lower() or query.lower() in pkg_info.get("description", "").lower():
info = PackageInfo()
info.name = pkg_name
info.version = pkg_info.get("version", "")
info.author = pkg_info.get("author", "")
info.description = pkg_info.get("description", "")
info.download_url = pkg_info.get("download_url", "")
info.dependencies = pkg_info.get("dependencies", [])
results.append(info)
self.index_cache[pkg_name] = info
return results
def install(self, name: str, version: str = "") -> bool:
"""安装包,支持 @{作者/插件名} 格式"""
# 解析输入格式 @{author/plugin} 或直接插件名
author = "FutureOSS" # 默认作者
plugin_name = name
if name.startswith("@{") and "/" in name:
# 解析 @{author/plugin} 格式
inner = name[2:-1] if name.endswith("}") else name[2:]
parts = inner.split("/", 1)
if len(parts) == 2:
author, plugin_name = parts
# 搜索获取下载链接
packages = self.search(plugin_name)
pkg_info = None
for p in packages:
if p.name == plugin_name and p.author == author:
if not version or p.version == version:
pkg_info = p
break
if not pkg_info or not pkg_info.download_url:
# 尝试从远程仓库直接构建 URL
pkg_info = PackageInfo()
pkg_info.name = plugin_name
pkg_info.author = author
pkg_info.version = version or "1.0.0"
pkg_info.download_url = self.registry + "/store/@{" + author + "/" + plugin_name + "}"
# 创建安装目录 @{author/plugin_name}
install_dir = PKG_DIR / ("@{" + author + "/" + plugin_name + "}")
install_dir.mkdir(parents=True, exist_ok=True)
try:
# 下载 manifest.json
manifest_url = f"{pkg_info.download_url}/manifest.json"
with urllib.request.urlopen(manifest_url, timeout=10) as resp:
manifest_data = json.loads(resp.read().decode("utf-8"))
with open(install_dir / "manifest.json", "w", encoding="utf-8") as f:
json.dump(manifest_data, f, ensure_ascii=False, indent=2)
# 下载 main.py
main_url = f"{pkg_info.download_url}/main.py"
with urllib.request.urlopen(main_url, timeout=10) as resp:
main_data = resp.read().decode("utf-8")
with open(install_dir / "main.py", "w", encoding="utf-8") as f:
f.write(main_data)
# 更新已安装列表
full_name = "@{" + author + "/" + plugin_name
self.installed[full_name] = manifest_data
print(f"[pkg] 已安装: {full_name} {manifest_data.get('metadata', {}).get('version', '')}")
return True
except Exception as e:
print(f"[pkg] 安装失败 {name}: {e}")
# 清理失败的安装
if install_dir.exists():
shutil.rmtree(install_dir)
return False
def uninstall(self, name: str) -> bool:
"""卸载包"""
install_dir = PKG_DIR / name
if not install_dir.exists():
print(f"[pkg] 包未安装: {name}")
return False
try:
shutil.rmtree(install_dir)
del self.installed[name]
print(f"[pkg] 已卸载: {name}")
return True
except Exception as e:
print(f"[pkg] 卸载失败 {name}: {e}")
return False
def update(self, name: str = "") -> bool:
"""更新包"""
if name:
# 更新单个包
if name not in self.installed:
print(f"[pkg] 包未安装: {name}")
return False
return self.install(name)
else:
# 更新所有已安装的包
success = True
for pkg_name in list(self.installed.keys()):
if not self.install(pkg_name):
success = False
return success
def list_installed(self) -> dict[str, Any]:
"""列出已安装的包"""
return self.installed
class PkgPlugin(Plugin):
"""包管理插件"""
def __init__(self):
self.manager = PackageManager()
def init(self, deps: dict = None):
"""初始化"""
PKG_DIR.mkdir(parents=True, exist_ok=True)
print("[pkg] 包管理器已初始化")
def start(self):
"""启动"""
print(f"[pkg] 包管理器已启动,已安装 {len(self.manager.installed)} 个包")
def stop(self):
"""停止"""
pass
# 注册类型
register_plugin_type("PackageManager", PackageManager)
register_plugin_type("PackageInfo", PackageInfo)
def New():
return PkgPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "pkg",
"version": "1.0.0",
"author": "FutureOSS",
"description": "包管理 - 插件的搜索、安装、卸载和更新",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"registry": "https://gitee.com/starlight-apk/future-oss-pkg/raw/main",
"install_dir": "./data/pkg"
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,77 @@
# 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()
```

View File

@@ -0,0 +1,203 @@
"""插件桥接器 - 共享事件、广播、桥接"""
from typing import Any, Callable, Optional
from dataclasses import dataclass, field
from oss.plugin.types import Plugin, register_plugin_type
@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:
pass
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 self._channels.copy()
class ServiceRegistry:
"""服务注册表RPC"""
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:
"""远程调用"""
if plugin_name not in self._services:
raise RuntimeError(f"插件 '{plugin_name}' 未注册服务")
if service_name not in self._services[plugin_name]:
raise RuntimeError(f"插件 '{plugin_name}' 未注册服务 '{service_name}'")
return self._services[plugin_name][service_name](*args, **kwargs)
def list_services(self, plugin_name: str = None) -> dict[str, dict[str, Callable]]:
"""列出服务"""
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[str, dict[str, Any]] = {}
def create_bridge(self, name: str, from_plugin: str, to_plugin: str, event_mapping: dict[str, str]):
"""创建桥接:将 from_plugin 的事件映射到 to_plugin"""
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):
"""移除桥接"""
if name in self._bridges:
del self._bridges[name]
def get_bridges(self) -> dict[str, dict[str, Any]]:
"""获取所有桥接"""
return self._bridges.copy()
class PluginBridgePlugin(Plugin):
"""插件桥接器插件"""
def __init__(self):
self.event_bus = EventBus()
self.broadcast = None
self.bridge = None
self.services = ServiceRegistry()
self.storage = None # 共享存储接口
def init(self, deps: dict = None):
"""初始化"""
self.broadcast = BroadcastManager(self.event_bus)
self.bridge = BridgeManager(self.event_bus)
def start(self):
"""启动"""
print("[plugin-bridge] 事件总线、广播、桥接、RPC、共享存储已启动")
def stop(self):
"""停止"""
self.event_bus.clear_history()
def set_plugin_storage(self, storage_plugin):
"""设置存储插件引用"""
if storage_plugin:
self.storage = storage_plugin.get_shared()
# 注册类型
register_plugin_type("BridgeEvent", BridgeEvent)
register_plugin_type("EventBus", EventBus)
register_plugin_type("BroadcastManager", BroadcastManager)
register_plugin_type("BridgeManager", BridgeManager)
register_plugin_type("ServiceRegistry", ServiceRegistry)
def New():
return PluginBridgePlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "plugin-bridge",
"version": "1.0.0",
"author": "FutureOSS",
"description": "插件桥接器 - 共享事件、广播、桥接",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": ["plugin-storage"],
"permissions": ["plugin-storage"]
}

View File

@@ -0,0 +1,16 @@
# plugin-loader 插件加载器
核心插件,负责扫描、加载和管理所有其他插件。
## 功能
- 自动扫描 `store/``./data/pkg/` 目录
- 动态加载 `main.py` 并调用 `New()` 获取实例
- 解析 `manifest.json` 获取插件元数据
- 自动扫描插件能力AST 分析)
- 按依赖关系排序加载顺序
- 关联能力提供者与消费者
## 使用
无需手动使用,框架启动时自动加载。

View File

@@ -0,0 +1,609 @@
"""插件加载器插件 - 支持能力扫描和扩展"""
import sys
import json
import importlib.util
from pathlib import Path
from typing import Any, Optional
from oss.plugin.types import Plugin, register_plugin_type
from oss.plugin.capabilities import scan_capabilities
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] = []
class PermissionError(Exception):
"""权限错误"""
pass
class PluginProxy:
"""插件代理 - 防止越级访问"""
def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict[str, dict[str, Any]]):
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 [name for name in self._allowed_plugins if name 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[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):
"""注册能力提供者"""
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[str] = None) -> Optional[Any]:
"""获取能力提供者实例(带权限检查)"""
if capability not in self.providers:
return None
if self.permission_check and allowed_plugins is not None:
provider_name = self.providers[capability]["plugin"]
if provider_name != requester and provider_name 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[str]:
"""获取能力消费者列表"""
return self.consumers.get(capability, [])
class PluginManager:
"""插件管理器"""
def __init__(self, permission_check: bool = True):
self.plugins: dict[str, dict[str, Any]] = {}
self.lifecycle_plugin: Optional[Any] = None
self._dependency_plugin: Optional[Any] = None # dependency 插件引用
self.capability_registry = CapabilityRegistry(permission_check=permission_check)
self.permission_check = permission_check
def set_lifecycle(self, lifecycle_plugin: Any):
"""设置生命周期插件"""
self.lifecycle_plugin = lifecycle_plugin
def _load_manifest(self, plugin_dir: Path) -> dict[str, Any]:
"""加载 manifest.json"""
manifest_file = plugin_dir / "manifest.json"
if not manifest_file.exists():
return {}
with open(manifest_file, "r", encoding="utf-8") as f:
return json.load(f)
def _load_readme(self, plugin_dir: Path) -> str:
"""加载 README.md"""
readme_file = plugin_dir / "README.md"
if not readme_file.exists():
return ""
with open(readme_file, "r", encoding="utf-8") as f:
return f.read()
def _load_config(self, plugin_dir: Path) -> dict[str, Any]:
"""加载 Python 配置文件(带安全措施)"""
config_file = plugin_dir / "config.py"
if not config_file.exists():
return {}
safe_globals = {
"__builtins__": {
"True": True,
"False": False,
"None": None,
"dict": dict,
"list": list,
"str": str,
"int": int,
"float": float,
"bool": bool,
}
}
local_vars = {}
with open(config_file, "r", encoding="utf-8") as f:
code = compile(f.read(), str(config_file), "exec")
exec(code, safe_globals, local_vars)
return {
k: v for k, v in local_vars.items()
if not k.startswith("_") and not callable(v)
}
def _load_extensions(self, plugin_dir: Path) -> dict[str, Any]:
"""加载扩展语法Python 文件)"""
ext_file = plugin_dir / "extensions.py"
if not ext_file.exists():
return {}
safe_globals = {
"__builtins__": {
"True": True,
"False": False,
"None": None,
"dict": dict,
"list": list,
"str": str,
"int": int,
"float": float,
"bool": bool,
}
}
local_vars = {}
with open(ext_file, "r", encoding="utf-8") as f:
code = compile(f.read(), str(ext_file), "exec")
exec(code, safe_globals, local_vars)
return {
k: v for k, v in local_vars.items()
if not k.startswith("_") and not callable(v)
}
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
# 清理插件名(去掉 } 后缀)
plugin_name = plugin_dir.name.rstrip("}")
# 解析权限
permissions = manifest.get("permissions", [])
# 沙箱加载
if use_sandbox:
from oss.plugin.loader import PluginLoader as FrameworkLoader
framework_loader = FrameworkLoader(enable_sandbox=True)
result = framework_loader.load_sandbox_plugin(plugin_dir)
if not result:
return None
module = result["module"]
instance = result["instance"]
else:
spec = importlib.util.spec_from_file_location(
f"plugin.{plugin_name}", str(main_file)
)
module = importlib.util.module_from_spec(spec)
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", [])
# 注册能力
for cap in capabilities:
self.capability_registry.register_provider(cap, plugin_name, instance)
# 创建生命周期
if self.lifecycle_plugin and plugin_name != "lifecycle":
lc = self.lifecycle_plugin.create(plugin_name)
info.lifecycle = lc
self.plugins[plugin_name] = {
"instance": instance,
"module": module,
"info": info,
"permissions": permissions,
}
return instance
def load_all(self, store_dir: str = "store"):
"""加载 store 和 data/pkg 下所有插件(跳过自己)"""
# 检查是否有任何插件存在
has_plugins = self._check_any_plugins(store_dir)
if not has_plugins:
print("[plugin-loader] 未检测到任何插件,自动引导安装...")
self._bootstrap_installation()
# 可选:加载 lifecycle
lifecycle_plugin = None
lifecycle_dir = Path(store_dir) / "@{FutureOSS}" / "lifecycle"
if lifecycle_dir.exists() and (lifecycle_dir / "main.py").exists():
try:
instance = self.load(lifecycle_dir)
if instance:
lifecycle_plugin = instance
self.plugins.pop("lifecycle", None)
except Exception:
pass
# 可选:加载 dependency
dependency_plugin = None
dependency_dir = Path(store_dir) / "@{FutureOSS}" / "dependency"
if dependency_dir.exists() and (dependency_dir / "main.py").exists():
try:
instance = self.load(dependency_dir)
if instance:
dependency_plugin = instance
self._dependency_plugin = instance # 保存引用供拓扑排序使用
self.plugins.pop("dependency", None)
except Exception:
pass
# 加载 lifecycle
if lifecycle_plugin:
self.set_lifecycle(lifecycle_plugin)
# 加载其他插件
self._load_plugins_from_dir(Path(store_dir))
self._load_plugins_from_dir(Path("./data/pkg"))
# 可选:按依赖排序
if dependency_plugin:
self._sort_by_dependencies(dependency_plugin)
def _load_plugins_from_dir(self, store_dir: Path):
"""从指定目录加载插件"""
if not store_dir.exists():
return
# 第一遍:加载所有插件
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 not in ("plugin-loader", "lifecycle", "dependency"):
if (plugin_dir / "main.py").exists():
self.load(plugin_dir)
# 第二遍:关联能力
self._link_capabilities()
def _check_any_plugins(self, store_dir: str) -> bool:
"""检查是否存在任何插件"""
store = Path(store_dir)
if store.exists():
for author_dir in store.iterdir():
if author_dir.is_dir():
for plugin_dir in author_dir.iterdir():
if plugin_dir.is_dir() and (plugin_dir / "main.py").exists():
return True
pkg_dir = Path("./data/pkg")
if pkg_dir.exists():
for d in pkg_dir.iterdir():
if d.is_dir() and (d / "main.py").exists():
return True
return False
def _bootstrap_installation(self):
"""引导安装 FutureOSS 官方插件"""
# 加载 pkg 插件
pkg_dir = Path("store/@{FutureOSS}/pkg")
if pkg_dir.exists() and (pkg_dir / "main.py").exists():
try:
pkg_instance = self.load(pkg_dir, use_sandbox=False)
if pkg_instance:
pkg_mgr = pkg_instance.manager
print("[plugin-loader] 正在搜索可用插件...")
results = pkg_mgr.search()
if not results:
print("[plugin-loader] 未找到远程插件")
return
print(f"[plugin-loader] 发现 {len(results)} 个插件,开始安装...")
installed_count = 0
for pkg_info in results:
print(f"[plugin-loader] 安装: {pkg_info.name}")
if pkg_mgr.install(pkg_info.name):
installed_count += 1
if installed_count > 0:
print(f"[plugin-loader] 已安装 {installed_count} 个插件,重新扫描加载...")
# pkg 保留,重新加载其他插件
except Exception as e:
print(f"[plugin-loader] 引导安装失败: {e}")
else:
print("[plugin-loader] pkg 插件不存在,跳过引导安装")
def _sort_by_dependencies(self, dep_plugin):
"""按依赖关系排序"""
if not dep_plugin:
return
# 添加所有插件的依赖
for name, info in self.plugins.items():
deps = info["info"].dependencies
dep_plugin.add_plugin(name, deps)
try:
order = dep_plugin.resolve()
# 重新排序 plugins
sorted_plugins = {}
for name in order:
if name in self.plugins:
sorted_plugins[name] = self.plugins[name]
# 检查是否所有插件都在排序结果中
missing = set(self.plugins.keys()) - set(sorted_plugins.keys())
for name in missing:
sorted_plugins[name] = self.plugins[name]
self.plugins = sorted_plugins
except Exception as e:
print(f"[plugin-loader] 依赖解析失败: {e}")
def _link_capabilities(self):
"""关联能力:带权限检查"""
for plugin_name, info in self.plugins.items():
caps = info["info"].capabilities
allowed = info.get("permissions", [])
for cap in caps:
# 如果这个插件是某个能力的提供者
if self.capability_registry.has_capability(cap):
# 找到所有需要这个能力的消费者
consumers = self.capability_registry.get_consumers(cap)
for consumer_name in consumers:
if consumer_name in self.plugins:
consumer_info = self.plugins[consumer_name]["info"]
consumer_allowed = self.plugins[consumer_name].get("permissions", [])
# 权限检查
try:
provider = self.capability_registry.get_provider(
cap,
requester=consumer_name,
allowed_plugins=consumer_allowed
)
if provider and hasattr(consumer_info, "extensions"):
consumer_info.extensions[f"_{cap}_provider"] = provider
except PermissionError as e:
print(f"[plugin-loader] 权限拒绝: {e}")
def start_all(self):
"""启动所有插件(假设已初始化)"""
# 注入依赖实例
self._inject_dependencies()
# 启动所有插件
for name, info in self.plugins.items():
try:
info["instance"].start()
except Exception as e:
print(f"[plugin-loader] 启动失败 {name}: {e}")
def init_and_start_all(self):
"""初始化并启动所有插件
正确顺序:
1. 注入依赖实例
2. 按拓扑顺序 init() 所有插件
3. 按拓扑顺序 start() 所有插件
"""
print(f"[plugin-loader] init_and_start_all 被调用plugins={len(self.plugins)}")
# 1. 注入依赖实例
self._inject_dependencies()
# 2. 获取拓扑排序
ordered_plugins = self._get_ordered_plugins()
print(f"[plugin-loader] 插件启动顺序: {' -> '.join(ordered_plugins)}")
# 3. 初始化所有插件(跳过 plugin-loader 自己)
print("[plugin-loader] 开始初始化所有插件...")
for name in ordered_plugins:
if "plugin-loader" in name:
continue
info = self.plugins[name]
try:
print(f"[plugin-loader] 初始化: {name}")
info["instance"].init()
except Exception as e:
print(f"[plugin-loader] 初始化失败 {name}: {e}")
# 4. 启动所有插件(跳过 plugin-loader 自己)
print("[plugin-loader] 开始启动所有插件...")
for name in ordered_plugins:
if "plugin-loader" in name:
continue
info = self.plugins[name]
try:
print(f"[plugin-loader] 启动: {name}")
info["instance"].start()
except Exception as e:
print(f"[plugin-loader] 启动失败 {name}: {e}")
def _get_ordered_plugins(self) -> list[str]:
"""获取按依赖排序的插件列表"""
# 如果没有 dependency 插件,直接返回原始顺序
if not hasattr(self, '_dependency_plugin') or not self._dependency_plugin:
return list(self.plugins.keys())
try:
# 使用 dependency 插件解析
order = self._dependency_plugin.resolve()
# 过滤出实际存在的插件
return [name for name in order if name in self.plugins]
except Exception as e:
print(f"[plugin-loader] 依赖解析失败,使用原始顺序: {e}")
return list(self.plugins.keys())
def _inject_dependencies(self):
"""注入插件依赖实例"""
print(f"[plugin-loader] 开始注入依赖,共 {len(self.plugins)} 个插件")
# 构建名称映射(处理 } 后缀问题)
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():
instance = info["instance"]
info_obj = info.get("info")
if not info_obj:
continue
deps = info_obj.dependencies
if not deps:
continue
print(f"[plugin-loader] {name} 依赖: {deps}")
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_name = f"set_{dep_name.replace('-', '_')}"
print(f"[plugin-loader] 尝试注入: {name} <- {actual_dep} ({setter_name})")
if hasattr(instance, setter_name):
try:
getattr(instance, setter_name)(dep_instance)
print(f"[plugin-loader] 注入成功: {name} <- {actual_dep}")
except Exception as e:
print(f"[plugin-loader] 注入依赖失败 {name}.{setter_name}: {e}")
else:
print(f"[plugin-loader] 警告: {name} 没有 {setter_name} 方法")
def stop_all(self):
"""停止所有插件"""
for name, info in reversed(list(self.plugins.items())):
try:
info["instance"].stop()
except Exception:
pass
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
def init(self, deps: dict = None):
"""加载所有插件"""
if self._loaded:
return
self._loaded = True
print("[plugin-loader] 开始加载插件...")
self.manager.load_all()
def start(self):
"""启动所有插件"""
if self._started:
return
self._started = True
print("[plugin-loader] 启动插件...")
self.manager.init_and_start_all()
def stop(self):
"""停止所有插件"""
print("[plugin-loader] 停止插件...")
self.manager.stop_all()
# 注册类型
register_plugin_type("PluginManager", PluginManager)
register_plugin_type("PluginInfo", PluginInfo)
register_plugin_type("CapabilityRegistry", CapabilityRegistry)
def New():
return PluginLoaderPlugin()

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