From a705d18e06f001b032f8d94a7c944e76c25e9747 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Tue, 25 Aug 2026 17:03:55 +0800 Subject: [PATCH] =?UTF-8?q?modify:=E8=BF=9B=E7=A8=8B=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83=E7=9B=AE=E5=BD=95=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- src/chongming_cli/build_plugin.py | 227 ++++++++---------------- src/chongming_cli/gen_plugin.py | 285 ++++++++++++++++++++++++------ 3 files changed, 316 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index 1ae3934..56eb85e 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ chongming 网关的命令脚本集合,独立包维护(从 `src/chongming/scr | 命令 | 说明 | | ------------ | ------------------------------------------- | | `build` | 应用构建打包(PyInstaller / Nuitka) | -| `build_plugin` | 进程插件打包为可部署 zip | -| `gen_models` | 从 `config/workers//*.toml` 生成 Pydantic 模型 | -| `gen_plugin` | 生成风格统一的 Worker 插件骨架 | +| `build_plugin` | 进程插件项目根直接压缩为可部署 zip | +| `gen_models` | 从插件配置 `*.toml` 生成 Pydantic 模型 | +| `gen_plugin` | 生成风格统一的 Worker 插件骨架(`--process` 生成进程插件完整项目) | 另有 `chongming_cli.code_content` 模块:将目录下代码文件汇总为 Markdown(`python -m chongming_cli.code_content `)。 @@ -21,9 +21,11 @@ chongming 网关的命令脚本集合,独立包维护(从 `src/chongming/scr uv run build pyinstaller # 构建打包 uv run build --only mbank # 仅重新打包 .mbank 模块文件(跳过其余流程) uv run build --only mbank,vue # 只执行指定步骤 -uv run build_plugin demo # 进程插件打包 → dist/demo.zip -uv run gen_plugin myplugin # 生成插件骨架 -uv run gen_models config/workers/myplugin src/plugins/myplugin +uv run build_plugin demo # 进程插件打包 → dist/demo.zip(= workers/demo/ 直接压缩) +uv run gen_plugin myplugin # 生成 inline 插件骨架 +uv run gen_plugin myplugin --process # 生成进程插件完整项目(workers/myplugin/) +uv run gen_models config/workers/myplugin src/plugins/myplugin # inline 插件模型 +uv run gen_models workers/myplugin/config/workers/myplugin workers/myplugin/workers/myplugin # 进程插件模型 ``` `build --only ` 可选步骤:`mbank` / `prepare` / `obfuscate` / `pack` / `clean` / `vue`,可逗号分隔或重复传多个。 diff --git a/src/chongming_cli/build_plugin.py b/src/chongming_cli/build_plugin.py index 6143610..4e7ada3 100644 --- a/src/chongming_cli/build_plugin.py +++ b/src/chongming_cli/build_plugin.py @@ -1,5 +1,5 @@ """ -进程插件打包器 — 将 external 进程插件打包为可部署 zip +进程插件打包器 — 将 external 进程插件开发目录打包为可部署 zip 用法: uv run build_plugin [--output ] @@ -8,46 +8,42 @@ uv run build_plugin demo # 打包 workers/demo → dist/demo.zip uv run build_plugin demo --output /tmp/pkg -产物布局(zip 根即容器工作目录,unzip 后直接 ./start.sh 启动): - workers// 插件代码(模块 workers.) - config/workers// 插件注册 + 端点配置 - config/zenoh_config.json5 Zenoh 会话配置 - config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需) - pyproject.toml uv 依赖清单(chongming-worker + 插件扩展依赖) - start.sh 容器入口: uv sync + uv run worker_serve --type +开发目录即产物: workers// 是自包含的插件项目根(= 容器工作目录 = zip 根), +打包只做校验 + 直接压缩,不再从仓库多处装配: + + workers// 插件项目根(zip 根即容器工作目录) + ├── workers// 插件代码(模块 workers.) + ├── config/workers// 插件注册 + 端点配置 + ├── config/zenoh_config.json5 Zenoh 会话配置 + ├── config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需) + ├── pyproject.toml uv 依赖清单(chongming-worker + 扩展依赖) + └── start.sh 容器入口: uv sync + uv run worker_serve --type 说明: - - 插件扩展依赖声明在 workers//requirements.txt(可选,每行一个包), - 打包时自动合并进 pyproject.toml dependencies - - 根 config.toml 仅提取 [zenoh] 节重新生成,避免把网关侧配置(如 JWT 密钥)泄露进插件容器 + - 打包自动排除 __pycache__ / *.pyc / .venv / *.egg-info / .git + - start.sh 权限强制为 0o755(防止 git 丢执行位导致容器无法启动) + - 新项目骨架用 `uv run gen_plugin --process` 生成 """ import argparse import re -import shutil import sys import tomllib +import zipfile from pathlib import Path from ._root import find_project_root NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$") -# chongming-worker 内网 git 源(与网关 pyproject.toml [tool.uv.sources] 保持一致) -CHONGMING_WORKER_GIT = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git" -PYPI_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" +# 打包排除的路径模式(目录名 / glob) +EXCLUDE_DIRS = {"__pycache__", ".venv", ".git"} +EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".egg-info") # 项目根目录(CHONGMING_ROOT 环境变量 > CWD 向上查找,见 _root.py) ROOT = find_project_root() WORKERS_DIR = ROOT / "workers" -PLUGINS_CONFIG_DIR = ROOT / "config" / "workers" -ZENOH_CONFIG = ROOT / "config" / "zenoh_config.json5" -ROOT_CONFIG = ROOT / "config.toml" -# ============================================================ -# 产物生成 -# ============================================================ - def _rel(path: Path) -> Path | str: """相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。""" try: @@ -56,103 +52,79 @@ def _rel(path: Path) -> Path | str: return path -def _read_plugin_conf(name: str) -> dict: - """读取 config/workers//config.toml 完整内容(校验含 [workers] 节)。""" - path = PLUGINS_CONFIG_DIR / name / "config.toml" - with open(path, "rb") as f: - conf = tomllib.load(f) - if not conf.get("workers"): - raise ValueError(f"{path.relative_to(ROOT)} 缺少 [workers] 节") - return conf +def _check_project_layout(project_dir: Path, name: str) -> dict: + """ + 校验插件项目结构完整(运行时必需的文件),返回插件 [workers] 节配置。 + Raises: + FileNotFoundError / ValueError: 结构不合法 + """ + # 1. 插件注册 + 端点配置(worker 进程自读) + conf_path = project_dir / "config" / "workers" / name / "config.toml" + if not conf_path.exists(): + raise FileNotFoundError( + f"未找到插件配置: {conf_path.relative_to(project_dir)}/(缺失的话先运行 " + f"`uv run gen_plugin {name} --process` 生成骨架)" + ) + with open(conf_path, "rb") as f: + plugin_conf = tomllib.load(f) + workers = plugin_conf.get("workers") + if not workers: + raise ValueError(f"{conf_path} 缺少 [workers] 节") -def _gen_root_config_toml() -> str: - """生成最小根 config.toml(仅 [zenoh] 节,worker 进程只读它)。""" - with open(ROOT_CONFIG, "rb") as f: + # 2. Zenoh 会话配置(worker 启动必需) + if not (project_dir / "config" / "zenoh_config.json5").exists(): + raise FileNotFoundError( + f"未找到 Zenoh 配置: {_rel(project_dir / 'config' / 'zenoh_config.json5')}" + ) + + # 3. 最小根配置(worker_serve 读取 [zenoh] 节打开会话) + root_conf_path = project_dir / "config.toml" + if not root_conf_path.exists(): + raise FileNotFoundError(f"未找到最小根配置: {_rel(root_conf_path)}") + with open(root_conf_path, "rb") as f: root_conf = tomllib.load(f) - json5_path = root_conf.get("zenoh", {}).get("json5_path", "config/zenoh_config.json5") - return f"""# 最小根配置(由 build_plugin 自动生成,仅含 worker 进程所需的 [zenoh] 节) -[zenoh] -json5_path = "{json5_path}" -""" + if not root_conf.get("zenoh"): + raise ValueError(f"{root_conf_path} 缺少 [zenoh] 节(worker 启动必需)") + + return workers -def _gen_pyproject_toml(name: str, version: str, extra_deps: list[str]) -> str: - """生成插件 uv 依赖清单(chongming-worker + 插件扩展依赖)。""" - deps = [ - "chongming-worker", - "orjson", # chongming-worker 运行时依赖(SDK metadata 未声明,容器内需显式安装) - *extra_deps, - ] - deps_lines = "\n".join(f' "{d}",' for d in deps) - return f"""# {name} 进程插件依赖清单(由 build_plugin 自动生成) -[project] -name = "{name}-worker" -version = "{version}" -description = "{name} 进程插件(external 模式,独立容器部署)" -requires-python = ">=3.12" -dependencies = [ -{deps_lines} -] +def _zip_project(project_dir: Path, zip_path: Path) -> None: + """把项目根内容压缩为 zip(zip 根即容器工作目录),排除缓存/环境目录。""" + zip_path.unlink(missing_ok=True) + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for path in sorted(project_dir.rglob("*")): + if not path.is_file(): + continue + rel = path.relative_to(project_dir) + if any(part in EXCLUDE_DIRS for part in rel.parts): + continue + if any(part.endswith(EXCLUDE_SUFFIXES) for part in rel.parts): + continue + info = zipfile.ZipInfo.from_file(path, str(rel)) + if rel.name == "start.sh": + info.external_attr = 0o100755 << 16 # 强制可执行,防 git 丢执行位 + else: + info.external_attr = (path.stat().st_mode & 0xFFFF) << 16 + zf.writestr(info, path.read_bytes()) -[project.scripts] -worker_serve = "chongming_worker.process:worker_serve" - -[[tool.uv.index]] -url = "{PYPI_MIRROR}" - -[tool.uv.sources] -chongming-worker = {{ git = "{CHONGMING_WORKER_GIT}" }} -""" - - -def _gen_start_sh(name: str) -> str: - """生成容器入口脚本:uv 引导依赖 + 启动 worker 进程。""" - return f"""#!/usr/bin/env bash -# {name} 进程插件容器入口(由 build_plugin 自动生成) -# 要求: 镜像预装 uv(https://docs.astral.sh/uv/) -set -e -cd "$(dirname "$0")" - -uv sync --no-dev -exec uv run worker_serve --type {name} "$@" -""" - - -def _read_extra_deps(name: str) -> list[str]: - """读取 workers//requirements.txt(可选),每行一个依赖。""" - path = WORKERS_DIR / name / "requirements.txt" - if not path.exists(): - return [] - deps = [] - for line in path.read_text(encoding="utf-8").splitlines(): - dep = line.strip() - if dep and not dep.startswith("#"): - deps.append(dep) - return deps - - -# ============================================================ -# 打包主逻辑 -# ============================================================ def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path: """ 打包进程插件为可部署 zip,返回产物路径。 - 失败(插件/配置缺失、契约不合法)抛 FileNotFoundError / ValueError。 + 失败(插件/配置缺失、结构不合法)抛 FileNotFoundError / ValueError。 """ if not NAME_RE.match(name): raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)") - workers_src = WORKERS_DIR / name - if not workers_src.is_dir(): - raise FileNotFoundError(f"未找到插件代码目录: {_rel(workers_src)}/") + project_dir = WORKERS_DIR / name + if not project_dir.is_dir(): + raise FileNotFoundError(f"未找到插件项目目录: {_rel(project_dir)}/") - conf = _read_plugin_conf(name) # 缺配置在此抛 FileNotFoundError - workers = conf["workers"] + workers = _check_project_layout(project_dir, name) - plugin_type = workers.get("type", name) load_mode = workers.get("load_mode", "inline") if load_mode != "external": print( @@ -160,48 +132,11 @@ def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path: "独立进程将与网关内副本重复服务。进程插件请在 [workers] 节设置 " "load_mode = \"external\"。" ) - version = str(conf.get("worker", {}).get("version", "0.1.0")) output = Path(output_dir) - stage = output / name - if stage.exists(): - shutil.rmtree(stage) - stage.mkdir(parents=True, exist_ok=True) - - # ---- 1. 插件代码 ---- - shutil.copytree( - workers_src, - stage / "workers" / name, - ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), - ) - - # ---- 2. 插件配置 ---- - shutil.copytree( - PLUGINS_CONFIG_DIR / name, - stage / "config" / "workers" / name, - ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), - ) - - # ---- 3. Zenoh 会话配置 ---- - if not ZENOH_CONFIG.exists(): - raise FileNotFoundError(f"未找到 Zenoh 配置: {_rel(ZENOH_CONFIG)}") - shutil.copy(ZENOH_CONFIG, stage / "config" / "zenoh_config.json5") - - # ---- 4. 生成最小根配置 + 依赖清单 + 启动脚本 ---- - (stage / "config.toml").write_text(_gen_root_config_toml(), encoding="utf-8") - (stage / "pyproject.toml").write_text( - _gen_pyproject_toml(name, version, _read_extra_deps(name)), encoding="utf-8" - ) - start_sh = stage / "start.sh" - start_sh.write_text(_gen_start_sh(plugin_type), encoding="utf-8") - start_sh.chmod(0o755) - - # ---- 5. 压缩(zip 根即容器工作目录)---- + output.mkdir(parents=True, exist_ok=True) zip_path = output / f"{name}.zip" - zip_path.unlink(missing_ok=True) - shutil.make_archive(str(output / name), "zip", root_dir=stage) - - # 暂存目录保留便于排查,也可直接作为容器工作目录 + _zip_project(project_dir, zip_path) return zip_path @@ -227,14 +162,8 @@ def main() -> None: sys.exit(1) print(f"✅ 打包完成: {zip_path} ({_format_size(zip_path)})") - print("📦 产物布局(zip 根即容器工作目录):") - print(f" workers/{args.name}/ 插件代码") - print(f" config/workers/{args.name}/ 插件配置") - print(" config/zenoh_config.json5 Zenoh 会话配置") - print(" config.toml 最小根配置(仅 [zenoh] 节)") - print(" pyproject.toml uv 依赖清单") - print(" start.sh 容器入口") - print(f"\n🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh") + print(f"📦 产物 = {_rel(WORKERS_DIR / args.name)}/ 项目根的直接压缩") + print(f"🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh") def build_plugin(): diff --git a/src/chongming_cli/gen_plugin.py b/src/chongming_cli/gen_plugin.py index 982cc12..31b3f7e 100644 --- a/src/chongming_cli/gen_plugin.py +++ b/src/chongming_cli/gen_plugin.py @@ -2,27 +2,32 @@ 插件脚手架生成器 — 生成风格统一的 Worker 插件模板 用法: - uv run gen-plugin [operation ...] + uv run gen_plugin [operation ...] [--process] 示例: - # 生成插件骨架(含一个示例操作): - uv run gen-plugin myplugin + # 生成 inline 插件骨架(默认,代码在 src/plugins/,配置在 config/workers/): + uv run gen_plugin myplugin + + # 生成进程插件完整项目(自包含,代码 + 配置 + 依赖清单 + 入口脚本): + uv run gen_plugin myplugin --process # 指定初始操作: - uv run gen-plugin myplugin analyze count + uv run gen_plugin myplugin analyze count --process # 为已存在的插件增量添加操作: - uv run gen-plugin myplugin add + uv run gen_plugin myplugin add --process -工作流: - 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 # 启动验证 +生成内容(--process): workers// 自包含项目根(= 打包 zip 的镜像): + workers//workers//__init__.py 插件入口(Plugin 基类 + 模块级工厂) + workers//workers//router/ 端点模块聚合 + 每个操作的 RPC 实现骨架 + workers//config/workers//config.toml 插件注册(module=workers., load_mode=external) + workers//config/workers//.toml 每个操作的 RPC 端点定义 + workers//config/zenoh_config.json5 Zenoh 会话配置(自仓库根复制) + workers//config.toml 最小根配置(仅 [zenoh] 节) + workers//pyproject.toml uv 依赖清单(chongming-worker + orjson) + workers//start.sh 入口脚本(uv sync + worker_serve) -生成内容: +生成内容(默认 inline): config/workers//config.toml Worker 注册 + 运行时配置 config/workers//.toml 每个操作的 RPC 端点定义 src/plugins//__init__.py 基于 Plugin 基类的统一入口 @@ -31,7 +36,9 @@ src/plugins//router/.py 每个操作的 RPC 实现骨架 src/plugins//models/ 由 gen_models 生成(见步骤 2) """ +import argparse import re +import shutil import sys from pathlib import Path @@ -41,9 +48,15 @@ from ._root import find_project_root ROOT = find_project_root() CONFIG_DIR = ROOT / "config" / "workers" PLUGIN_DIR = ROOT / "src" / "plugins" +PROCESS_DIR = ROOT / "workers" +ZENOH_CONFIG = ROOT / "config" / "zenoh_config.json5" NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$") +# chongming-worker 内网 git 源(与网关 pyproject.toml [tool.uv.sources] 保持一致) +CHONGMING_WORKER_GIT = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git" +PYPI_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" + # ============================================================ # 模板 @@ -54,7 +67,34 @@ def snake_to_camel(name: str) -> str: return "".join(part.title() for part in name.split("_")) -def _gen_config_toml(name: str) -> str: +def _gen_config_toml(name: str, process: bool = False) -> str: + if process: + return f'''# ====== 插件注册信息(进程插件:独立进程运行) ====== +[workers] +type = "{name}" +module = "workers.{name}" +factory = "create_workers" +cleanup = "cleanup" +count = 1 +# 进程插件:由 Docker/K8s 编排拉起独立进程(容器入口 ./start.sh / worker_serve --type {name}), +# 经 Zenoh 注册路由,网关动态挂载;与 src/plugins/ 下的 inline 插件可同时存在 +load_mode = "external" + +[worker] +name = "{name}" +version = "0.1.0" +description = "{name} 进程插件 — 独立进程运行,经 Zenoh 注册路由" +config_version = "1.0" +defaults = {{ method = "GET", timeout = 30.0, auth_required = false, cacheable = false, internal = false }} + +[worker.registration] +router_prefix = "/{name}" +tags = ["{name}", "process-plugin"] + +# ====== Worker 运行时配置(由 worker_serve 传递给 create_workers) ====== +[workers.config] +data_dir = "./data" +''' return f'''# {name} Worker 插件配置 [worker] @@ -79,9 +119,9 @@ data_dir = "./data" ''' -def _gen_operation_toml(name: str, operation: str) -> str: +def _gen_operation_toml(name: str, operation: str, gen_models_cmd: str) -> str: return f'''# {name}.{operation} — {operation} 操作 -# 修改后需重新运行: uv run gen_models config/workers/{name} src/plugins/{name} +# 修改后需重新运行: uv run gen_models {gen_models_cmd} [GET] cacheable = true summary = "{operation} 操作描述" @@ -92,15 +132,15 @@ result = {{ type = "str", required = true }} ''' -def _gen_plugin_init(name: str) -> str: +def _gen_plugin_init(name: str, config_rel: str, code_rel: 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} + 1. 编辑 {config_rel}/*.toml 定义 RPC 端点 + 2. uv run gen_models {config_rel} {code_rel} 3. 在 router/<操作>.py 中实现业务逻辑 4. 在 on_init / on_cleanup 中初始化 / 清理资源 """ @@ -200,11 +240,11 @@ def _gen_router_init(name: str, operations: list[str]) -> str: return "\n".join(lines) -def _gen_router_operation(name: str, operation: str) -> str: +def _gen_router_operation(name: str, operation: str, config_rel: str) -> str: op_camel = snake_to_camel(operation) return f'''"""RPC 端点: {name}.{operation} -模型由 gen_models 生成(编辑 config/workers/{name}/{operation}.toml 后重新生成)。 +模型由 gen_models 生成(编辑 {config_rel}/{operation}.toml 后重新生成)。 """ from chongming_worker.decorators import rpc_method @@ -223,6 +263,95 @@ async def {operation}(request: {op_camel}GetRequest) -> {op_camel}GetResponse: ''' +def _gen_root_config_toml() -> str: + """最小根配置(仅 [zenoh] 节,worker 进程只读它)。""" + return f"""# 最小根配置(worker 进程启动只读 [zenoh] 节) +[zenoh] +json5_path = "config/zenoh_config.json5" +""" + + +def _gen_pyproject_toml(name: str) -> str: + """插件 uv 依赖清单(chongming-worker + 扩展依赖,扩展依赖自行追加)。""" + deps = [ + "chongming-worker", + "orjson", # chongming-worker 运行时依赖(SDK metadata 未声明,容器内需显式安装) + ] + deps_lines = "\n".join(f' "{d}",' for d in deps) + return f"""# {name} 进程插件依赖清单(扩展依赖直接在此追加,如 "minio") +[project] +name = "{name}-worker" +version = "0.1.0" +description = "{name} 进程插件(external 模式,独立容器部署)" +requires-python = ">=3.12" +dependencies = [ +{deps_lines} +] + +[project.scripts] +worker_serve = "chongming_worker.process:worker_serve" + +[[tool.uv.index]] +url = "{PYPI_MIRROR}" + +[tool.uv.sources] +chongming-worker = {{ git = "{CHONGMING_WORKER_GIT}" }} +""" + + +def _gen_start_sh(name: str) -> str: + """容器入口脚本:uv 引导依赖 + 启动 worker 进程。""" + return f"""#!/usr/bin/env bash +# {name} 进程插件容器入口(由 gen_plugin --process 生成) +# 要求: 镜像预装 uv(https://docs.astral.sh/uv/) +set -e +cd "$(dirname "$0")" + +uv sync --no-dev +exec uv run worker_serve --type {name} "$@" +""" + + +def _gen_readme(name: str) -> str: + return f"""# {name} 进程插件 + +独立进程运行的 Worker 插件(`load_mode = "external"`),与网关零导入关系、仅经 Zenoh 通讯。 + +## 目录结构(开发目录即打包产物) + +```text +. +├── workers/{name}/ 插件代码(模块 workers.{name}) +├── config/ +│ ├── zenoh_config.json5 Zenoh 会话配置 +│ └── workers/{name}/ 插件注册 + 端点配置(config.toml + .toml) +├── config.toml 最小根配置(仅 [zenoh] 节) +├── pyproject.toml uv 依赖清单(扩展依赖直接在此追加) +└── start.sh 入口脚本(uv sync + uv run worker_serve --type {name}) +``` + +## 开发 + +```bash +# 生成 Pydantic 模型(在仓库根执行,目录参数相对仓库根) +uv run gen_models workers/{name}/config/workers/{name} workers/{name}/workers/{name} + +# 本地调试(在项目根执行,或直接 ./start.sh) +cd workers/{name} +uv sync # 首次:安装依赖(项目独立 venv,与容器一致) +uv run worker_serve --type {name} +``` + +## 打包(在仓库根执行) + +```bash +uv run build_plugin {name} # → dist/{name}.zip(= 本目录的直接压缩) +``` + +详见 docs/process-plugin.md。 +""" + + # ============================================================ # 主逻辑 # ============================================================ @@ -236,9 +365,9 @@ def _write(path: Path, content: str) -> None: print(f"✅ 生成: {path.relative_to(ROOT)}") -def _existing_operations(plugin_dir: Path) -> list[str]: +def _existing_operations(code_dir: Path) -> list[str]: """扫描 router/ 下已有的操作文件(排除 __init__.py)。""" - router_dir = plugin_dir / "router" + router_dir = code_dir / "router" if not router_dir.exists(): return [] ops = [] @@ -249,66 +378,112 @@ def _existing_operations(plugin_dir: Path) -> list[str]: return ops -def generate_plugin(name: str, operations: list[str]) -> int: +def _gen_models_cmd(config_dir: Path, code_dir: Path) -> str: + """gen_models 的两个目录参数(相对 ROOT,命令在 ROOT 下执行)。""" + return f"{config_dir.relative_to(ROOT)} {code_dir.relative_to(ROOT)}" + + +def generate_plugin(name: str, operations: list[str], process: bool = False) -> 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 process: + project_dir = PROCESS_DIR / name + config_dir = project_dir / "config" / "workers" / name + code_dir = project_dir / "workers" / name + else: + config_dir = CONFIG_DIR / name + code_dir = PLUGIN_DIR / name + gen_models_cmd = _gen_models_cmd(config_dir, code_dir) + is_new = not code_dir.exists() # ---- 首次创建: 完整骨架 ---- if is_new: - _write(config_dir / "config.toml", _gen_config_toml(name)) + _write(config_dir / "config.toml", _gen_config_toml(name, process)) 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( + _write(config_dir / f"{op}.toml", _gen_operation_toml(name, op, gen_models_cmd)) + _write(code_dir / "__init__.py", _gen_plugin_init( + name, config_dir.relative_to(ROOT), code_dir.relative_to(ROOT) + )) + _write(code_dir / "global_var.py", _gen_global_var(name)) + (code_dir / "models" / "__init__.py").parent.mkdir(parents=True, exist_ok=True) + (code_dir / "models" / "__init__.py").write_text( '"""模型导出 — 由 gen_models 生成,请勿手动修改"""\n', encoding="utf-8", ) - print(f"✅ 生成: {plugin_dir.relative_to(ROOT)}/models/__init__.py") + print(f"✅ 生成: {code_dir.relative_to(ROOT)}/models/__init__.py") + + if process: + _write(project_dir / "config.toml", _gen_root_config_toml()) + _write(project_dir / "pyproject.toml", _gen_pyproject_toml(name)) + _write(project_dir / "README.md", _gen_readme(name)) + _write(project_dir / "start.sh", _gen_start_sh(name)) + (project_dir / "start.sh").chmod(0o755) + # Zenoh 会话配置自仓库根复制(后续可自行修改为插件专用配置) + if ZENOH_CONFIG.exists(): + dest = project_dir / "config" / "zenoh_config.json5" + if dest.exists(): + print(f"⏭️ 跳过(已存在): {dest.relative_to(ROOT)}") + else: + shutil.copy(ZENOH_CONFIG, dest) + print(f"✅ 生成: {dest.relative_to(ROOT)}(自 {_rel(ZENOH_CONFIG)} 复制)") + else: + print( + f"⚠️ 未找到 Zenoh 配置: {_rel(ZENOH_CONFIG)}," + "请自行放入 config/zenoh_config.json5" + ) # ---- 增量: 仅补 operations(已存在文件不覆盖)---- - existing = _existing_operations(plugin_dir) + existing = _existing_operations(code_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)) + _write(config_dir / f"{op}.toml", _gen_operation_toml(name, op, gen_models_cmd)) + _write(code_dir / "router" / f"{op}.py", _gen_router_operation(name, op, str(config_dir.relative_to(ROOT)))) # ---- 重写 router/__init__.py(聚合所有操作)---- - all_ops = _existing_operations(plugin_dir) + all_ops = _existing_operations(code_dir) if all_ops: - init_file = plugin_dir / "router" / "__init__.py" + init_file = code_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(" 3. uv run serve") + mode = "进程插件项目" if process else "inline 插件" + print(f"\n📋 {mode} '{name}' 已就绪,下一步:") + print(f" 1. uv run gen_models {gen_models_cmd}") + print(f" 2. 编辑 {code_dir.relative_to(ROOT)}/router/ 实现业务逻辑") + if process: + print(f" 3. cd {PROCESS_DIR.relative_to(ROOT)}/{name} && ./start.sh 或 uv run worker_serve --type {name}") + print(f" 4. uv run build_plugin {name}") + else: + print(" 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) +def _rel(path: Path) -> Path | str: + """相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。""" + try: + return path.relative_to(ROOT) + except ValueError: + return path - name = args[0] - operations = args[1:] or ["ping"] - sys.exit(generate_plugin(name, operations)) + +def main() -> None: + parser = argparse.ArgumentParser(description="chongming 插件脚手架生成器") + parser.add_argument("name", help="插件名(如 myplugin)") + parser.add_argument("operations", nargs="*", help="初始操作名(默认 ping;`add` 为增量添加)") + parser.add_argument( + "--process", + action="store_true", + help="生成进程插件完整项目(workers//,自包含,可打包部署)", + ) + args = parser.parse_args() + + operations = args.operations or ["ping"] + sys.exit(generate_plugin(args.name, operations, process=args.process)) def gen_plugin():