diff --git a/README.md b/README.md index a38c3cd..ddecdb6 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,22 @@ ## 核心特性 -| 特性 | 说明 | -| --------------------- | -------------------------------------------------------------- | -| **FastAPI + Uvicorn** | 高性能异步 Web 框架,自动生成 OpenAPI 文档 | -| **Zenoh 服务发现** | 基于 liveliness 的 Worker 注册与 RPC 调用,负载均衡 + 超时重试 | -| **MQTT 集成** | 内嵌 amqtt Broker,MQTT ↔ Worker 双向桥接 | -| **动态路由生成** | 扫描 `config/workers/*.toml` 自动注册 HTTP 路由 | -| **Worker 框架** | `@rpc_method` 装饰器 + 自动参数解析 + Pydantic 模型绑定 | -| **自动化模型生成** | `gen_models` 根据 TOML 配置生成请求/响应模型 | -| **JWT 鉴权** | Rust 实现,路由级 `auth_required` 控制 | -| **Argon2id 密码哈希** | Rust 实现的高性能密码安全方案 | -| **异步 SQLite** | aiosqlite + SQLModel,多进程并发安全 | -| **本地文件存储** | 内置文件上传/下载端点 | -| **流媒体代理** | ffmpeg + MediaMTX RTSP 流转发 | -| **构建打包** | PyInstaller / Nuitka 一键打包为独立可执行文件 | -| **Vue 3 前端** | Element Plus UI、Pinia 状态管理、类型安全的 API 客户端 | +| 特性 | 说明 | +| --------------------- | -------------------------------------------------------------------- | +| **FastAPI + Uvicorn** | 高性能异步 Web 框架,自动生成 OpenAPI 文档 | +| **Zenoh 服务发现** | 基于 liveliness 的 Worker 注册与 RPC 调用,负载均衡 + 超时重试 | +| **MQTT 集成** | 内嵌 amqtt Broker,MQTT ↔ Worker 双向桥接 | +| **动态路由生成** | 扫描 `config/workers/*.toml` 自动注册 HTTP 路由 | +| **Worker 框架** | `@rpc_method` 装饰器 + `Plugin` 基类统一生命周期 + Pydantic 模型绑定 | +| **自动化模型生成** | `gen_models` 根据 TOML 配置生成请求/响应模型 | +| **插件脚手架** | `gen_plugin` 一键生成风格统一的 Worker 插件骨架 | +| **JWT 鉴权** | Rust 实现,路由级 `auth_required` 控制 | +| **Argon2id 密码哈希** | Rust 实现的高性能密码安全方案 | +| **异步 SQLite** | aiosqlite + SQLModel,多进程并发安全 | +| **本地文件存储** | 内置文件上传/下载端点 | +| **流媒体代理** | ffmpeg + MediaMTX RTSP 流转发 | +| **构建打包** | PyInstaller / Nuitka 一键打包为独立可执行文件 | +| **Vue 3 前端** | Element Plus UI、Pinia 状态管理、类型安全的 API 客户端 | --- @@ -45,7 +46,7 @@ chongming-zenoh/ │ │ ├── app/ │ │ │ ├── core/ # WorkerManager, RouteGenerator, MQTTBridge, auth │ │ │ └── routes/ # 内置路由(文件上传等) -│ │ └── scripts/ # build, gen_models +│ │ └── scripts/ # build, gen_models, gen_plugin │ ├── chongming-web/ # Vue 3 前端 │ │ └── src/ │ │ ├── api/generated/ # 自动生成 API 客户端 @@ -264,6 +265,7 @@ python publisher.py example add '{"a":3,"b":5}' # 调用 add ```bash uv run serve # 启动开发服务 +uv run gen_plugin # 生成统一风格插件骨架 uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型 uv run python -m chongming.scripts.build # 构建打包(PyInstaller/Nuitka) uv pip install -e utils/ # 安装/更新工具包 @@ -326,6 +328,7 @@ uv run python -m chongming.scripts.build nuitka ```bash uv run serve # 启动开发服务器 +uv run gen_plugin # 生成统一风格插件骨架 uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型 uv run build pyinstaller # PyInstaller 打包 uv run build nuitka # Nuitka 打包 diff --git a/pyproject.toml b/pyproject.toml index e1e6abb..235c363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,5 +65,6 @@ chongming-mqtt = { git = "http://124.71.68.6:3000/chongming/chongming-mqtt.git" [project.scripts] build = "chongming.scripts.build:build" gen_models = "chongming.scripts.gen_models:gen_models" +gen_plugin = "chongming.scripts.gen_plugin:gen_plugin" gunicorn = "chongming:gunicorn_serve" serve = "chongming:serve" diff --git a/src/chongming/scripts/gen_plugin.py b/src/chongming/scripts/gen_plugin.py new file mode 100644 index 0000000..a3a989e --- /dev/null +++ b/src/chongming/scripts/gen_plugin.py @@ -0,0 +1,327 @@ +""" +插件脚手架生成器 — 生成风格统一的 Worker 插件模板 + +用法: + uv run gen-plugin [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/.py 实现业务逻辑 + 4. 编辑 src/plugins/myplugin/__init__.py 的 on_init / on_cleanup + 5. uv run serve # 启动验证 + +生成内容: + config/workers//config.toml Worker 注册 + 运行时配置 + config/workers//.toml 每个操作的 RPC 端点定义 + src/plugins//__init__.py 基于 Plugin 基类的统一入口 + src/plugins//global_var.py 白名单版全局变量容器 + src/plugins//router/__init__.py 端点模块聚合导出 + src/plugins//router/.py 每个操作的 RPC 实现骨架 + src/plugins//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 [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()