删除了不需要的文件

This commit is contained in:
Falck
2026-04-26 16:55:22 +08:00
committed by GitHub
parent f1625df952
commit 1295aaed91
21 changed files with 0 additions and 3153 deletions

1
.pid
View File

@@ -1 +0,0 @@
18490

287
.pylintrc
View File

@@ -1,287 +0,0 @@
[MASTER]
jobs=4
persistent=yes
rcfile=
load-plugins=
extension-pkg-whitelist=
ignore-patterns=^test_.*\.py$
ignore=CVS
output-format=colorized
reports=yes
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
score=yes
fail-under=7.0
[MESSAGES CONTROL]
disable=all
enable=
missing-docstring,
empty-docstring,
invalid-name,
too-few-public-methods,
too-many-arguments,
too-many-instance-attributes,
too-many-locals,
too-many-public-methods,
too-many-statements,
redefined-builtin,
redefined-outer-name,
unused-argument,
unused-import,
unused-variable,
unused-wildcard-import,
wrong-import-order,
wrong-import-position,
import-error,
no-name-in-module,
no-member,
no-self-use,
not-callable,
undefined-variable,
used-before-assignment,
broad-except,
bare-except,
try-except-raise,
duplicate-code,
fixme,
trailing-whitespace,
bad-whitespace,
line-too-long,
missing-final-newline,
mixed-line-endings,
bad-continuation,
trailing-newlines,
multiple-statements,
anomalous-backslash-in-string,
deprecated-module,
deprecated-method,
super-with-arguments,
raise-missing-from,
consider-using-f-string,
consider-using-with,
use-implicit-booleaness-not-comparison,
use-list-literal,
use-dict-literal,
consider-using-enumerate,
consider-iterating-dictionary,
consider-using-set-comprehension,
consider-using-generator,
consider-using-any-or-all,
consider-using-in,
consider-using-max-builtin,
consider-using-min-builtin,
consider-using-sum,
consider-merging-isinstance,
chained-comparison,
simplifiable-if-expression,
unnecessary-lambda,
unnecessary-comprehension,
unnecessary-dunder-call,
unnecessary-pass,
unnecessary-ellipsis,
useless-else-on-loop,
useless-return,
useless-object-inheritance,
useless-suppression,
wrong-spelling-in-comment,
wrong-spelling-in-docstring
[REPORTS]
output-format=text
files-output=no
reports=yes
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
score=yes
[REFACTORING]
max-nested-blocks=5
max-line-length=88
max-module-lines=1000
max-statements=50
max-args=5
max-locals=15
max-returns=6
max-branches=12
max-statements-in-a-loop=20
max-public-methods=20
max-attributes=7
max-parents=7
max-bool-expr=5
[BASIC]
good-names=i,
j,
k,
ex,
Run,
_,
__,
fd,
msg,
v,
var,
x,
y,
z,
ax,
fig,
plt,
df,
idx,
cnt,
doc,
env,
app,
req,
res,
cls,
self,
mcs,
obj,
mod,
pkgs,
pkg,
cfg,
conf,
config,
opts,
args,
kwargs,
logger,
log
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
docstring-min-length=-1
[FORMAT]
max-line-length=88
ignore-long-lines=^\\s*(# )?<?https?://\\S+>?$
single-line-if-stmt=no
single-line-class-stmt=no
max-module-lines=1000
indent-string=' '
[SIMILARITIES]
min-similarity-lines=4
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=no
[TYPECHECK]
ignored-modules=
ignored-classes=optparse.Values,thread._local,_thread._local
generated-members=REQUEST,acl_users,aq_parent
contextmanager-decorators=contextlib.contextmanager
missing-member-hint=yes
missing-member-hint-distance=1
missing-member-max-choices=1
missing-member-local-gt=2
ignore-on-opaque-inference=yes
ignored-checks-for-mixins=
signature-mutators=
ignore-mixin-members=yes
ignore-none=yes
ignored-parents=
ignore-erase=no
ignore-import-error=yes
ignore-missing-imports=yes
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
additional-builtins=
[VARIABLES]
additional-builtins=
init-import=no
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
callbacks=cb_,_cb
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
allow-global-unused-variables=no
[LOGGING]
logging-modules=logging
[MISCELLANEOUS]
notes=FIXME,
XXX,
TODO,
HACK,
BUG,
NOTE,
OPTIMIZE,
REVIEW,
WARNING,
DEPRECATED
max-string-length=100
[DESIGN]
max-args=5
max-locals=15
max-returns=6
max-branches=12
max-statements=50
max-parents=7
max-attributes=7
min-public-methods=2
max-public-methods=20
[IMPORTS]
deprecated-modules=
import-graph=
ext-import-graph=
int-import-graph=
known-standard-library=
known-third-party=
known-local-folder=
preferred-modules=
allow-wildcard-with-all=no
allow-any-import-level=no
allow-relative-imports=yes
allow-from-import-under-package=yes
allow-import-from-same-module=no
allow-import-from-package=yes
allow-unused-imports=no
allow-cyclic-import=no
cyclic-import-limit=10
ignore-imports=no
ignore-import-error=yes
ignore-missing-imports=yes
preferred-modules=
single-line-exceptions=no
[EXCEPTIONS]
overgeneral-exceptions=Exception,BaseException,StandardError,ArithmeticError,LookupError,EnvironmentError,EOFError,ImportError
ignore-on-exception=no
[CLASSES]
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
valid-classmethod-first-arg=cls
valid-metaclass-classmethod-first-arg=mcs
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
bad-dunder-names=__authors__,
__version__,
__date__,
__credits__,
__status__,
__maintainer__,
__email__,
__contact__,
__copyright__,
__license__,
__uri__,
__url__,
__program__,
__description__,
__build__
[STRING]
check-str-concat-over-line-jumps=no

661
ai.md
View File

@@ -1,661 +0,0 @@
# FutureOSS - AI 专用项目介绍
## 项目概述
**FutureOSS** 是一个面向开发者的插件化运行时框架,采用「一切皆为插件」的设计理念。它是一个轻量级、安全、灵活的底层支撑系统,适用于构建微服务、开发工具链和可扩展的业务系统。
**核心设计哲学**最小化核心框架最大化插件扩展能力。核心框架仅提供最基本的插件加载和管理功能所有其他功能HTTP服务、Web界面、日志系统等都通过插件实现。
## 技术栈
- **语言**: Python 3.10+
- **主要依赖**:
- `click`: CLI 框架
- `pyyaml`: 配置解析
- `websockets`: WebSocket 支持
- `psutil`: 系统监控
- `cryptography`: 加密和签名验证
- **架构**: 插件化微内核架构
- **协议支持**: HTTP RESTful API, WebSocket, TCP HTTP
- **部署**: Docker 容器化支持
## 项目结构
```
/root/future-oss/
├── 📁 oss/ # 核心框架代码
│ ├── cli.py # CLI 命令入口
│ ├── config/ # 配置系统
│ ├── logger/ # 日志系统
│ ├── plugin/ # 插件框架核心
│ │ ├── capabilities.py # 能力接口定义
│ │ ├── loader.py # 插件加载器
│ │ ├── manager.py # 插件生命周期管理
│ │ └── types.py # 类型定义
│ └── shared/ # 共享组件
│ └── router.py # 统一路由系统
├── 📁 store/ # 插件仓库(核心功能)
│ ├── @{FutureOSS}/ # 官方核心插件
│ │ ├── plugin-loader/ # 插件加载器(核心)
│ │ ├── http-api/ # HTTP API 服务
│ │ ├── http-tcp/ # TCP HTTP 服务
│ │ ├── ws-api/ # WebSocket API
│ │ ├── dashboard/ # Web 控制台
│ │ ├── dependency/ # 依赖解析
│ │ ├── signature-verifier/ # 签名验证
│ │ ├── plugin-bridge/ # 插件间通信
│ │ ├── plugin-storage/ # 数据持久化
│ │ ├── pkg-manager/ # 包管理
│ │ ├── log-terminal/ # 日志终端
│ │ ├── json-codec/ # JSON 编解码器
│ │ └── webui/ # Web 用户界面
│ └── @{Falck}/ # 社区插件
│ ├── html-render/ # HTML 渲染引擎
│ └── web-toolkit/ # Web 开发工具集
├── 📁 data/ # 运行时数据目录
├── 📁 static/ # 静态资源
├── 📁 templates/ # 模板文件
├── 📁 tools/ # 开发工具脚本
├── 📁 video/ # 演示视频和文档
├── 📄 pyproject.toml # Python 项目配置
├── 📄 requirements.txt # Python 依赖
├── 📄 docker-compose.yml # Docker 编排配置
├── 📄 Dockerfile # Docker 构建文件
├── 📄 README.md # 项目说明文档
└── 📄 LICENSE # Apache 2.0 许可证
```
## 核心架构
### 1. 插件系统架构
FutureOSS 采用三层插件架构:
1. **核心框架层** (`oss/`): 提供最基本的插件加载和管理能力
2. **核心插件层** (`store/@{FutureOSS}/`): 官方提供的核心功能插件
3. **社区插件层** (`store/@{Falck}/`): 第三方社区插件
### 2. 插件生命周期
```
加载 (load) → 初始化 (init) → 启动 (start) → 运行 (run) → 停止 (stop)
```
### 3. 插件元数据格式
每个插件必须包含 `manifest.json` 文件,格式如下:
```json
{
"metadata": {
"name": "插件名称",
"version": "版本号",
"author": "作者",
"description": "功能描述",
"type": "插件类型 (core/protocol/utility)"
},
"config": {
"enabled": true/false,
"args": {
"参数名": "参数值"
}
},
"dependencies": ["依赖插件列表"],
"permissions": ["所需权限列表"]
}
```
### 4. 安全机制
- **数字签名验证**: 每个插件包含 SIGNATURE 文件
- **权限分级控制**: 插件声明所需权限
- **沙箱环境**: 可选的安全隔离
- **来源验证**: 插件作者命名空间 (@{作者名})
## 核心插件功能
### 系统核心插件
1. **plugin-loader**: 插件扫描、加载与生命周期管理(必需)
2. **http-api**: HTTP RESTful API 服务(端口 8080
3. **http-tcp**: TCP 高性能 HTTP 服务(端口 8082
4. **ws-api**: WebSocket API 服务(端口 8081
5. **dashboard**: Web 可视化监控仪表盘
6. **dependency**: 插件依赖解析与自动安装
7. **signature-verifier**: 插件数字签名验证
8. **plugin-bridge**: 插件间通信桥接
9. **plugin-storage**: 插件数据持久化存储
10. **pkg-manager**: 插件包管理(安装/卸载/搜索)
11. **log-terminal**: 日志终端实时输出
12. **json-codec**: 统一 JSON 编解码器
13. **webui**: Web 用户界面框架
### 社区插件
1. **html-render**: HTML 模板渲染引擎
2. **web-toolkit**: Web 开发工具集(静态文件/模板/路由)
### 禁用插件(默认不加载)
1. **hot-reload**: 开发模式热重载
2. **i18n**: 国际化支持
3. **lifecycle**: 插件生命周期钩子
4. **code-reviewer**: 代码审查工具
5. **plugin-loader-pro**: 高级插件加载器
## PL 注入机制
PL 注入是 plugin-loader 插件提供的一种扩展机制,允许插件通过 `PL/` 文件夹向插件加载器注册自定义功能。
### 工作原理
插件加载器在启动时自动扫描所有插件,检查其 `manifest.json` 中是否声明了 `pl_injection` 配置项:
```
插件加载器启动
扫描所有插件 manifest.json
检查 config.args.pl_injection
├── true → 检查 PL/ 文件夹
│ ├── 存在 PL/main.py → 沙箱执行 → 调用 register(injector) → ✅ 正常加载
│ └── 缺少 PL/ 或 PL/main.py → ⚠️ 警告并 ❌ 拒绝加载
└── false/未声明 → ✅ 正常加载(跳过 PL 检查)
```
### 使用方式
#### 1. 在 manifest.json 中声明
```json
{
"config": {
"args": {
"pl_injection": true
}
}
}
```
#### 2. 创建 PL/main.py
```
store/@{作者名}/插件名/
├── manifest.json # 声明 pl_injection: true
├── main.py # 插件主逻辑
├── PL/ # PL 注入文件夹
│ └── main.py # 注入逻辑(必须包含 register() 函数)
└── README.md
```
#### 3. 实现 register() 函数
```python
# PL/main.py
def register(injector):
"""向插件加载器注册功能
Args:
injector: PLInjector 实例
"""
# 注册普通功能
injector.register_function("my_helper", my_func, "功能描述")
# 注册 HTTP 路由
injector.register_route("GET", "/pl/hello", handler)
# 注册事件处理器
injector.register_event_handler("plugin.started", on_started)
```
### 注入器 API
| 方法 | 说明 |
|------|------|
| `register_function(name, func, description="")` | 注册注入功能 |
| `register_route(method, path, handler)` | 注册 HTTP 路由 |
| `register_event_handler(event_name, handler)` | 注册事件处理器 |
| `get_injected_functions(name=None)` | 获取已注册的注入功能 |
| `get_injection_info(plugin_name=None)` | 获取注入信息 |
| `has_injection(plugin_name)` | 检查插件是否有 PL 注入 |
| `get_registry_info()` | 获取注册表完整信息 |
### 安全限制
PL 注入机制实施了多层安全限制:
| 限制类型 | 具体措施 |
|---------|---------|
| **文件类型限制** | 禁止 PL 文件夹中包含 `.sh``.bat``.exe``.dll``.so` 等可执行文件 |
| **静态源码检查** | 编译前扫描源码,禁止导入 `os/sys/subprocess/socket/ctypes` 等系统模块,禁止 `exec/eval/compile/open/__import__` |
| **沙箱执行** | 在受限沙箱中执行 PL/main.py仅提供安全的 builtins |
| **参数校验** | 功能名称、路由路径、HTTP 方法、事件名称均通过正则校验 |
| **数量限制** | 每个插件最多注册 50 个功能,每个名称最多被注册 10 次 |
| **异常安全** | 所有注册函数自动包装 try-catch异常不影响主流程 |
| **调用者溯源** | 通过栈帧回溯自动识别调用者插件名,防止冒充注册 |
### 行为说明
| 场景 | 结果 |
|------|------|
| `pl_injection: true` + 存在 `PL/main.py` | ✅ 正常加载,执行注入 |
| `pl_injection: true` + 缺少 `PL/` 文件夹 | ❌ 警告并拒绝加载该插件 |
| `pl_injection: true` + 存在 `PL/` 但缺少 `main.py` | ❌ 警告并拒绝加载该插件 |
| 未声明 `pl_injection` | ✅ 正常加载,跳过 PL 检查 |
| `pl_injection: false` | ✅ 正常加载,跳过 PL 检查 |
## 开发与部署
### 开发环境设置
```bash
# 安装依赖
pip install -e .
# 启动开发服务器
oss serve
# 访问 Web 控制台
# http://localhost:8080
```
### Docker 部署
```bash
# 使用 Docker Compose
docker-compose up -d
# 暴露端口
# 8080: HTTP API + 网站
# 8081: WebSocket
# 8082: HTTP TCP
```
### 插件开发
1. **创建插件目录**: `store/@{作者名}/{插件名}/`
2. **编写 manifest.json**: 定义插件元数据
3. **实现 main.py**: 插件主逻辑
4. **添加 SIGNATURE**: 数字签名(可选)
5. **测试插件**: 通过 pkg-manager 安装测试
## API 接口
### HTTP API (端口 8080)
- `GET /health`: 健康检查
- `GET /api/plugins`: 获取插件列表
- `GET /api/plugins/{name}`: 获取插件详情
- `POST /api/plugins/{name}/enable`: 启用插件
- `POST /api/plugins/{name}/disable`: 禁用插件
### WebSocket API (端口 8081)
- 实时日志推送
- 系统状态监控
- 插件事件通知
### TCP HTTP (端口 8082)
- 高性能 HTTP 服务
- 兼容 HTTP/1.1 协议
## 配置系统
### 配置文件位置
1. **全局配置**: `config.yaml` (可选)
2. **插件配置**: `store/@{作者名}/{插件名}/config.json`
3. **环境变量**: 支持 Docker 环境变量覆盖
### 配置优先级
```
环境变量 > 全局配置文件 > 插件默认配置
```
## 数据存储
### 数据目录结构
```
data/
├── html-render/ # 网站渲染文件
├── web-toolkit/ # Web 工具配置
├── plugin-storage/ # 插件持久化数据
└── DCIM/ # 共享资源存储
```
### 存储类型
1. **临时存储**: 内存缓存
2. **持久化存储**: 文件系统 (data/ 目录)
3. **插件私有存储**: 每个插件独立存储空间
## 监控与日志
### 日志系统
- **日志级别**: DEBUG, INFO, WARNING, ERROR, CRITICAL
- **输出目标**: 控制台、文件、WebSocket
- **日志格式**: 结构化 JSON 日志
### 监控指标
- 系统资源使用率 (CPU/内存/磁盘)
- 插件运行状态
- API 请求统计
- 错误率和异常监控
## 扩展能力
### 自定义插件开发
FutureOSS 支持多种插件类型:
1. **协议插件**: 实现网络协议 (HTTP/WebSocket/TCP)
2. **工具插件**: 提供开发工具功能
3. **界面插件**: 扩展 Web 控制台
4. **存储插件**: 实现数据存储后端
5. **中间件插件**: 请求处理管道
### 插件间通信
- **事件系统**: 发布/订阅模式
- **直接调用**: 通过插件管理器
- **共享存储**: 通过 plugin-storage
- **消息队列**: 通过 plugin-bridge
## 最佳实践
### 性能优化
1. **懒加载插件**: 按需加载非核心插件
2. **连接池管理**: 数据库和网络连接复用
3. **缓存策略**: 合理使用内存缓存
4. **异步处理**: I/O 密集型操作异步化
### 安全建议
1. **签名验证**: 生产环境启用所有插件签名验证
2. **权限最小化**: 插件只申请必要权限
3. **沙箱隔离**: 不可信插件启用沙箱模式
4. **定期更新**: 及时更新插件和安全补丁
### 高可用性
1. **健康检查**: 配置完整的健康检查端点
2. **故障转移**: 关键插件多实例部署
3. **监控告警**: 设置系统监控和告警
4. **备份恢复**: 定期备份插件配置和数据
## 故障排除
### 常见问题
1. **插件加载失败**: 检查 manifest.json 格式和依赖
2. **服务启动失败**: 检查端口冲突和权限
3. **性能问题**: 监控系统资源使用情况
4. **内存泄漏**: 检查插件资源释放
### 调试工具
1. **日志终端**: 实时查看系统日志
2. **Web 控制台**: 可视化监控插件状态
3. **API 接口**: 通过 REST API 获取系统信息
4. **开发工具**: tools/ 目录下的辅助脚本
## 官方网站项目
### 项目位置
`/root/future-oss/website/` - FutureOSS 官方宣传网站
### 技术栈
- **后端**: Node.js + Express.js
- **前端**: EJS 模板引擎 + 原生 JavaScript + CSS3
- **构建工具**: npm
- **开发工具**: VS Code 运行与调试配置
### 项目结构
```
website/
├── 📁 public/ # 静态资源
│ ├── css/ # 样式文件
│ │ ├── main.css # 基础样式和变量
│ │ ├── components.css # 组件样式
│ │ ├── animations.css # 动画效果
│ │ └── pages/ # 页面特定样式
│ ├── js/ # JavaScript 文件
│ │ ├── main.js # 主逻辑
│ │ ├── router.js # 前端路由
│ │ ├── animations.js # 动画控制
│ │ └── pages/ # 页面特定脚本
│ └── images/ # 图片资源
├── 📁 src/ # 源代码
│ ├── server.js # Express 服务器
│ ├── router.js # 路由配置
│ ├── controllers/ # 控制器
│ │ ├── pagesController.js # 页面控制器
│ │ └── apiController.js # API 控制器
│ ├── middleware/ # 中间件
│ │ └── performance.js # 性能优化中间件
│ ├── components/ # 组件(预留)
│ ├── pages/ # 页面逻辑(预留)
│ └── styles/ # 样式源码(预留)
├── 📁 views/ # EJS 视图模板
│ ├── layouts/ # 布局文件
│ │ └── main.ejs # 主布局
│ ├── pages/ # 页面模板
│ │ ├── home.ejs # 首页
│ │ ├── features.ejs # 特性页
│ │ ├── architecture.ejs # 架构页
│ │ └── plugins.ejs # 插件页
│ └── partials/ # 局部组件
│ ├── navbar.ejs # 导航栏
│ └── footer.ejs # 页脚
├── 📄 package.json # npm 配置
├── 📄 package-lock.json # 依赖锁定
└── 📄 server.log # 服务器日志
```
### 核心功能
#### 1. 多页面网站
- **首页** (`/`): 项目介绍和快速开始
- **特性页** (`/features`): 核心功能展示
- **架构页** (`/architecture`): 技术架构说明
- **插件页** (`/plugins`): 插件生态系统
#### 2. 性能优化特性
- **响应时间监控**: X-Response-Time 头部
- **缓存控制**: 静态资源长期缓存
- **压缩支持**: Gzip 和预压缩
- **安全头**: CSP、XSS 保护等
- **内存监控**: 实时内存使用监控
#### 3. 用户体验增强
- **加载动画**: 页面加载旋转指示器
- **页面切换动画**: 平滑的页面过渡效果
- **图片懒加载**: 延迟加载非视口图片
- **骨架屏**: 内容加载占位符
- **响应式设计**: 完整的移动端适配
#### 4. API 接口
- `GET /api/health`: 健康检查
- `GET /api/metrics`: 性能指标
- `GET /api/info`: 服务器信息
- `GET /api/stress-test`: 压力测试(仅开发环境)
### 技术特点
#### 服务器特性
- **端口自动切换**: 8080被占用时自动使用8081、8082等
- **优雅关闭**: 支持 SIGTERM/SIGINT 信号优雅关闭
- **错误处理**: 完善的错误处理和404页面
- **中间件栈**: 完整的性能优化中间件
#### 前端特性
- **组件化设计**: 导航栏、页脚等独立组件
- **前端路由**: 支持无刷新页面切换
- **CSS 变量**: 统一的主题变量系统
- **动画系统**: 丰富的CSS动画和过渡效果
#### 开发工具
- **VS Code 调试配置**: 支持直接运行与调试
- **热重载支持**: nodemon 开发模式
- **性能监控**: 实时性能指标输出
- **日志系统**: 结构化服务器日志
### 部署与运行
#### 开发模式
```bash
cd /root/future-oss/website
npm install
npm start
# 访问 http://localhost:8080 (或自动切换的端口)
```
#### 生产部署
```bash
# 使用 PM2 进程管理
pm2 start src/server.js --name "futureoss-website"
# 使用 Docker
docker build -t futureoss-website .
docker run -p 8080:8080 futureoss-website
```
#### VS Code 调试
1. 打开运行与调试面板 (Ctrl+Shift+D)
2. 选择 "FutureOSS 网站: 启动Node.js服务器"
3. 按 F5 启动调试
### 性能优化措施
#### 已实施的优化
1. **响应头优化**: 缓存控制、安全头、压缩头
2. **静态资源优化**: 长期缓存、预压缩支持
3. **代码分割**: 按页面加载CSS和JS
4. **图片优化**: 懒加载、占位符、响应式图片
5. **动画优化**: 减少重绘、will-change提示
#### 监控指标
- **响应时间**: X-Response-Time 头部
- **内存使用**: X-Memory-Usage 头部(开发环境)
- **慢响应日志**: 超过1秒的请求记录
- **错误率**: 500错误监控和记录
### 已知问题和解决方案
#### 1. ERR_HTTP_HEADERS_SENT 错误
- **问题**: 在响应已发送后设置响应头
- **原因**: `responseTime` 中间件在 `finish` 事件中设置头部
- **解决方案**: 使用 `headers` 事件或重写 `end` 方法
- **修复代码**:
```javascript
// 错误写法(在 finish 事件中):
res.on('finish', () => {
res.setHeader('X-Response-Time', `${duration}ms`); // 错误!
});
// 正确写法(重写 end 方法):
const originalEnd = res.end;
res.end = function(...args) {
if (!res.headersSent) {
res.setHeader('X-Response-Time', `${duration}ms`);
}
return originalEnd.apply(this, args);
};
```
#### 2. CSS 加载问题
- **问题**: 页面只显示纯文本CSS未加载
- **原因**: EJS 布局系统未正确配置
- **解决方案**: 安装并配置 `express-ejs-layouts`
- **修复步骤**:
1. `npm install express-ejs-layouts`
2. 在 server.js 中添加中间件
3. 设置默认布局 `app.set('layout', 'layouts/main')`
#### 3. 加载性能问题
- **问题**: 页面加载慢,无加载反馈
- **解决方案**: 添加加载动画和性能优化
- **实施措施**:
1. 添加页面加载旋转动画
2. 实现图片懒加载
3. 添加骨架屏占位符
4. 优化静态资源缓存
### 扩展和定制
#### 添加新页面
1. 在 `views/pages/` 创建新的 `.ejs` 文件
2. 在 `controllers/pagesController.js` 添加渲染函数
3. 在 `src/router.js` 添加路由
4. 在 `public/css/pages/` 添加页面特定CSS
5. 在 `public/js/pages/` 添加页面特定JS
#### 添加新组件
1. 在 `views/partials/` 创建组件 `.ejs` 文件
2. 在 `public/css/components.css` 添加组件样式
3. 在布局或页面中使用 `<%- include('partials/组件名') %>`
#### 主题定制
通过修改 CSS 变量实现主题切换:
```css
:root {
--primary-color: #1e6fbb; /* 主色调 */
--text-primary: #333333; /* 主要文字 */
--bg-primary: #ffffff; /* 背景色 */
/* 更多变量... */
}
```
### 维护指南
#### 日常维护
1. **日志监控**: 定期检查 `server.log` 文件
2. **性能监控**: 关注慢响应日志
3. **错误处理**: 及时修复报告的错误
4. **依赖更新**: 定期更新 npm 包
#### 故障排除
1. **服务器无法启动**: 检查端口占用,查看日志
2. **页面样式异常**: 检查CSS文件路径和网络请求
3. **API 无响应**: 检查路由配置和控制器
4. **性能下降**: 使用 `/api/metrics` 接口诊断
#### 备份策略
1. **代码备份**: Git 版本控制
2. **配置备份**: 备份 `package.json` 和服务器配置
3. **数据备份**: 备份用户上传内容(如有)
4. **日志归档**: 定期归档服务器日志
## 未来发展方向
### 短期规划
1. 插件市场功能完善
2. 更多官方插件开发
3. 性能优化和稳定性提升
4. 官方网站功能增强
### 长期愿景
1. 跨语言插件支持
2. 云原生集成
3. 企业级功能扩展
4. 生态系统建设
## 注意事项
1. **生产部署**: 建议使用 Docker 容器化部署
2. **数据备份**: 定期备份 data/ 目录重要数据
3. **安全更新**: 关注安全公告并及时更新
4. **社区支持**: 通过 Gitee Issues 获取帮助
5. **网站维护**: 定期更新官方网站内容和功能
---
*本文件专为 AI 助手设计,提供 FutureOSS 项目的全面技术概述,帮助 AI 理解项目架构、功能和使用方式。更新日期2026年4月19日*

View File

@@ -1,39 +0,0 @@
"""CLI 命令单元测试"""
import pytest
from click.testing import CliRunner
from oss.cli import cli, version, serve
class TestCLI:
"""测试 CLI 命令"""
def test_version_command(self):
"""测试版本命令"""
runner = CliRunner()
result = runner.invoke(version)
assert result.exit_code == 0
assert "Future OSS" in result.output
assert "1.2.0" in result.output
def test_cli_help(self):
"""测试 CLI 帮助信息"""
runner = CliRunner()
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert "Future OSS" in result.output
assert "serve" in result.output
assert "version" in result.output
def test_serve_command_exists(self):
"""测试 serve 命令存在"""
runner = CliRunner()
result = runner.invoke(serve, ['--help'])
assert result.exit_code == 0
assert "启动 Future OSS" in result.output
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,105 +0,0 @@
"""配置管理测试"""
import os
import pytest
from pathlib import Path
import tempfile
import json
from oss.config.config import Config
class TestConfig:
"""配置管理测试类"""
def test_default_values(self):
"""测试默认配置值"""
config = Config()
assert config.http_api_port == 8080
assert config.http_tcp_port == 8082
assert config.host == "0.0.0.0"
assert config.log_level == "INFO"
assert config.permission_check is True
def test_env_override(self, monkeypatch):
"""测试环境变量覆盖"""
monkeypatch.setenv("HTTP_API_PORT", "9999")
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
config = Config()
assert config.http_api_port == 9999
assert config.log_level == "DEBUG"
def test_file_config(self):
"""测试配置文件加载"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump({"HTTP_API_PORT": 7777, "LOG_LEVEL": "WARNING"}, f)
temp_path = f.name
try:
config = Config(temp_path)
assert config.http_api_port == 7777
assert config.log_level == "WARNING"
finally:
os.unlink(temp_path)
def test_env_priority_over_file(self, monkeypatch):
"""测试环境变量优先级高于配置文件"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump({"HTTP_API_PORT": 7777}, f)
temp_path = f.name
try:
monkeypatch.setenv("HTTP_API_PORT", "8888")
config = Config(temp_path)
assert config.http_api_port == 8888 # 环境变量优先
finally:
os.unlink(temp_path)
monkeypatch.delenv("HTTP_API_PORT", raising=False)
def test_get_set(self):
"""测试 get/set 方法"""
config = Config()
assert config.get("HTTP_API_PORT") == 8080
config.set("HTTP_API_PORT", 6666)
assert config.get("HTTP_API_PORT") == 6666
def test_properties(self):
"""测试属性访问"""
config = Config()
assert isinstance(config.data_dir, Path)
assert isinstance(config.store_dir, Path)
assert config.data_dir.name == "data"
assert config.store_dir.name == "store"
def test_all_method(self):
"""测试 all() 方法返回所有配置"""
config = Config()
all_config = config.all()
assert "HTTP_API_PORT" in all_config
assert "HOST" in all_config
assert len(all_config) > 5
def test_bool_conversion(self, monkeypatch):
"""测试布尔值转换"""
monkeypatch.setenv("PERMISSION_CHECK", "false")
config = Config()
assert config.permission_check is False
monkeypatch.setenv("PERMISSION_CHECK", "true")
config = Config()
assert config.permission_check is True
def test_int_conversion(self, monkeypatch):
"""测试整数转换"""
monkeypatch.setenv("MAX_WORKERS", "8")
config = Config()
assert config.get("MAX_WORKERS") == 8
# 无效值应该保持默认
monkeypatch.setenv("MAX_WORKERS", "invalid")
config = Config()
assert config.get("MAX_WORKERS") == 4 # 默认值
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,48 +0,0 @@
"""插件加载器单元测试"""
import pytest
from pathlib import Path
from oss.plugin.loader import PluginLoader
class TestPluginLoader:
"""测试插件加载器核心功能"""
def test_loader_initialization(self):
"""测试加载器初始化"""
loader = PluginLoader()
assert loader.loaded == {}
def test_load_nonexistent_plugin(self):
"""测试加载不存在的插件"""
loader = PluginLoader()
result = loader.load_core_plugin("nonexistent-plugin")
assert result is None
def test_load_plugin_loader(self):
"""测试加载 plugin-loader 核心插件"""
loader = PluginLoader()
result = loader.load_core_plugin("plugin-loader")
assert result is not None
assert "instance" in result
assert "module" in result
assert "path" in result
assert "name" in result
assert result["name"] == "plugin-loader"
assert hasattr(result["instance"], "init")
assert hasattr(result["instance"], "start")
assert hasattr(result["instance"], "stop")
def test_loaded_plugins_tracking(self):
"""测试已加载插件跟踪"""
loader = PluginLoader()
initial_count = len(loader.loaded)
loader.load_core_plugin("plugin-loader")
assert len(loader.loaded) == initial_count + 1
assert "plugin-loader" in loader.loaded
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,53 +0,0 @@
"""插件管理器单元测试"""
import pytest
from oss.plugin.manager import PluginManager
class TestPluginManager:
"""测试插件管理器核心功能"""
def test_manager_initialization(self):
"""测试管理器初始化"""
manager = PluginManager()
assert manager.plugin_loader is None
assert manager.loader is not None
def test_manager_load(self):
"""测试管理器加载 plugin-loader"""
manager = PluginManager()
manager.load()
assert manager.plugin_loader is not None
assert hasattr(manager.plugin_loader, "init")
assert hasattr(manager.plugin_loader, "start")
assert hasattr(manager.plugin_loader, "stop")
def test_manager_start_without_load(self):
"""测试未加载时启动(应安全处理)"""
manager = PluginManager()
# 不应抛出异常
manager.start()
def test_manager_stop_without_load(self):
"""测试未加载时停止(应安全处理)"""
manager = PluginManager()
# 不应抛出异常
manager.stop()
def test_manager_lifecycle(self):
"""测试完整生命周期"""
manager = PluginManager()
# 加载
manager.load()
assert manager.plugin_loader is not None
# 启动(会初始化所有插件)
# 注意:实际启动需要完整环境,这里只测试方法存在
assert callable(manager.plugin_loader.init)
assert callable(manager.plugin_loader.start)
assert callable(manager.plugin_loader.stop)
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env python3
"""
为 dependency 和 signature-verifier 插件签名
"""
import sys
import json
import base64
import hashlib
import time
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
# ========== 配置 ==========
PROJECT_ROOT = Path(__file__).parent.parent
PRIVATE_KEY_FILE = PROJECT_ROOT / "data" / "signature-verifier" / "keys" / "private" / "futureoss_private.pem"
PLUGINS = ["dependency", "signature-verifier"]
def compute_plugin_hash(plugin_dir: Path) -> str:
"""计算插件目录的内容哈希"""
hasher = hashlib.sha256()
files_to_hash = []
for file_path in sorted(plugin_dir.rglob("*")):
if file_path.is_file() and file_path.name != "SIGNATURE":
rel_path = file_path.relative_to(plugin_dir)
files_to_hash.append((str(rel_path), file_path))
for rel_path, file_path in files_to_hash:
hasher.update(rel_path.encode("utf-8"))
hasher.update(file_path.read_bytes())
return hasher.hexdigest()
def sign_plugin(plugin_dir: Path):
"""为插件签名"""
# 加载私钥
print(f"加载私钥: {PRIVATE_KEY_FILE}")
private_key = serialization.load_pem_private_key(
PRIVATE_KEY_FILE.read_bytes(),
password=None,
backend=default_backend()
)
# 计算插件哈希
print(f"计算插件目录哈希: {plugin_dir.name}...")
plugin_hash = compute_plugin_hash(plugin_dir)
print(f"哈希: {plugin_hash}")
# 签名
signed_data = f"FutureOSS:{plugin_hash}".encode("utf-8")
signature = private_key.sign(
signed_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 写入签名文件
sig_data = {
"signature": base64.b64encode(signature).decode(),
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": time.time(),
"plugin_hash": plugin_hash,
"author": "FutureOSS"
}
signature_file = plugin_dir / "SIGNATURE"
signature_file.write_text(json.dumps(sig_data, indent=2))
print(f"✓ 已签名: {plugin_dir.name}")
def main():
if not PRIVATE_KEY_FILE.exists():
print(f"错误: 私钥文件不存在: {PRIVATE_KEY_FILE}")
sys.exit(1)
store_dir = PROJECT_ROOT / "store" / "@{FutureOSS}"
for plugin_name in PLUGINS:
plugin_dir = store_dir / plugin_name
if plugin_dir.exists():
sign_plugin(plugin_dir)
else:
print(f"警告: 插件目录不存在: {plugin_dir}")
print("\n完成!")
if __name__ == "__main__":
main()

View File

@@ -1,102 +0,0 @@
#!/usr/bin/env python3
"""
为 plugin-loader 插件签名
"""
import sys
import json
import base64
import hashlib
import time
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
# ========== 配置 ==========
PROJECT_ROOT = Path(__file__).parent.parent
PRIVATE_KEY_FILE = PROJECT_ROOT / "data" / "signature-verifier" / "keys" / "private" / "futureoss_private.pem"
PLUGIN_DIR = PROJECT_ROOT / "store" / "@{FutureOSS}" / "plugin-loader"
def compute_plugin_hash(plugin_dir: Path) -> str:
"""计算插件目录的内容哈希"""
hasher = hashlib.sha256()
files_to_hash = []
for file_path in sorted(plugin_dir.rglob("*")):
if file_path.is_file() and file_path.name != "SIGNATURE":
rel_path = file_path.relative_to(plugin_dir)
files_to_hash.append((str(rel_path), file_path))
for rel_path, file_path in files_to_hash:
hasher.update(rel_path.encode("utf-8"))
hasher.update(file_path.read_bytes())
return hasher.hexdigest()
def sign_plugin():
"""为插件签名"""
# 加载私钥
print(f"加载私钥: {PRIVATE_KEY_FILE}")
private_key = serialization.load_pem_private_key(
PRIVATE_KEY_FILE.read_bytes(),
password=None,
backend=default_backend()
)
# 计算插件哈希
print(f"计算插件目录哈希...")
plugin_hash = compute_plugin_hash(PLUGIN_DIR)
print(f"哈希: {plugin_hash}")
# 签名
signed_data = f"FutureOSS:{plugin_hash}".encode("utf-8")
signature = private_key.sign(
signed_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 写入签名文件
sig_data = {
"signature": base64.b64encode(signature).decode(),
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": time.time(),
"plugin_hash": plugin_hash,
"author": "FutureOSS"
}
signature_file = PLUGIN_DIR / "SIGNATURE"
signature_file.write_text(json.dumps(sig_data, indent=2))
print(f"\n✓ 签名成功!")
print(f" 插件: {PLUGIN_DIR.name}")
print(f" 签名文件: {signature_file}")
print(f" 算法: RSA-SHA256")
print(f" 时间戳: {time.strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
try:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
except ImportError:
print("错误: 未安装 cryptography 库")
print("运行: pip install cryptography")
sys.exit(1)
if not PLUGIN_DIR.exists():
print(f"错误: 插件目录不存在: {PLUGIN_DIR}")
sys.exit(1)
if not PRIVATE_KEY_FILE.exists():
print(f"错误: 私钥文件不存在: {PRIVATE_KEY_FILE}")
sys.exit(1)
sign_plugin()

View File

@@ -1,193 +0,0 @@
#!/usr/bin/env python3
"""
密钥生成与插件签名工具
- 生成 Falck 官方密钥对
- 为所有官方插件签名
"""
import sys
import json
import base64
import hashlib
import time
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.backends import default_backend
# ========== 配置 ==========
PROJECT_ROOT = Path(__file__).parent.parent # 修复tools 的上级目录
KEY_DIR = PROJECT_ROOT / "data" / "signature-verifier" / "keys"
STORE_DIR = PROJECT_ROOT / "store"
# 官方作者目录
OFFICIAL_AUTHORS = ["FutureOSS", "Falck"]
def generate_keypair(author: str):
"""生成 4096 位 RSA 密钥对"""
print(f"\n{'='*60}")
print(f"生成 {author} 的密钥对...")
print(f"{'='*60}")
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
public_key = private_key.public_key()
# 保存私钥
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
priv_dir = KEY_DIR / "private"
priv_dir.mkdir(parents=True, exist_ok=True)
priv_file = priv_dir / f"{author.lower()}_private.pem"
priv_file.write_bytes(private_pem)
print(f"私钥已保存: {priv_file}")
# 保存公钥
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
pub_dir = KEY_DIR / "public"
pub_dir.mkdir(parents=True, exist_ok=True)
pub_file = pub_dir / f"{author}.pem"
pub_file.write_bytes(public_pem)
print(f"公钥已保存: {pub_file}")
# 显示公钥(用于嵌入代码)
print(f"\n--- 公钥 PEM (用于嵌入 main.py) ---")
print(public_pem.decode())
print(f"--- END ---\n")
return private_key, public_key
def compute_plugin_hash(plugin_dir: Path) -> str:
"""计算插件目录的内容哈希"""
hasher = hashlib.sha256()
files_to_hash = []
for file_path in sorted(plugin_dir.rglob("*")):
if file_path.is_file() and file_path.name != "SIGNATURE":
rel_path = file_path.relative_to(plugin_dir)
files_to_hash.append((str(rel_path), file_path))
for rel_path, file_path in files_to_hash:
hasher.update(rel_path.encode("utf-8"))
hasher.update(file_path.read_bytes())
return hasher.hexdigest()
def sign_plugin(plugin_dir: Path, private_key, signer_name: str, author: str):
"""为插件生成签名"""
plugin_hash = compute_plugin_hash(plugin_dir)
# 签名
signed_data = f"{author}:{plugin_hash}".encode("utf-8")
signature = private_key.sign(
signed_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 写入签名文件
sig_data = {
"signature": base64.b64encode(signature).decode(),
"signer": signer_name,
"algorithm": "RSA-SHA256",
"timestamp": time.time(),
"plugin_hash": plugin_hash,
"author": author
}
signature_file = plugin_dir / "SIGNATURE"
signature_file.write_text(json.dumps(sig_data, indent=2))
print(f" ✓ 已签名: {plugin_dir.name} (哈希: {plugin_hash[:16]}...)")
def sign_all_plugins(private_key):
"""为所有官方插件签名"""
for author in OFFICIAL_AUTHORS:
author_dir = STORE_DIR / f"@{{{author}}}"
if not author_dir.exists():
print(f"\n警告: 作者目录不存在: {author_dir}")
continue
print(f"\n{'='*60}")
print(f"为 @{{{author}}} 的插件签名...")
print(f"{'='*60}")
count = 0
for plugin_dir in sorted(author_dir.iterdir()):
if plugin_dir.is_dir() and (plugin_dir / "manifest.json").exists():
sign_plugin(plugin_dir, private_key, author, author)
count += 1
print(f"\n完成: 已签名 {count} 个 @{author} 插件")
def main():
print("="*60)
print("FutureOSS 插件签名工具")
print("="*60)
# 检查 cryptography
try:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.backends import default_backend
except ImportError:
print("错误: 未安装 cryptography 库")
print("运行: pip install cryptography")
sys.exit(1)
# 步骤 1: 生成密钥对
print("\n步骤 1: 生成 Falck 官方密钥对...")
falck_priv, falck_pub = generate_keypair("Falck")
print("\n步骤 1b: 生成 FutureOSS 官方密钥对...")
foss_priv, foss_pub = generate_keypair("FutureOSS")
# 步骤 2: 为所有官方插件签名(使用对应的密钥)
print("\n步骤 2: 为所有官方插件签名...")
# Falck 的插件用 Falck 密钥签名
falck_dir = STORE_DIR / "@{Falck}"
if falck_dir.exists():
print(f"\n{'='*60}")
print("为 @{Falck} 的插件使用 Falck 密钥签名...")
print(f"{'='*60}")
for plugin_dir in sorted(falck_dir.iterdir()):
if plugin_dir.is_dir() and (plugin_dir / "manifest.json").exists():
sign_plugin(plugin_dir, falck_priv, "Falck", "Falck")
# FutureOSS 的插件用 FutureOSS 密钥签名
foss_dir = STORE_DIR / "@{FutureOSS}"
if foss_dir.exists():
print(f"\n{'='*60}")
print("为 @{FutureOSS} 的插件使用 FutureOSS 密钥签名...")
print(f"{'='*60}")
for plugin_dir in sorted(foss_dir.iterdir()):
if plugin_dir.is_dir() and (plugin_dir / "manifest.json").exists():
sign_plugin(plugin_dir, foss_priv, "FutureOSS", "FutureOSS")
print("\n" + "="*60)
print("全部完成!")
print("="*60)
print(f"\n密钥位置: {KEY_DIR}")
print("请将公钥嵌入 signature-verifier/main.py 的 FALCK_PUBLIC_KEY_PEM 变量")
print("并妥善保管私钥,不要提交到版本控制系统!")
if __name__ == "__main__":
main()

View File

@@ -1,102 +0,0 @@
#!/usr/bin/env python3
"""
为单个插件签名
"""
import sys
import json
import base64
import hashlib
import time
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
# ========== 配置 ==========
PROJECT_ROOT = Path(__file__).parent.parent
PRIVATE_KEY_FILE = PROJECT_ROOT / "data" / "signature-verifier" / "keys" / "private" / "futureoss_private.pem"
PLUGIN_DIR = PROJECT_ROOT / "store" / "@{FutureOSS}" / "log-terminal"
def compute_plugin_hash(plugin_dir: Path) -> str:
"""计算插件目录的内容哈希"""
hasher = hashlib.sha256()
files_to_hash = []
for file_path in sorted(plugin_dir.rglob("*")):
if file_path.is_file() and file_path.name != "SIGNATURE":
rel_path = file_path.relative_to(plugin_dir)
files_to_hash.append((str(rel_path), file_path))
for rel_path, file_path in files_to_hash:
hasher.update(rel_path.encode("utf-8"))
hasher.update(file_path.read_bytes())
return hasher.hexdigest()
def sign_plugin():
"""为插件签名"""
# 加载私钥
print(f"加载私钥: {PRIVATE_KEY_FILE}")
private_key = serialization.load_pem_private_key(
PRIVATE_KEY_FILE.read_bytes(),
password=None,
backend=default_backend()
)
# 计算插件哈希
print(f"计算插件目录哈希...")
plugin_hash = compute_plugin_hash(PLUGIN_DIR)
print(f"哈希: {plugin_hash}")
# 签名
signed_data = f"FutureOSS:{plugin_hash}".encode("utf-8")
signature = private_key.sign(
signed_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 写入签名文件
sig_data = {
"signature": base64.b64encode(signature).decode(),
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": time.time(),
"plugin_hash": plugin_hash,
"author": "FutureOSS"
}
signature_file = PLUGIN_DIR / "SIGNATURE"
signature_file.write_text(json.dumps(sig_data, indent=2))
print(f"\n✓ 签名成功!")
print(f" 插件: {PLUGIN_DIR.name}")
print(f" 签名文件: {signature_file}")
print(f" 算法: RSA-SHA256")
print(f" 时间戳: {time.strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
try:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
except ImportError:
print("错误: 未安装 cryptography 库")
print("运行: pip install cryptography")
sys.exit(1)
if not PLUGIN_DIR.exists():
print(f"错误: 插件目录不存在: {PLUGIN_DIR}")
sys.exit(1)
if not PRIVATE_KEY_FILE.exists():
print(f"错误: 私钥文件不存在: {PRIVATE_KEY_FILE}")
sys.exit(1)
sign_plugin()

View File

@@ -1,199 +0,0 @@
#!/usr/bin/env python3
"""
签名验证测试脚本
测试签名验证功能是否正常工作
"""
import sys
import json
import base64
import hashlib
from pathlib import Path
# 添加项目路径
sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent / "store/@{FutureOSS}/signature-verifier"))
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidSignature
# 导入签名验证插件
from main import SignatureVerifier, SignatureSigner
def test_verify_official_plugins():
"""测试验证所有已签名的官方插件"""
print("="*60)
print("测试 1: 验证所有官方插件签名")
print("="*60)
store_dir = Path(__file__).parent.parent / "store"
verifier = SignatureVerifier(key_dir="./data/signature-verifier/keys")
authors = ["FutureOSS", "Falck"]
total = 0
passed = 0
failed = 0
for author in authors:
author_dir = store_dir / f"@{{{author}}}"
if not author_dir.exists():
continue
print(f"\n--- @{author} ---")
for plugin_dir in sorted(author_dir.iterdir()):
if plugin_dir.is_dir() and (plugin_dir / "manifest.json").exists():
total += 1
valid, msg = verifier.verify_plugin(plugin_dir, author)
status = "✅ 通过" if valid else "❌ 失败"
print(f" {status}: {plugin_dir.name} - {msg}")
if valid:
passed += 1
else:
failed += 1
print(f"\n{'='*60}")
print(f"结果: {passed}/{total} 通过, {failed} 失败")
print(f"{'='*60}")
return failed == 0
def test_tamper_detection():
"""测试篡改检测"""
print("\n" + "="*60)
print("测试 2: 篡改检测")
print("="*60)
store_dir = Path(__file__).parent.parent / "store"
verifier = SignatureVerifier(key_dir="./data/signature-verifier/keys")
# 选择一个测试插件
test_plugin = store_dir / "@{FutureOSS}" / "dashboard"
if not test_plugin.exists():
print("跳过: dashboard 插件不存在")
return True
# 验证原始签名
valid_before, msg_before = verifier.verify_plugin(test_plugin, "FutureOSS")
print(f"\n篡改前: {'✅ 有效' if valid_before else '❌ 无效'} - {msg_before}")
if not valid_before:
print("警告: 原始签名已无效,跳过篡改测试")
return False
# 创建一个临时篡改文件
tamper_file = test_plugin / "__tamper_test__.tmp"
tamper_file.write_text("tampered content")
# 验证篡改后的签名
valid_after, msg_after = verifier.verify_plugin(test_plugin, "FutureOSS")
print(f"篡改后: {'✅ 有效' if valid_after else '❌ 无效'} - {msg_after}")
# 清理
tamper_file.unlink()
# 再次验证应该恢复有效
valid_clean, msg_clean = verifier.verify_plugin(test_plugin, "FutureOSS")
print(f"清理后: {'✅ 有效' if valid_clean else '❌ 无效'} - {msg_clean}")
# 预期:篡改后无效,清理后有效
success = not valid_after and valid_clean
print(f"\n{'='*60}")
print(f"篡改检测: {'✅ 成功' if success else '❌ 失败'}")
print(f"{'='*60}")
return success
def test_missing_signature():
"""测试缺失签名文件"""
print("\n" + "="*60)
print("测试 3: 缺失签名检测")
print("="*60)
store_dir = Path(__file__).parent.parent / "store"
verifier = SignatureVerifier(key_dir="./data/signature-verifier/keys")
# 选择一个插件并临时移除签名
test_plugin = store_dir / "@{FutureOSS}" / "json-codec"
if not test_plugin.exists():
print("跳过: json-codec 插件不存在")
return True
sig_file = test_plugin / "SIGNATURE"
if not sig_file.exists():
print("跳过: json-codec 没有签名文件")
return True
# 备份签名
backup = sig_file.read_text()
sig_file.unlink()
# 验证
valid, msg = verifier.verify_plugin(test_plugin, "FutureOSS")
print(f"无签名: {'✅ 有效' if valid else '❌ 无效'} - {msg}")
# 恢复
sig_file.write_text(backup)
valid_restored, msg_restored = verifier.verify_plugin(test_plugin, "FutureOSS")
print(f"恢复后: {'✅ 有效' if valid_restored else '❌ 无效'} - {msg_restored}")
success = not valid and valid_restored
print(f"\n{'='*60}")
print(f"缺失签名检测: {'✅ 成功' if success else '❌ 失败'}")
print(f"{'='*60}")
return success
def test_official_check():
"""测试 is_official_plugin 方法"""
print("\n" + "="*60)
print("测试 4: 官方插件识别")
print("="*60)
store_dir = Path(__file__).parent.parent / "store"
verifier = SignatureVerifier(key_dir="./data/signature-verifier/keys")
# 测试官方插件
official_plugin = store_dir / "@{FutureOSS}" / "dashboard"
is_official = verifier.is_official_plugin(official_plugin)
print(f"dashboard 是官方插件: {'✅ 是' if is_official else '❌ 否'}")
success = is_official
print(f"\n{'='*60}")
print(f"官方插件识别: {'✅ 成功' if success else '❌ 失败'}")
print(f"{'='*60}")
return success
def main():
print("FutureOSS 签名验证系统测试")
print("="*60)
results = []
results.append(("官方插件验证", test_verify_official_plugins()))
results.append(("篡改检测", test_tamper_detection()))
results.append(("缺失签名检测", test_missing_signature()))
results.append(("官方插件识别", test_official_check()))
print("\n" + "="*60)
print("测试总结")
print("="*60)
for name, passed in results:
status = "✅ 通过" if passed else "❌ 失败"
print(f" {status}: {name}")
all_passed = all(r[1] for r in results)
print(f"\n{'='*60}")
print(f"总体结果: {'✅ 全部通过' if all_passed else '❌ 有失败'}")
print(f"{'='*60}")
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,274 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FutureOSS - 架构解析</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'PingFang SC', sans-serif;
background: #0d0d0d;
color: #fff;
overflow: hidden;
height: 100vh;
width: 100vw;
}
.bg-lines {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background:
repeating-linear-gradient(45deg, rgba(79,172,254,0.05) 0px, rgba(79,172,254,0.05) 1px, transparent 1px, transparent 40px),
repeating-linear-gradient(-45deg, rgba(79,172,254,0.05) 0px, rgba(79,172,254,0.05) 1px, transparent 1px, transparent 40px);
z-index: 0;
}
#play-overlay {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.9);
display: flex; align-items: center; justify-content: center;
z-index: 100; cursor: pointer; transition: opacity 0.5s;
}
#play-overlay.hidden { opacity: 0; pointer-events: none; }
.play-circle {
width: 120px; height: 120px;
border: 4px solid #4facfe; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 3rem; animation: pulse 2s ease-in-out infinite;
transition: transform 0.3s, background 0.3s;
}
.play-circle:hover { transform: scale(1.1); background: rgba(79,172,254,0.2); }
@keyframes pulse {
0%,100% { box-shadow: 0 0 0 0 rgba(79,172,254,0.4); }
50% { box-shadow: 0 0 0 30px rgba(79,172,254,0); }
}
#stage { position: relative; width: 100%; height: 100%; z-index: 1; display: flex; align-items: center; justify-content: center; }
/* 架构图 */
.arch-diagram {
display: flex; flex-direction: column; align-items: center; gap: 20px;
opacity: 0; transform: scale(0.5); transition: all 1s cubic-bezier(0.68,-0.55,0.265,1.55);
}
.arch-diagram.show { opacity: 1; transform: scale(1); }
.layer {
display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;
opacity: 0; transform: translateY(40px); transition: all 0.8s cubic-bezier(0.68,-0.55,0.265,1.55);
}
.layer.show { opacity: 1; transform: translateY(0); }
.block {
padding: 20px 30px;
border-radius: 12px;
text-align: center;
font-weight: 600;
font-size: 0.95rem;
min-width: 140px;
position: relative;
}
.block::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 14px;
background: linear-gradient(135deg, #4facfe, #00f2fe);
z-index: -1;
opacity: 0.5;
}
.block.core { background: #1a3a5c; }
.block.plugin { background: #1a2a4c; }
.block.infra { background: #0f2a3c; }
.block span { display: block; font-size: 1.5rem; margin-bottom: 8px; }
.arrow-down {
font-size: 1.5rem;
color: #4facfe;
opacity: 0;
animation: none;
}
.arrow-down.show {
opacity: 1;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%,100% { transform: translateY(0); }
50% { transform: translateY(8px); }
}
.layer-label {
position: absolute;
left: -180px;
top: 50%;
transform: translateY(-50%);
font-size: 0.85rem;
color: #888;
writing-mode: vertical-lr;
letter-spacing: 4px;
}
#desc {
position: absolute;
bottom: 15%;
width: 80%;
text-align: center;
font-size: 1.2rem;
color: #aaa;
opacity: 0;
transition: opacity 0.8s;
}
#desc.show { opacity: 1; }
#progress {
position: fixed; bottom: 0; left: 0;
height: 4px;
background: linear-gradient(90deg, #4facfe, #00f2fe, #f093fb);
width: 0%; transition: width 0.3s; z-index: 50;
}
#title {
position: fixed; top: 40px; left: 50%; transform: translateX(-50%);
font-size: 1.8rem; font-weight: 700; opacity: 0; transition: opacity 0.8s; z-index: 10;
text-shadow: 0 4px 20px rgba(79,172,254,0.5);
}
#title.show { opacity: 1; }
#controls {
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
display: flex; gap: 20px; z-index: 50; opacity: 0; transition: opacity 0.5s;
}
#controls.show { opacity: 1; }
.ctrl-btn {
width: 50px; height: 50px; border-radius: 50%; background: rgba(255,255,255,0.1);
border: none; color: #fff; font-size: 1.3rem; cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.ctrl-btn:hover { background: rgba(79,172,254,0.5); transform: scale(1.1); }
</style>
</head>
<body>
<div class="bg-lines"></div>
<div id="play-overlay">
<div class="play-circle"></div>
</div>
<div id="title">🏗️ 架构解析</div>
<div id="stage">
<div class="arch-diagram" id="arch">
<!-- 前端层 -->
<div class="layer" id="layer-frontend">
<div class="label" style="position:absolute;left:-160px;top:50%;transform:translateY(-50%);color:#666;font-size:0.8rem;writing-mode:vertical-lr;letter-spacing:3px;">前端层</div>
<div class="block plugin"><span>🌐</span>WebUI 容器</div>
<div class="block plugin"><span>📊</span>Dashboard</div>
<div class="block plugin"><span>💻</span>Log Terminal</div>
</div>
<div class="arrow-down"></div>
<!-- HTTP 层 -->
<div class="layer" id="layer-http">
<div class="label" style="position:absolute;left:-160px;top:50%;transform:translateY(-50%);color:#666;font-size:0.8rem;writing-mode:vertical-lr;letter-spacing:3px;">服务层</div>
<div class="block plugin"><span>🔌</span>HTTP API<br><small style="color:#888">:8080</small></div>
<div class="block plugin"><span>🔌</span>TCP Server<br><small style="color:#888">:8082</small></div>
<div class="block plugin"><span>🔌</span>WebSocket</div>
</div>
<div class="arrow-down"></div>
<!-- 核心层 -->
<div class="layer" id="layer-core">
<div class="label" style="position:absolute;left:-160px;top:50%;transform:translateY(-50%);color:#666;font-size:0.8rem;writing-mode:vertical-lr;letter-spacing:3px;">核心层</div>
<div class="block core"><span>📦</span>Plugin Loader</div>
<div class="block core"><span>🌉</span>Plugin Bridge</div>
<div class="block core"><span>🔐</span>签名验证</div>
</div>
<div class="arrow-down"></div>
<!-- 基础设施 -->
<div class="layer" id="layer-infra">
<div class="label" style="position:absolute;left:-160px;top:50%;transform:translateY(-50%);color:#666;font-size:0.8rem;writing-mode:vertical-lr;letter-spacing:3px;">基础设施</div>
<div class="block infra"><span>💾</span>Plugin Storage</div>
<div class="block infra"><span>🎨</span>Logger</div>
<div class="block infra"><span>📦</span>Pkg Manager</div>
<div class="block infra"><span>🔗</span>Dependency</div>
</div>
</div>
<div id="desc">一切皆为插件,从核心到界面</div>
</div>
<div id="progress"></div>
<div id="controls">
<button class="ctrl-btn" id="btn-replay" title="重播"></button>
<button class="ctrl-btn" id="btn-sound" title="音效">🔊</button>
<button class="ctrl-btn" id="btn-back" title="返回"></button>
</div>
<script>
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx=null, soundEnabled=true, bgMusic=null;
function initAudio(){if(!audioCtx)audioCtx=new AudioCtx();}
function playBgMusic(){
if(!soundEnabled||!audioCtx)return;stopBgMusic();
const dur=30,sr=audioCtx.sampleRate,buf=audioCtx.createBuffer(2,sr*dur,sr);
for(let ch=0;ch<2;ch++){const d=buf.getChannelData(ch);for(let i=0;i<d.length;i++){const t=i/sr;d[i]=(Math.sin(2*Math.PI*196*t)*0.08+Math.sin(2*Math.PI*294*t)*0.06+Math.sin(2*Math.PI*392*t)*0.05)*Math.exp(-t%6);}}
bgMusic=audioCtx.createBufferSource();bgMusic.buffer=buf;bgMusic.loop=true;
const g=audioCtx.createGain();g.gain.value=0.1;bgMusic.connect(g).connect(audioCtx.destination);bgMusic.start();
}
function stopBgMusic(){if(bgMusic){try{bgMusic.stop();}catch(e){}bgMusic=null;}}
function playPop(){
if(!soundEnabled||!audioCtx)return;
const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type='sine';
o.frequency.setValueAtTime(700,audioCtx.currentTime);o.frequency.exponentialRampToValueAtTime(250,audioCtx.currentTime+0.18);
g.gain.setValueAtTime(0.2,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(0.01,audioCtx.currentTime+0.18);
o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+0.18);
}
document.getElementById('btn-sound').addEventListener('click',()=>{
soundEnabled=!soundEnabled;document.getElementById('btn-sound').textContent=soundEnabled?'🔊':'🔇';
if(!soundEnabled)stopBgMusic();else playBgMusic();
});
const overlay=document.getElementById('play-overlay');
const title=document.getElementById('title');
const progress=document.getElementById('progress');
const controls=document.getElementById('controls');
const arch=document.getElementById('arch');
const desc=document.getElementById('desc');
const layers=[document.getElementById('layer-frontend'),document.getElementById('layer-http'),document.getElementById('layer-core'),document.getElementById('layer-infra')];
const arrows=document.querySelectorAll('.arrow-down');
function setProgress(p){progress.style.width=p+'%';}
function startAnimation(){
setProgress(0);
arch.classList.remove('show');
layers.forEach(l=>l.classList.remove('show'));
arrows.forEach(a=>a.classList.remove('show'));
title.classList.remove('show');
desc.classList.remove('show');
controls.classList.remove('show');
overlay.classList.add('hidden');
initAudio();playBgMusic();playPop();
setTimeout(()=>{title.classList.add('show');setProgress(10);},300);
setTimeout(()=>{arch.classList.add('show');setProgress(15);playPop();},800);
layers.forEach((layer,i)=>{
setTimeout(()=>{layer.classList.add('show');playPop();setProgress(25+(i+1)*15);},1500+i*1200);
});
arrows.forEach((arrow,i)=>{
setTimeout(()=>{arrow.classList.add('show');},2000+i*1200);
});
setTimeout(()=>{desc.classList.add('show');setProgress(85);},1500+layers.length*1200+500);
setTimeout(()=>{controls.classList.add('show');setProgress(100);},1500+layers.length*1200+2000);
}
overlay.addEventListener('click',startAnimation);
document.getElementById('btn-replay').addEventListener('click',startAnimation);
document.getElementById('btn-back').addEventListener('click',()=>{stopBgMusic();location.href='index.html';});
</script>
</body>
</html>

View File

@@ -1,113 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FutureOSS - 视频展示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0a0a0a;
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
header {
padding: 40px 20px;
text-align: center;
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
header h1 { font-size: 2.5rem; margin-bottom: 10px; }
header p { opacity: 0.8; font-size: 1.1rem; }
.container {
max-width: 1200px;
padding: 40px 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
width: 100%;
}
.card {
background: #1a1a1a;
border-radius: 16px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3);
}
.card-thumb {
height: 180px;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
}
.card:nth-child(1) .card-thumb { background: linear-gradient(135deg, #667eea, #764ba2); }
.card:nth-child(2) .card-thumb { background: linear-gradient(135deg, #f093fb, #f5576c); }
.card:nth-child(3) .card-thumb { background: linear-gradient(135deg, #4facfe, #00f2fe); }
.card-info { padding: 20px; }
.card-info h3 { margin-bottom: 8px; font-size: 1.2rem; }
.card-info p { color: #888; font-size: 0.9rem; }
.play-btn {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 8px 16px;
background: #667eea;
border-radius: 8px;
font-size: 0.9rem;
transition: background 0.2s;
}
.card:hover .play-btn { background: #764ba2; }
footer {
padding: 30px;
text-align: center;
color: #666;
width: 100%;
}
</style>
</head>
<body>
<header>
<h1>🎬 FutureOSS 视频展示</h1>
<p>用动画和声音了解我们的项目</p>
</header>
<div class="container">
<div class="card" onclick="location.href='intro.html'">
<div class="card-thumb">📦</div>
<div class="card-info">
<h3>项目特性展示</h3>
<p>立方体动画演示核心特性,插件化、安全性、实时监控...</p>
<div class="play-btn">▶ 点击播放</div>
</div>
</div>
<div class="card" onclick="location.href='plugin-demo.html'">
<div class="card-thumb">🔌</div>
<div class="card-info">
<h3>插件开发演示</h3>
<p>从零开发一个插件,看这里就对了</p>
<div class="play-btn">▶ 点击播放</div>
</div>
</div>
<div class="card" onclick="location.href='architecture.html'">
<div class="card-thumb">🏗️</div>
<div class="card-info">
<h3>架构解析</h3>
<p>深入了解 FutureOSS 的技术架构和设计思想</p>
<div class="play-btn">▶ 点击播放</div>
</div>
</div>
</div>
<footer>
<p>FutureOSS © 2026 — 一切皆为插件</p>
</footer>
</body>
</html>

View File

@@ -1,553 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>FutureOSS</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.1.0/fonts/remixicon.css" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a0a;color:#ddd;overflow:hidden;height:100vh;width:100vw;font-family:'PingFang SC','Microsoft YaHei',sans-serif}
.letterbox{position:fixed;left:0;width:100%;height:7vh;background:#000;z-index:300;pointer-events:none}
.letterbox.top{top:0}.letterbox.bottom{bottom:0}
#grid-bg{position:fixed;inset:0;z-index:0;background-image:linear-gradient(rgba(100,120,180,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(100,120,180,.04) 1px,transparent 1px);background-size:50px 50px;animation:gs 30s linear infinite}
@keyframes gs{to{background-position:50px 50px}}
#stage{position:fixed;inset:0;z-index:10}
.sc{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .6s}
.sc.active{opacity:1;pointer-events:auto}
#subtitle-bar{position:fixed;bottom:12%;left:50%;transform:translateX(-50%);width:65%;text-align:center;z-index:100;pointer-events:none}
#subtitle{font-size:.95rem;line-height:1.8;font-weight:300;color:rgba(255,255,255,.45);text-shadow:0 2px 20px rgba(0,0,0,.9);opacity:0}
#subtitle.show{opacity:1;transition:opacity .4s}
#progress{position:fixed;bottom:0;left:0;height:1px;background:rgba(100,120,180,.3);width:0%;transition:width .3s;z-index:400}
#overlay{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:999;cursor:pointer;background:#0a0a0a;transition:opacity .8s}
#overlay.hidden{opacity:0;pointer-events:none}
#overlay .pt{font-size:.65rem;color:#222;letter-spacing:8px;margin-bottom:30px}
.pb{width:50px;height:50px;border:1px solid #333;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:#555}
#controls{position:fixed;bottom:2.5%;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:400;opacity:0;transition:opacity .4s}
#controls.show{opacity:1}
.cb{width:32px;height:32px;border-radius:50%;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);color:#444;font-size:.75rem;cursor:pointer;display:flex;align-items:center;justify-content:center}
.cb:hover{color:#fff;background:rgba(255,255,255,.1)}
/* 黑屏 */
.blank{}
/* 纯文字 */
.plain-text{font-size:2.2rem;font-weight:200;text-align:center;padding:0 40px;color:rgba(255,255,255,.5);opacity:0;transform:translateY(15px)}
.plain-text.show{opacity:1;transform:translateY(0);transition:all 1.2s ease}
/* 左右分屏 */
.split{display:flex;width:85%;max-width:1000px;gap:60px;align-items:center;padding:0 30px}
.split-left{flex:1;font-size:1.8rem;font-weight:300;color:rgba(255,255,255,.6);line-height:1.6;opacity:0;transform:translateX(-30px)}
.split-left.show{opacity:1;transform:translateX(0);transition:all .9s ease}
.split-right{flex:1;opacity:0;transform:translateX(30px)}
.split-right.show{opacity:1;transform:translateX(0);transition:all .9s ease .2s}
/* 痛点列表 */
.pain-list{list-style:none}
.pain-list li{padding:8px 0;font-size:.95rem;color:rgba(255,255,255,.4);opacity:0;transform:translateY(8px)}
.pain-list li.show{opacity:1;transform:translateY(0);transition:all .5s ease}
.pain-list li::before{content:'\D7';color:#553333;margin-right:10px;font-weight:bold}
/* 解决方案列表 */
.sol-list{list-style:none}
.sol-list li{padding:8px 0;font-size:.9rem;color:rgba(120,140,180,.7);opacity:0;transform:translateY(8px)}
.sol-list li.show{opacity:1;transform:translateY(0);transition:all .5s ease}
.sol-list li::before{content:'\2713';color:#4a6;margin-right:10px;font-weight:bold}
/* 项目名 */
.proj-name{font-size:3.5rem;font-weight:600;color:rgba(255,255,255,.7);opacity:0;transform:translateY(15px)}
.proj-name.show{opacity:1;transform:translateY(0);transition:all 1s ease}
.proj-tag{font-size:.85rem;color:#333;letter-spacing:4px;margin-top:12px;opacity:0}
.proj-tag.show{opacity:1;transition:opacity .6s}
/* 概念行 */
.c-line{font-size:1.5rem;font-weight:300;text-align:center;padding:0 30px;color:rgba(255,255,255,.45);opacity:0;transform:translateY(10px)}
.c-line.show{opacity:1;transform:translateY(0);transition:all .8s ease}
.em{color:#8899bb;font-weight:400}
/* 特性展示 */
.feat-split{display:flex;width:88%;max-width:950px;gap:50px;align-items:flex-start}
.feat-code{flex:1;background:rgba(255,255,255,.015);border:1px solid rgba(255,255,255,.05);border-radius:8px;padding:20px;font-family:Consolas,'Courier New',monospace;font-size:.75rem;line-height:1.8;color:#666;opacity:0;transform:translateX(-20px)}
.feat-code.show{opacity:1;transform:translateX(0);transition:all .7s ease}
.feat-code .kw{color:#7788aa}
.feat-code .str{color:#6a6}
.feat-code .fn{color:#aaa}
.feat-code .cm{color:#333}
.feat-label{flex:1;padding:10px 0}
.feat-label .title{font-size:1.3rem;font-weight:500;color:rgba(255,255,255,.7);margin-bottom:10px;opacity:0;transform:translateX(15px)}
.feat-label .title.show{opacity:1;transform:translateX(0);transition:all .6s ease .15s}
.feat-label .desc{font-size:.85rem;color:rgba(255,255,255,.3);line-height:1.7;opacity:0;transform:translateX(15px)}
.feat-label .desc.show{opacity:1;transform:translateX(0);transition:all .6s ease .3s}
/* 终端 */
.term-box{background:rgba(255,255,255,.015);border:1px solid rgba(255,255,255,.05);border-radius:8px;padding:20px 25px;font-family:Consolas,monospace;max-width:480px;width:85%;opacity:0;transform:translateY(8px)}
.term-box.show{opacity:1;transform:translateY(0);transition:all .7s ease}
.tline{font-size:.75rem;margin-bottom:5px;opacity:0;color:#444}
.tline.show{opacity:1;transition:opacity .25s}
.tline .p{color:#6677aa}
.tline .c{color:#bbb}
.tline .o{color:#4a5a4a}
.tline .i{color:#333}
.blk::after{content:'\2588';animation:blink .8s infinite;color:#6677aa}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
/* 对比 */
.compare{display:flex;width:85%;max-width:800px;gap:30px}
.compare-col{flex:1;padding:15px 20px;border-radius:8px;opacity:0;transform:translateY(10px)}
.compare-col.show{opacity:1;transform:translateY(0);transition:all .7s ease}
.compare-col.old{background:rgba(200,50,50,.03);border:1px solid rgba(200,50,50,.08)}
.compare-col.new{background:rgba(50,150,100,.03);border:1px solid rgba(50,150,100,.08)}
.compare-col h4{font-size:.6rem;letter-spacing:3px;margin-bottom:12px;font-weight:400}
.compare-col.old h4{color:#553333}
.compare-col.new h4{color:#3a5a3a}
.compare-col p{font-size:.8rem;color:#444;line-height:1.8;font-family:Consolas,monospace}
/* 数字 */
.num-row{display:flex;gap:30px;align-items:center}
.num-block{text-align:center;opacity:0;transform:translateY(8px)}
.num-block.show{opacity:1;transform:translateY(0);transition:all .6s ease}
.num-v{font-size:3rem;font-weight:600;color:rgba(255,255,255,.6);line-height:1}
.num-u{font-size:1rem;color:#445;margin-top:3px}
.num-l{font-size:.7rem;color:#2a2a2a;margin-top:5px;letter-spacing:2px}
.nsep{width:1px;height:60px;background:rgba(255,255,255,.04)}
/* 结尾 */
.end-box{text-align:center}
.end-n{font-size:2.5rem;font-weight:600;color:rgba(255,255,255,.6);opacity:0;transform:translateY(12px)}
.end-n.show{opacity:1;transform:translateY(0);transition:all .8s ease}
.end-t{font-size:.65rem;color:#222;letter-spacing:4px;margin-top:10px;opacity:0}
.end-t.show{opacity:1;transition:opacity .5s}
.end-u{margin-top:20px;opacity:0}
.end-u.show{opacity:1;transition:opacity .5s}
.end-u a{display:block;color:#333;font-size:.65rem;text-decoration:none;margin-bottom:4px}
.end-u a:hover{color:#555}
/* 闪光 */
#flash{position:fixed;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:400}
@media(max-width:768px){
.split,.feat-split,.compare{flex-direction:column;gap:20px}
.plain-text{font-size:1.5rem}
.proj-name{font-size:2.2rem}
.num-row{flex-direction:column;gap:15px}
.nsep{width:40px;height:1px}
}
</style>
</head>
<body>
<div class="letterbox top"></div>
<div class="letterbox bottom"></div>
<div id="grid-bg"></div>
<div id="overlay">
<div class="pt">FUTUREOSS</div>
<div class="pb"><i class="ri-play-fill"></i></div>
</div>
<div id="flash"></div>
<div id="subtitle-bar"><div id="subtitle"></div></div>
<div id="progress"></div>
<div id="controls">
<button class="cb" id="btn-replay"><i class="ri-restart-line"></i></button>
<button class="cb" id="btn-sound"><i class="ri-volume-up-fill"></i></button>
</div>
<div id="stage">
<!-- 0: 黑屏 -->
<div class="sc" id="s0"></div>
<!-- 1: 开场文字 -->
<div class="sc" id="s1">
<div class="plain-text" id="m1">不知道你有没有这种感觉</div>
</div>
<!-- 2: 继续 -->
<div class="sc" id="s2">
<div class="plain-text" id="m2">每次搭一个新项目,都很累</div>
</div>
<!-- 3: 左右分屏 - 痛点在右 -->
<div class="sc" id="s3">
<div class="split">
<div class="split-left" id="sl-why">因为每次都在<br>重复同样的事</div>
<div class="split-right" id="sr-pain">
<ul class="pain-list">
<li id="p0">装环境装到怀疑人生</li>
<li id="p1">找个配置文件翻遍整个项目</li>
<li id="p2">加个小功能要大改架构</li>
<li id="p3">出了问题连日志都看不明白</li>
</ul>
</div>
</div>
</div>
<!-- 4: 转折 -->
<div class="sc" id="s4">
<div class="plain-text" id="m4">于是我们想</div>
</div>
<!-- 5: 左右分屏 - 解法在右 -->
<div class="sc" id="s5">
<div class="split">
<div class="split-left" id="sl-want">能不能做一个<br>不用操心的框架?</div>
<div class="split-right" id="sr-sol">
<ul class="sol-list">
<li id="s0">不用再重复写基础代码</li>
<li id="s1">不用再手动管理依赖</li>
<li id="s2">不用在几十个文件里找配置</li>
<li id="s3">不用担心插件被篡改</li>
</ul>
</div>
</div>
</div>
<!-- 6: 揭示 -->
<div class="sc" id="s6" style="text-align:center">
<div class="proj-name" id="rn">FutureOSS</div>
<div class="proj-tag" id="rt">一切皆为插件</div>
</div>
<!-- 7: 概念 -->
<div class="sc" id="s7">
<div class="c-line" id="c1">不是<span class="em">部分功能</span>可插拔</div>
<div class="c-line" id="c2">而是<span class="em">所有东西</span>都是插件</div>
<div class="c-line" id="c3">仪表盘、日志、前端<span class="em">全部是</span></div>
</div>
<!-- 8: 特性1 插件化 -->
<div class="sc" id="s8">
<div class="feat-split">
<div class="feat-code" id="f8c">
<span class="cm"># 新建一个目录 丢进去</span>
<span class="kw">store</span>/@{你}/<span class="str">hello/</span>
<span class="fn"> main.py</span>
<span class="fn"> manifest.json</span>
<span class="fn"> README.md</span>
<span class="cm"># 启动自动加载 删掉自动消失</span>
<span class="cm"># 不用改一行核心代码</span>
</div>
<div class="feat-label">
<div class="title" id="f8t"><i class="ri-box-3-line" style="margin-right:8px"></i>插件化</div>
<div class="desc" id="f8d">想加功能?新建一个目录丢进去。想删?直接 rm -rf。不用改一行核心代码。</div>
</div>
</div>
</div>
<!-- 9: 特性2 签名 -->
<div class="sc" id="s9">
<div class="feat-split">
<div class="feat-code" id="f9c">
<span class="kw">验证中...</span>
<span class="o"> SHA-256 校验通过</span>
<span class="o"> RSA-4096 签名合法</span>
<span class="o"> 来源: @Falck</span>
<span class="cm"># 每个官方插件启动前</span>
<span class="cm"># 自动验证,失败直接拒绝加载</span>
</div>
<div class="feat-label">
<div class="title" id="f9t"><i class="ri-lock-password-line" style="margin-right:8px"></i>签名验证</div>
<div class="desc" id="f9d">不是事后检查,是加载前就验证。改了一个字节,整个插件拒绝加载。</div>
</div>
</div>
</div>
<!-- 10: 特性3 日志 -->
<div class="sc" id="s10">
<div class="feat-split">
<div class="feat-code" id="f10c">
<span class="kw">Log</span>.<span class="str">info</span>(<span class="o">"app"</span>, <span class="o">"启动"</span>)
<span class="kw">Log</span>.<span class="str">warn</span>(<span class="o">"db"</span>, <span class="o">"将满"</span>)
<span class="kw">Log</span>.<span class="str">error</span>(<span class="o">"api"</span>, <span class="o">"超时"</span>)
<span class="kw">Log</span>.<span class="str">tip</span>(<span class="o">"app"</span>, <span class="o">"已加载 20 插件"</span>)
<span class="cm"># 终端自动着色</span>
<span class="cm"># 不用 grep 找关键字</span>
</div>
<div class="feat-label">
<div class="title" id="f10t"><i class="ri-palette-line" style="margin-right:8px"></i>彩色日志</div>
<div class="desc" id="f10d">info 白 warn 黄 error 红。不用在一堆黑白文字里翻来覆去。</div>
</div>
</div>
</div>
<!-- 11: 特性4 商店 -->
<div class="sc" id="s11">
<div class="feat-split">
<div class="feat-code" id="f11c">
<span class="kw">$</span> <span class="str">pkg install @author/plugin</span>
<span class="o"> 下载完成</span>
<span class="o"> 签名验证通过</span>
<span class="o"> 已安装,重启生效</span>
<span class="cm"># 不用 git clone</span>
<span class="cm"># 不用手动复制目录</span>
<span class="cm"># 一行命令搞定</span>
</div>
<div class="feat-label">
<div class="title" id="f11t"><i class="ri-store-3-line" style="margin-right:8px"></i>插件商店</div>
<div class="desc" id="f11d">一条命令安装插件。不用去仓库翻目录、不用手动复制文件。</div>
</div>
</div>
</div>
<!-- 12: 特性5 监控 -->
<div class="sc" id="s12">
<div class="feat-split">
<div class="feat-code" id="f12c">
<span class="fn">仪表盘实时展示:</span>
CPU 78%
MEM 56%
NET 12.3M/s 4.1M/s
DISK RD 45MB WR 23MB
LOAD 1.2 0.8 0.6
<span class="cm"># 打开浏览器就有</span>
<span class="cm"># 不用装 Prometheus</span>
<span class="cm"># 不用配 Grafana</span>
</div>
<div class="feat-label">
<div class="title" id="f12t"><i class="ri-dashboard-line" style="margin-right:8px"></i>实时监控</div>
<div class="desc" id="f12d">启动就有仪表盘。不用搭 Prometheus、不用配 Grafana打开浏览器就行。</div>
</div>
</div>
</div>
<!-- 13: 对比 -->
<div class="sc" id="s13">
<div class="compare">
<div class="compare-col old" id="cm-old">
<h4>以前</h4>
<p>git clone xxx<br>pip install -r req.txt<br>python -m venv .venv<br>source .venv/bin/activate<br>pip install flask mysql...<br>改配置文件半小时<br>python main.py<br>报错 -> 查日志 -> 改配置<br>python main.py<br>又报错...</p>
</div>
<div class="compare-col new" id="cm-new">
<h4>现在</h4>
<p style="font-size:1.1rem;color:#fff;font-weight:600">bash start.sh</p>
</div>
</div>
</div>
<!-- 14: 终端 -->
<div class="sc" id="s14">
<div class="term-box" id="term">
<div class="tline" id="t0"><span class="p">$</span> <span class="c">bash start.sh</span></div>
<div class="tline" id="t1"><span class="i">检测环境...</span></div>
<div class="tline" id="t2"><span class="i">自动安装依赖...</span></div>
<div class="tline" id="t3"><span class="o">虚拟环境创建完成</span></div>
<div class="tline" id="t4"><span class="o">20 个插件加载完成</span></div>
<div class="tline" id="t5"><span class="o">http://localhost:8080</span></div>
<div class="tline" id="t6"><span class="p">$</span> <span class="blk"></span></div>
</div>
</div>
<!-- 15: 数字 -->
<div class="sc" id="s15">
<div class="num-row">
<div class="num-block" id="nb0"><div class="num-v"><span id="nv0">0</span></div><div class="num-u">+ 官方插件</div></div>
<div class="nsep"></div>
<div class="num-block" id="nb1"><div class="num-v"><span id="nv1">0</span></div><div class="num-u">行命令</div></div>
<div class="nsep"></div>
<div class="num-block" id="nb2"><div class="num-v"><span id="nv2">0</span></div><div class="num-l">% 插件化</div></div>
</div>
</div>
<!-- 16: 结尾 -->
<div class="sc" id="s16" style="text-align:center">
<div class="end-box">
<div class="end-n" id="en">FutureOSS</div>
<div class="end-t" id="et">一切皆为插件</div>
<div class="end-u" id="eu">
<a href="https://gitee.com/starlight-apk/feature-oss" target="_blank">gitee.com/starlight-apk/feature-oss</a>
<a href="https://futureoss.date" target="_blank">futureoss.date</a>
</div>
</div>
</div>
</div>
<script>
// 音效
var AC=window.AudioContext||window.webkitAudioContext;
var ac=null,sfxOn=true,bgSrc=null,timer=null;
function initA(){if(!ac)ac=new AC()}
function startBg(){if(!sfxOn||!ac)return;stopBg();var d=120,sr=ac.sampleRate,b=ac.createBuffer(2,sr*d,sr);for(var c=0;c<2;c++){var ch=b.getChannelData(c);for(var i=0;i<ch.length;i++){var t=i/sr;ch[i]=(Math.sin(6.28*220*t)*.04+Math.sin(6.28*330*t)*.025+Math.sin(6.28*440*t)*.02)*(.5+.5*Math.sin(6.28*.1*t))}}bgSrc=ac.createBufferSource();bgSrc.buffer=b;bgSrc.loop=true;var g=ac.createGain();g.gain.value=.05;bgSrc.connect(g).connect(ac.destination);bgSrc.start()}
function stopBg(){if(bgSrc){try{bgSrc.stop()}catch(e){}bgSrc=null}}
function pop(){if(!sfxOn||!ac)return;var o=ac.createOscillator(),g=ac.createGain();o.type='sine';o.frequency.setValueAtTime(300+Math.random()*400,ac.currentTime);o.frequency.exponentialRampToValueAtTime(80,ac.currentTime+.06);g.gain.setValueAtTime(.1,ac.currentTime);g.gain.exponentialRampToValueAtTime(.01,ac.currentTime+.06);o.connect(g).connect(ac.destination);o.start();o.stop(ac.currentTime+.06)}
function swo(){if(!sfxOn||!ac)return;var sz=ac.sampleRate*.35,bf=ac.createBuffer(1,sz,ac.sampleRate),d=bf.getChannelData(0);for(var i=0;i<sz;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/(sz*.06));var s=ac.createBufferSource();s.buffer=bf;var f=ac.createBiquadFilter();f.type='bandpass';f.frequency.value=400;f.Q.value=.3;var g=ac.createGain();g.gain.value=.02;s.connect(f).connect(g).connect(ac.destination);s.start()}
document.getElementById('btn-sound').onclick=function(){sfxOn=!sfxOn;var ic=this.querySelector('i');ic.className=sfxOn?'ri-volume-up-fill':'ri-volume-mute-line';if(!sfxOn)stopBg();else startBg()}
// 工具
var sub=document.getElementById('subtitle'),prog=document.getElementById('progress'),
overlay=document.getElementById('overlay'),ctrls=document.getElementById('controls'),
fl=document.getElementById('flash'),st0;
function setSub(h){sub.innerHTML=h;sub.classList.add('show')}
function hideSub(){sub.classList.remove('show')}
function showS(id){var ss=document.querySelectorAll('.sc');for(var i=0;i<ss.length;i++)ss[i].classList.remove('active');var el=document.getElementById(id);if(el)el.classList.add('active');swo()}
function doFlash(){fl.style.opacity='.08';fl.style.transition='opacity .5s';setTimeout(function(){fl.style.opacity='0'},50)}
function setProg(p){prog.style.width=p+'%'}
function animNum(elId,tg){var el=document.getElementById(elId),dur=1000,s=performance.now();function tk(n){var p=Math.min((n-s)/dur,1),e=1-Math.pow(1-p,3);el.textContent=Math.floor(tg*e);if(p<1)requestAnimationFrame(tk)}requestAnimationFrame(tk)}
// 时间轴
var tl=[],ti=0,tot;
function build(){
tl=[];
var m=function(s){return s*1000}
// 0: 黑屏
tl.push({t:m(.5),fn:function(){showS('s0')}})
tl.push({t:m(2),fn:function(){setSub('......')}})
tl.push({t:m(4.5),fn:function(){hideSub()}})
// 1: 开场
tl.push({t:m(5),fn:function(){showS('s1')}})
tl.push({t:m(5.6),fn:function(){pop();document.getElementById('m1').classList.add('show')}})
tl.push({t:m(6.5),fn:function(){setSub('不知道你有没有这种感觉')}})
// 2: 继续
tl.push({t:m(9),fn:function(){document.getElementById('m1').classList.remove('show')}})
tl.push({t:m(10),fn:function(){showS('s2')}})
tl.push({t:m(10.6),fn:function(){pop();document.getElementById('m2').classList.add('show')}})
tl.push({t:m(11.5),fn:function(){setSub('每次搭一个新项目,都很累')}})
// 3: 痛点
tl.push({t:m(14),fn:function(){doFlash();showS('s3')}})
tl.push({t:m(14.5),fn:function(){pop();document.getElementById('sl-why').classList.add('show')}})
tl.push({t:m(15.5),fn:function(){setSub('因为每次都在重复同样的事')}})
tl.push({t:m(17),fn:function(){pop();document.getElementById('sr-pain').classList.add('show')}})
tl.push({t:m(17.8),fn:function(){pop();document.getElementById('p0').classList.add('show')}})
tl.push({t:m(19),fn:function(){pop();document.getElementById('p1').classList.add('show')}})
tl.push({t:m(20.2),fn:function(){pop();document.getElementById('p2').classList.add('show')}})
tl.push({t:m(21.4),fn:function(){pop();document.getElementById('p3').classList.add('show')}})
// 4: 转折
tl.push({t:m(24),fn:function(){showS('s4')}})
tl.push({t:m(24.6),fn:function(){pop();document.getElementById('m4').classList.add('show')}})
tl.push({t:m(25.5),fn:function(){setSub('于是我们想......')}})
// 5: 解法
tl.push({t:m(28),fn:function(){showS('s5')}})
tl.push({t:m(28.5),fn:function(){pop();document.getElementById('sl-want').classList.add('show')}})
tl.push({t:m(29.5),fn:function(){setSub('能不能做一个不用操心的框架?')}})
tl.push({t:m(31),fn:function(){pop();document.getElementById('sr-sol').classList.add('show')}})
tl.push({t:m(31.8),fn:function(){pop();document.getElementById('s0').classList.add('show')}})
tl.push({t:m(33),fn:function(){pop();document.getElementById('s1').classList.add('show')}})
tl.push({t:m(34.2),fn:function(){pop();document.getElementById('s2').classList.add('show')}})
tl.push({t:m(35.4),fn:function(){pop();document.getElementById('s3').classList.add('show')}})
// 6: 揭示
tl.push({t:m(38),fn:function(){doFlash();showS('s6')}})
tl.push({t:m(38.5),fn:function(){pop();document.getElementById('rn').classList.add('show')}})
tl.push({t:m(40),fn:function(){hideSub()}})
tl.push({t:m(41),fn:function(){pop();document.getElementById('rt').classList.add('show')}})
tl.push({t:m(42),fn:function(){setSub('FutureOSS -- 一切皆为插件')}})
// 7: 概念
tl.push({t:m(44),fn:function(){showS('s7')}})
tl.push({t:m(44.5),fn:function(){pop();document.getElementById('c1').classList.add('show')}})
tl.push({t:m(46),fn:function(){setSub('不是部分功能可插拔')}})
tl.push({t:m(47.5),fn:function(){pop();document.getElementById('c2').classList.add('show')}})
tl.push({t:m(49),fn:function(){setSub('而是所有东西都是插件')}})
tl.push({t:m(50.5),fn:function(){pop();document.getElementById('c3').classList.add('show')}})
// 8: 插件化
tl.push({t:m(52),fn:function(){doFlash();showS('s8')}})
tl.push({t:m(52.3),fn:function(){pop();document.getElementById('f8c').classList.add('show')}})
tl.push({t:m(53),fn:function(){pop();document.getElementById('f8t').classList.add('show');document.getElementById('f8d').classList.add('show')}})
tl.push({t:m(54),fn:function(){setSub('想加功能新建目录丢进去 想删直接删掉')}})
// 9: 签名
tl.push({t:m(56.5),fn:function(){showS('s9')}})
tl.push({t:m(56.8),fn:function(){pop();document.getElementById('f9c').classList.add('show')}})
tl.push({t:m(57.5),fn:function(){pop();document.getElementById('f9t').classList.add('show');document.getElementById('f9d').classList.add('show')}})
tl.push({t:m(58.5),fn:function(){setSub('改一个字节 整个插件拒绝加载')}})
// 10: 日志
tl.push({t:m(61),fn:function(){showS('s10')}})
tl.push({t:m(61.3),fn:function(){pop();document.getElementById('f10c').classList.add('show')}})
tl.push({t:m(62),fn:function(){pop();document.getElementById('f10t').classList.add('show');document.getElementById('f10d').classList.add('show')}})
tl.push({t:m(63),fn:function(){setSub('不用在一堆黑白文字里翻来覆去')}})
// 11: 商店
tl.push({t:m(65.5),fn:function(){showS('s11')}})
tl.push({t:m(65.8),fn:function(){pop();document.getElementById('f11c').classList.add('show')}})
tl.push({t:m(66.5),fn:function(){pop();document.getElementById('f11t').classList.add('show');document.getElementById('f11d').classList.add('show')}})
tl.push({t:m(67.5),fn:function(){setSub('一条命令安装 不用翻目录 不用手动复制')}})
// 12: 监控
tl.push({t:m(70),fn:function(){showS('s12')}})
tl.push({t:m(70.3),fn:function(){pop();document.getElementById('f12c').classList.add('show')}})
tl.push({t:m(71),fn:function(){pop();document.getElementById('f12t').classList.add('show');document.getElementById('f12d').classList.add('show')}})
tl.push({t:m(72),fn:function(){setSub('不用搭Prometheus 不用配Grafana')}})
// 13: 对比
tl.push({t:m(74.5),fn:function(){doFlash();showS('s13')}})
tl.push({t:m(74.8),fn:function(){pop();document.getElementById('cm-old').classList.add('show')}})
tl.push({t:m(77),fn:function(){setSub('以前: clone 装依赖 配环境 改配置 启动 报错 查日志 改...')}})
tl.push({t:m(79),fn:function(){pop();document.getElementById('cm-new').classList.add('show')}})
tl.push({t:m(80),fn:function(){hideSub()}})
tl.push({t:m(80.5),fn:function(){setSub('现在: bash start.sh')}})
// 14: 终端
tl.push({t:m(82),fn:function(){showS('s14')}})
tl.push({t:m(82.3),fn:function(){pop();document.getElementById('term').classList.add('show')}})
tl.push({t:m(83.5),fn:function(){pop();document.getElementById('t0').classList.add('show')}})
tl.push({t:m(84.5),fn:function(){pop();document.getElementById('t1').classList.add('show')}})
tl.push({t:m(85.5),fn:function(){pop();document.getElementById('t2').classList.add('show')}})
tl.push({t:m(86.5),fn:function(){pop();document.getElementById('t3').classList.add('show')}})
tl.push({t:m(87.2),fn:function(){pop();document.getElementById('t4').classList.add('show')}})
tl.push({t:m(88),fn:function(){pop();document.getElementById('t5').classList.add('show')}})
tl.push({t:m(88.8),fn:function(){pop();document.getElementById('t6').classList.add('show')}})
tl.push({t:m(90),fn:function(){setSub('一行命令 自动装依赖 创建环境 启动服务')}})
// 15: 数字
tl.push({t:m(92),fn:function(){doFlash();showS('s15')}})
tl.push({t:m(92.3),fn:function(){pop();document.getElementById('nb0').classList.add('show');animNum('nv0',20)}})
tl.push({t:m(93.5),fn:function(){pop();document.getElementById('nb1').classList.add('show');animNum('nv1',1)}})
tl.push({t:m(94.7),fn:function(){pop();document.getElementById('nb2').classList.add('show');animNum('nv2',100)}})
tl.push({t:m(96),fn:function(){setSub('20+ 插件 1 行命令 100% 插件化')}})
// 16: 结尾
tl.push({t:m(98),fn:function(){doFlash();showS('s16')}})
tl.push({t:m(98.3),fn:function(){pop();document.getElementById('en').classList.add('show')}})
tl.push({t:m(99.5),fn:function(){hideSub()}})
tl.push({t:m(100),fn:function(){pop();document.getElementById('et').classList.add('show')}})
tl.push({t:m(101),fn:function(){setSub('一切皆为插件')}})
tl.push({t:m(102.5),fn:function(){pop();document.getElementById('eu').classList.add('show')}})
tl.push({t:m(104),fn:function(){setSub('代码开源 欢迎贡献')}})
tl.push({t:m(106),fn:function(){ctrls.classList.add('show');setProg(100)}})
return m(108)
}
function resetAll(){
var ss=document.querySelectorAll('.sc');for(var i=0;i<ss.length;i++)ss[i].classList.remove('active')
var ids=['m1','m2','m4','sl-why','sr-pain','p0','p1','p2','p3','sl-want','sr-sol','s0','s1','s2','s3','rn','rt','c1','c2','c3',
'f8c','f8t','f8d','f9c','f9t','f9d','f10c','f10t','f10d','f11c','f11t','f11d','f12c','f12t','f12d',
'cm-old','cm-new','term','t0','t1','t2','t3','t4','t5','t6','nb0','nb1','nb2','en','et','eu']
for(var i=0;i<ids.length;i++){var e=document.getElementById(ids[i]);if(e)e.classList.remove('show')}
document.getElementById('nv0').textContent='0';document.getElementById('nv1').textContent='0';document.getElementById('nv2').textContent='0'
hideSub();setProg(0);ctrls.classList.remove('show')
}
function start(){
overlay.classList.add('hidden');resetAll();initA();startBg()
tot=build();ti=0;st0=performance.now()
function next(){while(ti<tl.length){var it=tl[ti],el=performance.now()-st0,dl=it.t-el;if(dl>0){timer=setTimeout(function(){it.fn();ti++;setProg(it.t/tot*100);next()},dl);return;}it.fn();ti++}}
next()
}
overlay.onclick=start
document.getElementById('btn-replay').onclick=function(){clearTimeout(timer);start()}
</script>
</body>
</html>

View File

@@ -1,324 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FutureOSS - 插件开发演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'PingFang SC', sans-serif;
background: #0d0d0d;
color: #fff;
overflow: hidden;
height: 100vh;
width: 100vw;
}
.bg-grid {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-image:
linear-gradient(rgba(240, 147, 251, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(240, 147, 251, 0.1) 1px, transparent 1px);
background-size: 40px 40px;
z-index: 0;
}
#play-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
cursor: pointer;
transition: opacity 0.5s;
}
#play-overlay.hidden { opacity: 0; pointer-events: none; }
.play-circle {
width: 120px; height: 120px;
border: 4px solid #f093fb;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
animation: pulse 2s ease-in-out infinite;
transition: transform 0.3s, background 0.3s;
}
.play-circle:hover {
transform: scale(1.1);
background: rgba(240, 147, 251, 0.2);
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(240, 147, 251, 0.4); }
50% { box-shadow: 0 0 0 30px rgba(240, 147, 251, 0); }
}
/* 时间线 */
#timeline {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.step {
position: absolute;
width: 70%;
padding: 40px;
background: rgba(255,255,255,0.03);
border-radius: 20px;
border: 1px solid rgba(240, 147, 251, 0.3);
opacity: 0;
transform: scale(0.8) translateY(50px);
transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.step.active {
opacity: 1;
transform: scale(1) translateY(0);
}
.step.prev {
opacity: 0;
transform: scale(0.6) translateX(-100px);
}
.step-number {
width: 50px; height: 50px;
background: linear-gradient(135deg, #f093fb, #f5576c);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 20px;
}
.step h3 { font-size: 1.5rem; margin-bottom: 15px; }
.step p { color: #aaa; line-height: 1.8; font-size: 1.05rem; }
.step code {
display: block;
margin-top: 15px;
padding: 15px;
background: #1a1a1a;
border-radius: 8px;
font-family: 'Consolas', monospace;
font-size: 0.9rem;
color: #4facfe;
overflow-x: auto;
}
#progress {
position: fixed;
bottom: 0; left: 0;
height: 4px;
background: linear-gradient(90deg, #f093fb, #f5576c, #4facfe);
width: 0%;
transition: width 0.3s;
z-index: 50;
}
#title {
position: fixed;
top: 40px;
left: 50%;
transform: translateX(-50%);
font-size: 1.8rem;
font-weight: 700;
opacity: 0;
transition: opacity 0.8s;
z-index: 10;
text-shadow: 0 4px 20px rgba(240, 147, 251, 0.5);
}
#title.show { opacity: 1; }
#controls {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
z-index: 50;
opacity: 0;
transition: opacity 0.5s;
}
#controls.show { opacity: 1; }
.ctrl-btn {
width: 50px; height: 50px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
border: none;
color: #fff;
font-size: 1.3rem;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.ctrl-btn:hover { background: rgba(240, 147, 251, 0.5); transform: scale(1.1); }
/* 装饰几何 */
.geo {
position: absolute;
border: 2px solid rgba(240, 147, 251, 0.2);
border-radius: 4px;
animation: spin 10s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="geo" style="width:80px;height:80px;top:15%;left:10%;"></div>
<div class="geo" style="width:60px;height:60px;bottom:20%;right:15%;border-radius:50%;animation-duration:8s;"></div>
<div class="geo" style="width:100px;height:100px;top:60%;left:80%;animation-duration:12s;animation-direction:reverse;"></div>
<div id="play-overlay">
<div class="play-circle"></div>
</div>
<div id="title">🔌 从零开发一个插件</div>
<div id="timeline">
<div class="step" data-step="0">
<div class="step-number">1</div>
<h3>📁 创建插件目录</h3>
<p>按照规范在 <code>store/@{作者名}/插件名/</code> 下创建目录</p>
<code>
store/@{myname}/hello-world/<br>
├── main.py<br>
├── manifest.json<br>
└── README.md
</code>
</div>
<div class="step" data-step="1">
<div class="step-number">2</div>
<h3>📝 编写 manifest.json</h3>
<p>声明插件名称、版本、依赖和描述信息</p>
<code>
{<br>
&nbsp;&nbsp;"name": "hello-world",<br>
&nbsp;&nbsp;"version": "1.0.0",<br>
&nbsp;&nbsp;"author": "@{myname}",<br>
&nbsp;&nbsp;"description": "我的第一个插件"<br>
}
</code>
</div>
<div class="step" data-step="2">
<div class="step-number">3</div>
<h3>🐍 编写 main.py</h3>
<p>实现插件的初始化逻辑,注册路由或事件</p>
<code>
class Plugin:<br>
&nbsp;&nbsp;def init(self, app):<br>
&nbsp;&nbsp;&nbsp;&nbsp;@app.route("/hello")<br>
&nbsp;&nbsp;&nbsp;&nbsp;def hello():<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return {"msg": "Hello, FutureOSS!"}
</code>
</div>
<div class="step" data-step="3">
<div class="step-number">4</div>
<h3>🚀 启动 & 测试</h3>
<p>运行项目,访问 <code>http://localhost:8080/hello</code> 验证插件是否正常工作</p>
<code>
bash start.sh<br>
curl http://localhost:8080/hello<br>
# → {"msg": "Hello, FutureOSS!"}
</code>
</div>
<div class="step" data-step="4">
<div class="step-number">5</div>
<h3>📦 发布到商店</h3>
<p>打包插件并上传到 Gitee 商店,其他人一键安装!</p>
<code>
python tools/sign_single_plugin.py @{myname}/hello-world<br>
# 上传到 Gitee 商店仓库
</code>
</div>
</div>
<div id="progress"></div>
<div id="controls">
<button class="ctrl-btn" id="btn-replay" title="重播"></button>
<button class="ctrl-btn" id="btn-sound" title="音效">🔊</button>
<button class="ctrl-btn" id="btn-back" title="返回"></button>
</div>
<script>
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx = null, soundEnabled = true, bgMusic = null;
function initAudio() { if (!audioCtx) audioCtx = new AudioCtx(); }
function playBgMusic() {
if (!soundEnabled || !audioCtx) return;
stopBgMusic();
const dur = 30, sr = audioCtx.sampleRate;
const buf = audioCtx.createBuffer(2, sr * dur, sr);
for (let ch = 0; ch < 2; ch++) {
const d = buf.getChannelData(ch);
for (let i = 0; i < d.length; i++) {
const t = i / sr;
d[i] = (Math.sin(2*Math.PI*262*t)*0.1 + Math.sin(2*Math.PI*330*t)*0.08 + Math.sin(2*Math.PI*392*t)*0.06) * Math.exp(-t%5);
}
}
bgMusic = audioCtx.createBufferSource();
bgMusic.buffer = buf; bgMusic.loop = true;
const g = audioCtx.createGain(); g.gain.value = 0.12;
bgMusic.connect(g).connect(audioCtx.destination);
bgMusic.start();
}
function stopBgMusic() { if (bgMusic) { try{bgMusic.stop();}catch(e){} bgMusic=null; } }
function playPop() {
if (!soundEnabled || !audioCtx) return;
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type='sine'; o.frequency.setValueAtTime(900, audioCtx.currentTime);
o.frequency.exponentialRampToValueAtTime(300, audioCtx.currentTime+0.15);
g.gain.setValueAtTime(0.25, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime+0.15);
o.connect(g).connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+0.15);
}
document.getElementById('btn-sound').addEventListener('click', () => {
soundEnabled=!soundEnabled;
document.getElementById('btn-sound').textContent=soundEnabled?'🔊':'🔇';
if(!soundEnabled) stopBgMusic(); else playBgMusic();
});
const overlay = document.getElementById('play-overlay');
const title = document.getElementById('title');
const progress = document.getElementById('progress');
const controls = document.getElementById('controls');
const steps = document.querySelectorAll('.step');
let animationTimer = null;
function setProgress(p) { progress.style.width = p + '%'; }
function startAnimation() {
setProgress(0);
steps.forEach(s => { s.classList.remove('active','prev'); });
title.classList.remove('show');
controls.classList.remove('show');
overlay.classList.add('hidden');
initAudio(); playBgMusic(); playPop();
setTimeout(() => { title.classList.add('show'); setProgress(5); }, 300);
steps.forEach((step, i) => {
setTimeout(() => {
steps.forEach((s, j) => {
if (j < i) s.classList.add('prev'), s.classList.remove('active');
else s.classList.remove('prev');
});
step.classList.add('active');
playPop();
setProgress(10 + (i + 1) * 18);
}, 1000 + i * 3500);
});
setTimeout(() => { controls.classList.add('show'); setProgress(100); }, 1000 + steps.length * 3500 + 500);
}
overlay.addEventListener('click', startAnimation);
document.getElementById('btn-replay').addEventListener('click', startAnimation);
document.getElementById('btn-back').addEventListener('click', () => { stopBgMusic(); location.href='index.html'; });
</script>
</body>
</html>