⚡ 初始提交 - FutureOSS v1.0 插件化运行时框架
一切皆为插件的开发者工具运行时框架
🧩 核心特性:
- 插件热插拔 (importlib 动态加载)
- 依赖自动解析 (拓扑排序 + 循环检测)
- 企业级稳定 (熔断/降级/重试/隔离)
- 事件驱动 (发布/订阅事件总线)
- 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
19
.dockerignore
Normal file
19
.dockerignore
Normal 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
24
.gitignore
vendored
Normal 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
46
Dockerfile
Normal 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
190
LICENSE
Normal 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
106
README.md
Normal 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
43
docker-compose.yml
Normal 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
2
oss/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Future OSS"""
|
||||||
|
__version__ = "1.0.0"
|
||||||
BIN
oss/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
oss/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/__pycache__/cli.cpython-313.pyc
Normal file
BIN
oss/__pycache__/cli.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/__pycache__/oss_parser.cpython-313.pyc
Normal file
BIN
oss/__pycache__/oss_parser.cpython-313.pyc
Normal file
Binary file not shown.
56
oss/cli.py
Normal file
56
oss/cli.py
Normal 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()
|
||||||
BIN
oss/config/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
oss/config/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/config/__pycache__/config.cpython-313.pyc
Normal file
BIN
oss/config/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/logger/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
oss/logger/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/logger/__pycache__/logger.cpython-313.pyc
Normal file
BIN
oss/logger/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
15
oss/logger/logger.py
Normal file
15
oss/logger/logger.py
Normal 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}")
|
||||||
BIN
oss/plugin/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/capabilities.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/capabilities.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/event_bus.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/event_bus.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/loader.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/loader.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/manager.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/manager.cpython-313.pyc
Normal file
Binary file not shown.
BIN
oss/plugin/__pycache__/types.cpython-313.pyc
Normal file
BIN
oss/plugin/__pycache__/types.cpython-313.pyc
Normal file
Binary file not shown.
73
oss/plugin/capabilities.py
Normal file
73
oss/plugin/capabilities.py
Normal 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
125
oss/plugin/loader.py
Normal 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
33
oss/plugin/manager.py
Normal 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
92
oss/plugin/types.py
Normal 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
17
pyproject.toml
Normal 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
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
click>=8.0
|
||||||
|
pyyaml>=6.0
|
||||||
|
websockets>=12.0
|
||||||
146
start.bat
Normal file
146
start.bat
Normal 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
180
start.sh
Executable 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
421
static/banner.svg
Normal 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 && 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 |
41
store/@{Falck}/html-render/README.md
Normal file
41
store/@{Falck}/html-render/README.md
Normal 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-toolkit:Web 服务
|
||||||
|
- plugin-storage:HTML 存储
|
||||||
BIN
store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
99
store/@{Falck}/html-render/main.py
Normal file
99
store/@{Falck}/html-render/main.py
Normal 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()
|
||||||
17
store/@{Falck}/html-render/manifest.json
Normal file
17
store/@{Falck}/html-render/manifest.json
Normal 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"]
|
||||||
|
}
|
||||||
71
store/@{Falck}/web-toolkit/README.md
Normal file
71
store/@{Falck}/web-toolkit/README.md
Normal 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-api:HTTP 服务
|
||||||
|
- http-tcp:TCP HTTP 服务
|
||||||
BIN
store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc
Normal file
Binary file not shown.
158
store/@{Falck}/web-toolkit/main.py
Normal file
158
store/@{Falck}/web-toolkit/main.py
Normal 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()
|
||||||
21
store/@{Falck}/web-toolkit/manifest.json
Normal file
21
store/@{Falck}/web-toolkit/manifest.json
Normal 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"]
|
||||||
|
}
|
||||||
63
store/@{Falck}/web-toolkit/router.py
Normal file
63
store/@{Falck}/web-toolkit/router.py
Normal 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
|
||||||
69
store/@{Falck}/web-toolkit/static.py
Normal file
69
store/@{Falck}/web-toolkit/static.py
Normal 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()]
|
||||||
144
store/@{Falck}/web-toolkit/template.py
Normal file
144
store/@{Falck}/web-toolkit/template.py
Normal 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)
|
||||||
30
store/@{FutureOSS}/circuit-breaker/README.md
Normal file
30
store/@{FutureOSS}/circuit-breaker/README.md
Normal 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("熔断器已触发")
|
||||||
|
```
|
||||||
Binary file not shown.
70
store/@{FutureOSS}/circuit-breaker/main.py
Normal file
70
store/@{FutureOSS}/circuit-breaker/main.py
Normal 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()
|
||||||
17
store/@{FutureOSS}/circuit-breaker/manifest.json
Normal file
17
store/@{FutureOSS}/circuit-breaker/manifest.json
Normal 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": []
|
||||||
|
}
|
||||||
39
store/@{FutureOSS}/dependency/README.md
Normal file
39
store/@{FutureOSS}/dependency/README.md
Normal 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"]
|
||||||
|
}
|
||||||
|
```
|
||||||
BIN
store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
138
store/@{FutureOSS}/dependency/main.py
Normal file
138
store/@{FutureOSS}/dependency/main.py
Normal 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 依赖 B,B 依赖 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()
|
||||||
15
store/@{FutureOSS}/dependency/manifest.json
Normal file
15
store/@{FutureOSS}/dependency/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "dependency",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "FutureOSS",
|
||||||
|
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
|
||||||
|
"type": "core"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"enabled": true,
|
||||||
|
"args": {}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
32
store/@{FutureOSS}/hot-reload/README.md
Normal file
32
store/@{FutureOSS}/hot-reload/README.md
Normal 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()`
|
||||||
BIN
store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
196
store/@{FutureOSS}/hot-reload/main.py
Normal file
196
store/@{FutureOSS}/hot-reload/main.py
Normal 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()
|
||||||
18
store/@{FutureOSS}/hot-reload/manifest.json
Normal file
18
store/@{FutureOSS}/hot-reload/manifest.json
Normal 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"]
|
||||||
|
}
|
||||||
53
store/@{FutureOSS}/http-api/README.md
Normal file
53
store/@{FutureOSS}/http-api/README.md
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
BIN
store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-api/__pycache__/server.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
58
store/@{FutureOSS}/http-api/events.py
Normal file
58
store/@{FutureOSS}/http-api/events.py
Normal 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"
|
||||||
68
store/@{FutureOSS}/http-api/main.py
Normal file
68
store/@{FutureOSS}/http-api/main.py
Normal 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()
|
||||||
18
store/@{FutureOSS}/http-api/manifest.json
Normal file
18
store/@{FutureOSS}/http-api/manifest.json
Normal 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"]
|
||||||
|
}
|
||||||
57
store/@{FutureOSS}/http-api/middleware.py
Normal file
57
store/@{FutureOSS}/http-api/middleware.py
Normal 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()
|
||||||
72
store/@{FutureOSS}/http-api/router.py
Normal file
72
store/@{FutureOSS}/http-api/router.py
Normal 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
|
||||||
110
store/@{FutureOSS}/http-api/server.py
Normal file
110
store/@{FutureOSS}/http-api/server.py
Normal 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
|
||||||
51
store/@{FutureOSS}/http-tcp/README.md
Normal file
51
store/@{FutureOSS}/http-tcp/README.md
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
BIN
store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-tcp/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
21
store/@{FutureOSS}/http-tcp/events.py
Normal file
21
store/@{FutureOSS}/http-tcp/events.py
Normal 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"
|
||||||
34
store/@{FutureOSS}/http-tcp/main.py
Normal file
34
store/@{FutureOSS}/http-tcp/main.py
Normal 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()
|
||||||
18
store/@{FutureOSS}/http-tcp/manifest.json
Normal file
18
store/@{FutureOSS}/http-tcp/manifest.json
Normal 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": []
|
||||||
|
}
|
||||||
53
store/@{FutureOSS}/http-tcp/middleware.py
Normal file
53
store/@{FutureOSS}/http-tcp/middleware.py
Normal 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()
|
||||||
63
store/@{FutureOSS}/http-tcp/router.py
Normal file
63
store/@{FutureOSS}/http-tcp/router.py
Normal 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
|
||||||
193
store/@{FutureOSS}/http-tcp/server.py
Normal file
193
store/@{FutureOSS}/http-tcp/server.py
Normal 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())
|
||||||
83
store/@{FutureOSS}/json-codec/README.md
Normal file
83
store/@{FutureOSS}/json-codec/README.md
Normal 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}")
|
||||||
|
```
|
||||||
BIN
store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
161
store/@{FutureOSS}/json-codec/main.py
Normal file
161
store/@{FutureOSS}/json-codec/main.py
Normal 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()
|
||||||
15
store/@{FutureOSS}/json-codec/manifest.json
Normal file
15
store/@{FutureOSS}/json-codec/manifest.json
Normal 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": []
|
||||||
|
}
|
||||||
30
store/@{FutureOSS}/lifecycle/README.md
Normal file
30
store/@{FutureOSS}/lifecycle/README.md
Normal 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()
|
||||||
|
```
|
||||||
BIN
store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
150
store/@{FutureOSS}/lifecycle/main.py
Normal file
150
store/@{FutureOSS}/lifecycle/main.py
Normal 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()
|
||||||
15
store/@{FutureOSS}/lifecycle/manifest.json
Normal file
15
store/@{FutureOSS}/lifecycle/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "lifecycle",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "FutureOSS",
|
||||||
|
"description": "生命周期管理 - 管理插件的状态转换和钩子",
|
||||||
|
"type": "core"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"enabled": true,
|
||||||
|
"args": {}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
43
store/@{FutureOSS}/pkg/README.md
Normal file
43
store/@{FutureOSS}/pkg/README.md
Normal 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
|
||||||
|
```
|
||||||
BIN
store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
201
store/@{FutureOSS}/pkg/main.py
Normal file
201
store/@{FutureOSS}/pkg/main.py
Normal 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()
|
||||||
18
store/@{FutureOSS}/pkg/manifest.json
Normal file
18
store/@{FutureOSS}/pkg/manifest.json
Normal 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": []
|
||||||
|
}
|
||||||
77
store/@{FutureOSS}/plugin-bridge/README.md
Normal file
77
store/@{FutureOSS}/plugin-bridge/README.md
Normal 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()
|
||||||
|
```
|
||||||
Binary file not shown.
203
store/@{FutureOSS}/plugin-bridge/main.py
Normal file
203
store/@{FutureOSS}/plugin-bridge/main.py
Normal 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()
|
||||||
15
store/@{FutureOSS}/plugin-bridge/manifest.json
Normal file
15
store/@{FutureOSS}/plugin-bridge/manifest.json
Normal 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"]
|
||||||
|
}
|
||||||
16
store/@{FutureOSS}/plugin-loader/README.md
Normal file
16
store/@{FutureOSS}/plugin-loader/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# plugin-loader 插件加载器
|
||||||
|
|
||||||
|
核心插件,负责扫描、加载和管理所有其他插件。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 自动扫描 `store/` 和 `./data/pkg/` 目录
|
||||||
|
- 动态加载 `main.py` 并调用 `New()` 获取实例
|
||||||
|
- 解析 `manifest.json` 获取插件元数据
|
||||||
|
- 自动扫描插件能力(AST 分析)
|
||||||
|
- 按依赖关系排序加载顺序
|
||||||
|
- 关联能力提供者与消费者
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
无需手动使用,框架启动时自动加载。
|
||||||
Binary file not shown.
609
store/@{FutureOSS}/plugin-loader/main.py
Normal file
609
store/@{FutureOSS}/plugin-loader/main.py
Normal 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
Reference in New Issue
Block a user