删除了不需要的文件
This commit is contained in:
287
.pylintrc
287
.pylintrc
@@ -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
661
ai.md
@@ -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日*
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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>
|
||||
113
video/index.html
113
video/index.html
@@ -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>
|
||||
553
video/intro.html
553
video/intro.html
@@ -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>
|
||||
@@ -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>
|
||||
"name": "hello-world",<br>
|
||||
"version": "1.0.0",<br>
|
||||
"author": "@{myname}",<br>
|
||||
"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>
|
||||
def init(self, app):<br>
|
||||
@app.route("/hello")<br>
|
||||
def hello():<br>
|
||||
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>
|
||||
Reference in New Issue
Block a user