modify: 插件初始化
This commit is contained in:
@@ -8,21 +8,22 @@
|
|||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
| 特性 | 说明 |
|
| 特性 | 说明 |
|
||||||
| --------------------- | -------------------------------------------------------------- |
|
| --------------------- | -------------------------------------------------------------------- |
|
||||||
| **FastAPI + Uvicorn** | 高性能异步 Web 框架,自动生成 OpenAPI 文档 |
|
| **FastAPI + Uvicorn** | 高性能异步 Web 框架,自动生成 OpenAPI 文档 |
|
||||||
| **Zenoh 服务发现** | 基于 liveliness 的 Worker 注册与 RPC 调用,负载均衡 + 超时重试 |
|
| **Zenoh 服务发现** | 基于 liveliness 的 Worker 注册与 RPC 调用,负载均衡 + 超时重试 |
|
||||||
| **MQTT 集成** | 内嵌 amqtt Broker,MQTT ↔ Worker 双向桥接 |
|
| **MQTT 集成** | 内嵌 amqtt Broker,MQTT ↔ Worker 双向桥接 |
|
||||||
| **动态路由生成** | 扫描 `config/workers/*.toml` 自动注册 HTTP 路由 |
|
| **动态路由生成** | 扫描 `config/workers/*.toml` 自动注册 HTTP 路由 |
|
||||||
| **Worker 框架** | `@rpc_method` 装饰器 + 自动参数解析 + Pydantic 模型绑定 |
|
| **Worker 框架** | `@rpc_method` 装饰器 + `Plugin` 基类统一生命周期 + Pydantic 模型绑定 |
|
||||||
| **自动化模型生成** | `gen_models` 根据 TOML 配置生成请求/响应模型 |
|
| **自动化模型生成** | `gen_models` 根据 TOML 配置生成请求/响应模型 |
|
||||||
| **JWT 鉴权** | Rust 实现,路由级 `auth_required` 控制 |
|
| **插件脚手架** | `gen_plugin` 一键生成风格统一的 Worker 插件骨架 |
|
||||||
| **Argon2id 密码哈希** | Rust 实现的高性能密码安全方案 |
|
| **JWT 鉴权** | Rust 实现,路由级 `auth_required` 控制 |
|
||||||
| **异步 SQLite** | aiosqlite + SQLModel,多进程并发安全 |
|
| **Argon2id 密码哈希** | Rust 实现的高性能密码安全方案 |
|
||||||
| **本地文件存储** | 内置文件上传/下载端点 |
|
| **异步 SQLite** | aiosqlite + SQLModel,多进程并发安全 |
|
||||||
| **流媒体代理** | ffmpeg + MediaMTX RTSP 流转发 |
|
| **本地文件存储** | 内置文件上传/下载端点 |
|
||||||
| **构建打包** | PyInstaller / Nuitka 一键打包为独立可执行文件 |
|
| **流媒体代理** | ffmpeg + MediaMTX RTSP 流转发 |
|
||||||
| **Vue 3 前端** | Element Plus UI、Pinia 状态管理、类型安全的 API 客户端 |
|
| **构建打包** | PyInstaller / Nuitka 一键打包为独立可执行文件 |
|
||||||
|
| **Vue 3 前端** | Element Plus UI、Pinia 状态管理、类型安全的 API 客户端 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ chongming-zenoh/
|
|||||||
│ │ ├── app/
|
│ │ ├── app/
|
||||||
│ │ │ ├── core/ # WorkerManager, RouteGenerator, MQTTBridge, auth
|
│ │ │ ├── core/ # WorkerManager, RouteGenerator, MQTTBridge, auth
|
||||||
│ │ │ └── routes/ # 内置路由(文件上传等)
|
│ │ │ └── routes/ # 内置路由(文件上传等)
|
||||||
│ │ └── scripts/ # build, gen_models
|
│ │ └── scripts/ # build, gen_models, gen_plugin
|
||||||
│ ├── chongming-web/ # Vue 3 前端
|
│ ├── chongming-web/ # Vue 3 前端
|
||||||
│ │ └── src/
|
│ │ └── src/
|
||||||
│ │ ├── api/generated/ # 自动生成 API 客户端
|
│ │ ├── api/generated/ # 自动生成 API 客户端
|
||||||
@@ -264,6 +265,7 @@ python publisher.py example add '{"a":3,"b":5}' # 调用 add
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run serve # 启动开发服务
|
uv run serve # 启动开发服务
|
||||||
|
uv run gen_plugin <name> # 生成统一风格插件骨架
|
||||||
uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型
|
uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型
|
||||||
uv run python -m chongming.scripts.build # 构建打包(PyInstaller/Nuitka)
|
uv run python -m chongming.scripts.build # 构建打包(PyInstaller/Nuitka)
|
||||||
uv pip install -e utils/<name> # 安装/更新工具包
|
uv pip install -e utils/<name> # 安装/更新工具包
|
||||||
@@ -326,6 +328,7 @@ uv run python -m chongming.scripts.build nuitka
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run serve # 启动开发服务器
|
uv run serve # 启动开发服务器
|
||||||
|
uv run gen_plugin <name> # 生成统一风格插件骨架
|
||||||
uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型
|
uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型
|
||||||
uv run build pyinstaller # PyInstaller 打包
|
uv run build pyinstaller # PyInstaller 打包
|
||||||
uv run build nuitka # Nuitka 打包
|
uv run build nuitka # Nuitka 打包
|
||||||
|
|||||||
@@ -65,5 +65,6 @@ chongming-mqtt = { git = "http://124.71.68.6:3000/chongming/chongming-mqtt.git"
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
build = "chongming.scripts.build:build"
|
build = "chongming.scripts.build:build"
|
||||||
gen_models = "chongming.scripts.gen_models:gen_models"
|
gen_models = "chongming.scripts.gen_models:gen_models"
|
||||||
|
gen_plugin = "chongming.scripts.gen_plugin:gen_plugin"
|
||||||
gunicorn = "chongming:gunicorn_serve"
|
gunicorn = "chongming:gunicorn_serve"
|
||||||
serve = "chongming:serve"
|
serve = "chongming:serve"
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
"""
|
||||||
|
插件脚手架生成器 — 生成风格统一的 Worker 插件模板
|
||||||
|
|
||||||
|
用法:
|
||||||
|
uv run gen-plugin <name> [operation ...]
|
||||||
|
|
||||||
|
示例:
|
||||||
|
# 生成插件骨架(含一个示例操作):
|
||||||
|
uv run gen-plugin myplugin
|
||||||
|
|
||||||
|
# 指定初始操作:
|
||||||
|
uv run gen-plugin myplugin analyze count
|
||||||
|
|
||||||
|
# 为已存在的插件增量添加操作:
|
||||||
|
uv run gen-plugin myplugin add
|
||||||
|
|
||||||
|
工作流:
|
||||||
|
1. uv run gen-plugin myplugin # 生成 config + src 骨架(统一风格)
|
||||||
|
2. uv run gen_models config/workers/myplugin src/plugins/myplugin
|
||||||
|
# 生成 Pydantic 模型
|
||||||
|
3. 编辑 src/plugins/myplugin/router/<operation>.py 实现业务逻辑
|
||||||
|
4. 编辑 src/plugins/myplugin/__init__.py 的 on_init / on_cleanup
|
||||||
|
5. uv run serve # 启动验证
|
||||||
|
|
||||||
|
生成内容:
|
||||||
|
config/workers/<name>/config.toml Worker 注册 + 运行时配置
|
||||||
|
config/workers/<name>/<op>.toml 每个操作的 RPC 端点定义
|
||||||
|
src/plugins/<name>/__init__.py 基于 Plugin 基类的统一入口
|
||||||
|
src/plugins/<name>/global_var.py 白名单版全局变量容器
|
||||||
|
src/plugins/<name>/router/__init__.py 端点模块聚合导出
|
||||||
|
src/plugins/<name>/router/<op>.py 每个操作的 RPC 实现骨架
|
||||||
|
src/plugins/<name>/models/ 由 gen_models 生成(见步骤 2)
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_root() -> Path:
|
||||||
|
"""从本文件所在目录向上查找项目根(含 pyproject.toml 的目录)。"""
|
||||||
|
here = Path(__file__).resolve().parent
|
||||||
|
for parent in here.parents:
|
||||||
|
if (parent / "pyproject.toml").exists():
|
||||||
|
return parent
|
||||||
|
return here.parent # 兜底:脚本目录的父目录
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = _find_project_root()
|
||||||
|
CONFIG_DIR = ROOT / "config" / "workers"
|
||||||
|
PLUGIN_DIR = ROOT / "src" / "plugins"
|
||||||
|
|
||||||
|
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 模板
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def snake_to_camel(name: str) -> str:
|
||||||
|
"""snake_case -> CamelCase(如 analyze_media -> AnalyzeMedia)"""
|
||||||
|
return "".join(part.title() for part in name.split("_"))
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_config_toml(name: str) -> str:
|
||||||
|
return f'''# {name} Worker 插件配置
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
[worker.defaults]
|
||||||
|
timeout = 30
|
||||||
|
cacheable = false
|
||||||
|
|
||||||
|
[worker.registration]
|
||||||
|
router_prefix = "/{name}"
|
||||||
|
|
||||||
|
# Worker 注册(app lifespan 据此发现并加载插件)
|
||||||
|
[workers]
|
||||||
|
type = "{name}"
|
||||||
|
module = "plugins.{name}"
|
||||||
|
factory = "create_workers"
|
||||||
|
cleanup = "cleanup"
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
# 运行时配置(展平后传给 create_workers 的 config 参数)
|
||||||
|
[workers.config]
|
||||||
|
data_dir = "./data"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_operation_toml(name: str, operation: str) -> str:
|
||||||
|
return f'''# {name}.{operation} — {operation} 操作
|
||||||
|
# 修改后需重新运行: uv run gen_models config/workers/{name} src/plugins/{name}
|
||||||
|
[GET]
|
||||||
|
cacheable = true
|
||||||
|
summary = "{operation} 操作描述"
|
||||||
|
params = ["value: str"]
|
||||||
|
|
||||||
|
[GET.response_model]
|
||||||
|
result = {{ type = "str", required = true }}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_plugin_init(name: str) -> str:
|
||||||
|
cls = snake_to_camel(name)
|
||||||
|
return f'''"""{name.capitalize()} Worker 插件
|
||||||
|
|
||||||
|
统一风格模板 — 基于 chongming_worker.plugin.Plugin 基类。
|
||||||
|
|
||||||
|
工作流:
|
||||||
|
1. 编辑 config/workers/{name}/*.toml 定义 RPC 端点
|
||||||
|
2. uv run gen_models config/workers/{name} src/plugins/{name}
|
||||||
|
3. 在 router/<操作>.py 中实现业务逻辑
|
||||||
|
4. 在 on_init / on_cleanup 中初始化 / 清理资源
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from chongming_worker.plugin import Plugin
|
||||||
|
|
||||||
|
from . import router # noqa: F401 触发 @rpc_method 注册
|
||||||
|
# from .routes import router # 可选: 自定义 FastAPI 路由(网关自动发现)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class {cls}Plugin(Plugin):
|
||||||
|
"""{name} 插件 — 统一生命周期。"""
|
||||||
|
|
||||||
|
type = "{name}"
|
||||||
|
db_name = "{name}.db" # 自动在 data_dir 下建 SQLite 库(不需要则置 None)
|
||||||
|
auto_bind = True # 首次初始化时自动 auto_bind_models(self.type)
|
||||||
|
|
||||||
|
async def on_init(self):
|
||||||
|
"""首次 create_workers 时调用一次:初始化资源、启动后台任务。
|
||||||
|
|
||||||
|
数据库已由基类 _bootstrap 自动初始化(self.db)。
|
||||||
|
"""
|
||||||
|
self.set_var("db", self.db)
|
||||||
|
|
||||||
|
# 需要后台任务时:
|
||||||
|
# self.spawn_task(self._background_loop())
|
||||||
|
|
||||||
|
logger.info("[{name}] 初始化完成")
|
||||||
|
|
||||||
|
async def on_cleanup(self):
|
||||||
|
"""cleanup 时调用:关闭自定义客户端等(后台任务与数据库由基类处理)。"""
|
||||||
|
logger.info("[{name}] 清理完成")
|
||||||
|
|
||||||
|
|
||||||
|
# 暴露给 app lifespan 的模块级入口(bound method,签名与旧写法完全一致)
|
||||||
|
_plugin = {cls}Plugin()
|
||||||
|
create_workers = _plugin.create_workers
|
||||||
|
cleanup = _plugin.cleanup
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_global_var(name: str) -> str:
|
||||||
|
return f'''"""{name.capitalize()} 插件全局运行时状态 — 统一容器
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- 模块只做"声明默认值",不产生任何导入期副作用
|
||||||
|
- 所有值统一在 Plugin.on_init 中通过 set_global_var 显式写入
|
||||||
|
- set_global_var 仅允许写入 _ALLOWED 白名单,拼写错误会立即抛错
|
||||||
|
- require() 用于取"必须已初始化"的值,未初始化时抛清晰错误
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from chongming_database import Database
|
||||||
|
|
||||||
|
# 允许写入的全局变量白名单(防止拼写错误静默创建无用的新变量)
|
||||||
|
_ALLOWED = frozenset({{"db"}})
|
||||||
|
|
||||||
|
# 显式声明所有运行时状态(默认 None,运行时由 on_init 写入)
|
||||||
|
db: Database | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_global_var(name: str, value: Any) -> None:
|
||||||
|
"""白名单校验版 setter — 设置模块级运行时状态。"""
|
||||||
|
if name not in _ALLOWED:
|
||||||
|
raise KeyError(
|
||||||
|
f"global_var: 未知变量 '{{name}}'(允许: {{sorted(_ALLOWED)}})"
|
||||||
|
)
|
||||||
|
globals()[name] = value
|
||||||
|
|
||||||
|
|
||||||
|
def require(name: str) -> Any:
|
||||||
|
"""获取必须已初始化的全局变量,未初始化时抛清晰错误。"""
|
||||||
|
if name not in _ALLOWED:
|
||||||
|
raise KeyError(f"global_var: 未知变量 '{{name}}'(允许: {{sorted(_ALLOWED)}})")
|
||||||
|
value = globals().get(name)
|
||||||
|
if value is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"global_var: '{{name}}' 未初始化 —— 请确认插件 create_workers 已执行且配置正确"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = sorted(_ALLOWED) + ["set_global_var", "require"]
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_router_init(name: str, operations: list[str]) -> str:
|
||||||
|
lines = [f'"""{name.capitalize()} RPC 端点"""', ""]
|
||||||
|
for op in operations:
|
||||||
|
lines.append(f"from . import {op}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("__all__ = " + repr(operations))
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_router_operation(name: str, operation: str) -> str:
|
||||||
|
op_camel = snake_to_camel(operation)
|
||||||
|
return f'''"""RPC 端点: {name}.{operation}
|
||||||
|
|
||||||
|
模型由 gen_models 生成(编辑 config/workers/{name}/{operation}.toml 后重新生成)。
|
||||||
|
"""
|
||||||
|
from chongming_worker.decorators import rpc_method
|
||||||
|
|
||||||
|
from ..models.pydantic import {op_camel}GetRequest, {op_camel}GetResponse
|
||||||
|
|
||||||
|
|
||||||
|
@rpc_method(
|
||||||
|
subject="{name}.{operation}",
|
||||||
|
method="GET",
|
||||||
|
summary="{operation} 操作",
|
||||||
|
response_model={op_camel}GetResponse,
|
||||||
|
)
|
||||||
|
async def {operation}(request: {op_camel}GetRequest) -> {op_camel}GetResponse:
|
||||||
|
"""业务逻辑实现。"""
|
||||||
|
return {op_camel}GetResponse(result=f"hello: {{request.value}}")
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 主逻辑
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _write(path: Path, content: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if path.exists():
|
||||||
|
print(f"⏭️ 跳过(已存在): {path.relative_to(ROOT)}")
|
||||||
|
return
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
print(f"✅ 生成: {path.relative_to(ROOT)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _existing_operations(plugin_dir: Path) -> list[str]:
|
||||||
|
"""扫描 router/ 下已有的操作文件(排除 __init__.py)。"""
|
||||||
|
router_dir = plugin_dir / "router"
|
||||||
|
if not router_dir.exists():
|
||||||
|
return []
|
||||||
|
ops = []
|
||||||
|
for f in sorted(router_dir.glob("*.py")):
|
||||||
|
if f.name == "__init__.py":
|
||||||
|
continue
|
||||||
|
ops.append(f.stem)
|
||||||
|
return ops
|
||||||
|
|
||||||
|
|
||||||
|
def generate_plugin(name: str, operations: list[str]) -> int:
|
||||||
|
"""生成插件骨架。返回 0 表示成功,1 表示失败。"""
|
||||||
|
if not NAME_RE.match(name):
|
||||||
|
print(f"❌ 插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
config_dir = CONFIG_DIR / name
|
||||||
|
plugin_dir = PLUGIN_DIR / name
|
||||||
|
is_new = not plugin_dir.exists()
|
||||||
|
|
||||||
|
# ---- 首次创建: 完整骨架 ----
|
||||||
|
if is_new:
|
||||||
|
_write(config_dir / "config.toml", _gen_config_toml(name))
|
||||||
|
for op in operations:
|
||||||
|
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op))
|
||||||
|
_write(plugin_dir / "__init__.py", _gen_plugin_init(name))
|
||||||
|
_write(plugin_dir / "global_var.py", _gen_global_var(name))
|
||||||
|
(plugin_dir / "models" / "__init__.py").parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
(plugin_dir / "models" / "__init__.py").write_text(
|
||||||
|
'"""模型导出 — 由 gen_models 生成,请勿手动修改"""\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"✅ 生成: {plugin_dir.relative_to(ROOT)}/models/__init__.py")
|
||||||
|
|
||||||
|
# ---- 增量: 仅补 operations(已存在文件不覆盖)----
|
||||||
|
existing = _existing_operations(plugin_dir)
|
||||||
|
added = [op for op in operations if op not in existing]
|
||||||
|
if added:
|
||||||
|
for op in added:
|
||||||
|
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op))
|
||||||
|
_write(plugin_dir / "router" / f"{op}.py", _gen_router_operation(name, op))
|
||||||
|
|
||||||
|
# ---- 重写 router/__init__.py(聚合所有操作)----
|
||||||
|
all_ops = _existing_operations(plugin_dir)
|
||||||
|
if all_ops:
|
||||||
|
init_file = plugin_dir / "router" / "__init__.py"
|
||||||
|
init_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
init_file.write_text(_gen_router_init(name, all_ops), encoding="utf-8")
|
||||||
|
print(f"✅ 更新: {init_file.relative_to(ROOT)}")
|
||||||
|
|
||||||
|
print(f"\n📋 插件 '{name}' 已就绪,下一步:")
|
||||||
|
print(f" 1. uv run gen_models config/workers/{name} src/plugins/{name}")
|
||||||
|
print(f" 2. 编辑 src/plugins/{name}/router/ 实现业务逻辑")
|
||||||
|
print(f" 3. uv run serve")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if not args:
|
||||||
|
print("用法: uv run gen_plugin <name> [operation ...]")
|
||||||
|
print("示例:")
|
||||||
|
print(" uv run gen_plugin myplugin")
|
||||||
|
print(" uv run gen_plugin myplugin analyze count")
|
||||||
|
print(" uv run gen_plugin myplugin add # 增量添加操作")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
name = args[0]
|
||||||
|
operations = args[1:] or ["ping"]
|
||||||
|
sys.exit(generate_plugin(name, operations))
|
||||||
|
|
||||||
|
|
||||||
|
def gen_plugin():
|
||||||
|
"""CLI 入口(供 pyproject.toml [project.scripts] 调用)"""
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user