modify:进程插件开发环境目录结构变更
This commit is contained in:
@@ -7,9 +7,9 @@ chongming 网关的命令脚本集合,独立包维护(从 `src/chongming/scr
|
|||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
| ------------ | ------------------------------------------- |
|
| ------------ | ------------------------------------------- |
|
||||||
| `build` | 应用构建打包(PyInstaller / Nuitka) |
|
| `build` | 应用构建打包(PyInstaller / Nuitka) |
|
||||||
| `build_plugin` | 进程插件打包为可部署 zip |
|
| `build_plugin` | 进程插件项目根直接压缩为可部署 zip |
|
||||||
| `gen_models` | 从 `config/workers/<name>/*.toml` 生成 Pydantic 模型 |
|
| `gen_models` | 从插件配置 `*.toml` 生成 Pydantic 模型 |
|
||||||
| `gen_plugin` | 生成风格统一的 Worker 插件骨架 |
|
| `gen_plugin` | 生成风格统一的 Worker 插件骨架(`--process` 生成进程插件完整项目) |
|
||||||
|
|
||||||
另有 `chongming_cli.code_content` 模块:将目录下代码文件汇总为 Markdown(`python -m chongming_cli.code_content <dir>`)。
|
另有 `chongming_cli.code_content` 模块:将目录下代码文件汇总为 Markdown(`python -m chongming_cli.code_content <dir>`)。
|
||||||
|
|
||||||
@@ -21,9 +21,11 @@ chongming 网关的命令脚本集合,独立包维护(从 `src/chongming/scr
|
|||||||
uv run build pyinstaller # 构建打包
|
uv run build pyinstaller # 构建打包
|
||||||
uv run build --only mbank # 仅重新打包 .mbank 模块文件(跳过其余流程)
|
uv run build --only mbank # 仅重新打包 .mbank 模块文件(跳过其余流程)
|
||||||
uv run build --only mbank,vue # 只执行指定步骤
|
uv run build --only mbank,vue # 只执行指定步骤
|
||||||
uv run build_plugin demo # 进程插件打包 → dist/demo.zip
|
uv run build_plugin demo # 进程插件打包 → dist/demo.zip(= workers/demo/ 直接压缩)
|
||||||
uv run gen_plugin myplugin # 生成插件骨架
|
uv run gen_plugin myplugin # 生成 inline 插件骨架
|
||||||
uv run gen_models config/workers/myplugin src/plugins/myplugin
|
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 <step>` 可选步骤:`mbank` / `prepare` / `obfuscate` / `pack` / `clean` / `vue`,可逗号分隔或重复传多个。
|
`build --only <step>` 可选步骤:`mbank` / `prepare` / `obfuscate` / `pack` / `clean` / `vue`,可逗号分隔或重复传多个。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
进程插件打包器 — 将 external 进程插件打包为可部署 zip
|
进程插件打包器 — 将 external 进程插件开发目录打包为可部署 zip
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
uv run build_plugin <plugin_name> [--output <dir>]
|
uv run build_plugin <plugin_name> [--output <dir>]
|
||||||
@@ -8,46 +8,42 @@
|
|||||||
uv run build_plugin demo # 打包 workers/demo → dist/demo.zip
|
uv run build_plugin demo # 打包 workers/demo → dist/demo.zip
|
||||||
uv run build_plugin demo --output /tmp/pkg
|
uv run build_plugin demo --output /tmp/pkg
|
||||||
|
|
||||||
产物布局(zip 根即容器工作目录,unzip 后直接 ./start.sh 启动):
|
开发目录即产物: workers/<name>/ 是自包含的插件项目根(= 容器工作目录 = zip 根),
|
||||||
workers/<name>/ 插件代码(模块 workers.<name>)
|
打包只做校验 + 直接压缩,不再从仓库多处装配:
|
||||||
config/workers/<name>/ 插件注册 + 端点配置
|
|
||||||
config/zenoh_config.json5 Zenoh 会话配置
|
workers/<name>/ 插件项目根(zip 根即容器工作目录)
|
||||||
config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需)
|
├── workers/<name>/ 插件代码(模块 workers.<name>)
|
||||||
pyproject.toml uv 依赖清单(chongming-worker + 插件扩展依赖)
|
├── config/workers/<name>/ 插件注册 + 端点配置
|
||||||
start.sh 容器入口: uv sync + uv run worker_serve --type <name>
|
├── config/zenoh_config.json5 Zenoh 会话配置
|
||||||
|
├── config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需)
|
||||||
|
├── pyproject.toml uv 依赖清单(chongming-worker + 扩展依赖)
|
||||||
|
└── start.sh 容器入口: uv sync + uv run worker_serve --type <name>
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
- 插件扩展依赖声明在 workers/<name>/requirements.txt(可选,每行一个包),
|
- 打包自动排除 __pycache__ / *.pyc / .venv / *.egg-info / .git
|
||||||
打包时自动合并进 pyproject.toml dependencies
|
- start.sh 权限强制为 0o755(防止 git 丢执行位导致容器无法启动)
|
||||||
- 根 config.toml 仅提取 [zenoh] 节重新生成,避免把网关侧配置(如 JWT 密钥)泄露进插件容器
|
- 新项目骨架用 `uv run gen_plugin <name> --process` 生成
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
import tomllib
|
import tomllib
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ._root import find_project_root
|
from ._root import find_project_root
|
||||||
|
|
||||||
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||||
|
|
||||||
# chongming-worker 内网 git 源(与网关 pyproject.toml [tool.uv.sources] 保持一致)
|
# 打包排除的路径模式(目录名 / glob)
|
||||||
CHONGMING_WORKER_GIT = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git"
|
EXCLUDE_DIRS = {"__pycache__", ".venv", ".git"}
|
||||||
PYPI_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".egg-info")
|
||||||
|
|
||||||
# 项目根目录(CHONGMING_ROOT 环境变量 > CWD 向上查找,见 _root.py)
|
# 项目根目录(CHONGMING_ROOT 环境变量 > CWD 向上查找,见 _root.py)
|
||||||
ROOT = find_project_root()
|
ROOT = find_project_root()
|
||||||
WORKERS_DIR = ROOT / "workers"
|
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:
|
def _rel(path: Path) -> Path | str:
|
||||||
"""相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。"""
|
"""相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。"""
|
||||||
try:
|
try:
|
||||||
@@ -56,103 +52,79 @@ def _rel(path: Path) -> Path | str:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def _read_plugin_conf(name: str) -> dict:
|
def _check_project_layout(project_dir: Path, name: str) -> dict:
|
||||||
"""读取 config/workers/<name>/config.toml 完整内容(校验含 [workers] 节)。"""
|
"""
|
||||||
path = PLUGINS_CONFIG_DIR / name / "config.toml"
|
校验插件项目结构完整(运行时必需的文件),返回插件 [workers] 节配置。
|
||||||
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
|
|
||||||
|
|
||||||
|
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:
|
# 2. Zenoh 会话配置(worker 启动必需)
|
||||||
"""生成最小根 config.toml(仅 [zenoh] 节,worker 进程只读它)。"""
|
if not (project_dir / "config" / "zenoh_config.json5").exists():
|
||||||
with open(ROOT_CONFIG, "rb") as f:
|
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)
|
root_conf = tomllib.load(f)
|
||||||
json5_path = root_conf.get("zenoh", {}).get("json5_path", "config/zenoh_config.json5")
|
if not root_conf.get("zenoh"):
|
||||||
return f"""# 最小根配置(由 build_plugin 自动生成,仅含 worker 进程所需的 [zenoh] 节)
|
raise ValueError(f"{root_conf_path} 缺少 [zenoh] 节(worker 启动必需)")
|
||||||
[zenoh]
|
|
||||||
json5_path = "{json5_path}"
|
return workers
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_pyproject_toml(name: str, version: str, extra_deps: list[str]) -> str:
|
def _zip_project(project_dir: Path, zip_path: Path) -> None:
|
||||||
"""生成插件 uv 依赖清单(chongming-worker + 插件扩展依赖)。"""
|
"""把项目根内容压缩为 zip(zip 根即容器工作目录),排除缓存/环境目录。"""
|
||||||
deps = [
|
zip_path.unlink(missing_ok=True)
|
||||||
"chongming-worker",
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
"orjson", # chongming-worker 运行时依赖(SDK metadata 未声明,容器内需显式安装)
|
for path in sorted(project_dir.rglob("*")):
|
||||||
*extra_deps,
|
if not path.is_file():
|
||||||
]
|
continue
|
||||||
deps_lines = "\n".join(f' "{d}",' for d in deps)
|
rel = path.relative_to(project_dir)
|
||||||
return f"""# {name} 进程插件依赖清单(由 build_plugin 自动生成)
|
if any(part in EXCLUDE_DIRS for part in rel.parts):
|
||||||
[project]
|
continue
|
||||||
name = "{name}-worker"
|
if any(part.endswith(EXCLUDE_SUFFIXES) for part in rel.parts):
|
||||||
version = "{version}"
|
continue
|
||||||
description = "{name} 进程插件(external 模式,独立容器部署)"
|
info = zipfile.ZipInfo.from_file(path, str(rel))
|
||||||
requires-python = ">=3.12"
|
if rel.name == "start.sh":
|
||||||
dependencies = [
|
info.external_attr = 0o100755 << 16 # 强制可执行,防 git 丢执行位
|
||||||
{deps_lines}
|
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/<name>/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:
|
def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path:
|
||||||
"""
|
"""
|
||||||
打包进程插件为可部署 zip,返回产物路径。
|
打包进程插件为可部署 zip,返回产物路径。
|
||||||
|
|
||||||
失败(插件/配置缺失、契约不合法)抛 FileNotFoundError / ValueError。
|
失败(插件/配置缺失、结构不合法)抛 FileNotFoundError / ValueError。
|
||||||
"""
|
"""
|
||||||
if not NAME_RE.match(name):
|
if not NAME_RE.match(name):
|
||||||
raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
||||||
|
|
||||||
workers_src = WORKERS_DIR / name
|
project_dir = WORKERS_DIR / name
|
||||||
if not workers_src.is_dir():
|
if not project_dir.is_dir():
|
||||||
raise FileNotFoundError(f"未找到插件代码目录: {_rel(workers_src)}/")
|
raise FileNotFoundError(f"未找到插件项目目录: {_rel(project_dir)}/")
|
||||||
|
|
||||||
conf = _read_plugin_conf(name) # 缺配置在此抛 FileNotFoundError
|
workers = _check_project_layout(project_dir, name)
|
||||||
workers = conf["workers"]
|
|
||||||
|
|
||||||
plugin_type = workers.get("type", name)
|
|
||||||
load_mode = workers.get("load_mode", "inline")
|
load_mode = workers.get("load_mode", "inline")
|
||||||
if load_mode != "external":
|
if load_mode != "external":
|
||||||
print(
|
print(
|
||||||
@@ -160,48 +132,11 @@ def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path:
|
|||||||
"独立进程将与网关内副本重复服务。进程插件请在 [workers] 节设置 "
|
"独立进程将与网关内副本重复服务。进程插件请在 [workers] 节设置 "
|
||||||
"load_mode = \"external\"。"
|
"load_mode = \"external\"。"
|
||||||
)
|
)
|
||||||
version = str(conf.get("worker", {}).get("version", "0.1.0"))
|
|
||||||
|
|
||||||
output = Path(output_dir)
|
output = Path(output_dir)
|
||||||
stage = output / name
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
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 根即容器工作目录)----
|
|
||||||
zip_path = output / f"{name}.zip"
|
zip_path = output / f"{name}.zip"
|
||||||
zip_path.unlink(missing_ok=True)
|
_zip_project(project_dir, zip_path)
|
||||||
shutil.make_archive(str(output / name), "zip", root_dir=stage)
|
|
||||||
|
|
||||||
# 暂存目录保留便于排查,也可直接作为容器工作目录
|
|
||||||
return zip_path
|
return zip_path
|
||||||
|
|
||||||
|
|
||||||
@@ -227,14 +162,8 @@ def main() -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"✅ 打包完成: {zip_path} ({_format_size(zip_path)})")
|
print(f"✅ 打包完成: {zip_path} ({_format_size(zip_path)})")
|
||||||
print("📦 产物布局(zip 根即容器工作目录):")
|
print(f"📦 产物 = {_rel(WORKERS_DIR / args.name)}/ 项目根的直接压缩")
|
||||||
print(f" workers/{args.name}/ 插件代码")
|
print(f"🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh")
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
def build_plugin():
|
def build_plugin():
|
||||||
|
|||||||
+230
-55
@@ -2,27 +2,32 @@
|
|||||||
插件脚手架生成器 — 生成风格统一的 Worker 插件模板
|
插件脚手架生成器 — 生成风格统一的 Worker 插件模板
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
uv run gen-plugin <name> [operation ...]
|
uv run gen_plugin <name> [operation ...] [--process]
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
# 生成插件骨架(含一个示例操作):
|
# 生成 inline 插件骨架(默认,代码在 src/plugins/,配置在 config/workers/):
|
||||||
uv run gen-plugin myplugin
|
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
|
||||||
|
|
||||||
工作流:
|
生成内容(--process): workers/<name>/ 自包含项目根(= 打包 zip 的镜像):
|
||||||
1. uv run gen-plugin myplugin # 生成 config + src 骨架(统一风格)
|
workers/<name>/workers/<name>/__init__.py 插件入口(Plugin 基类 + 模块级工厂)
|
||||||
2. uv run gen_models config/workers/myplugin src/plugins/myplugin
|
workers/<name>/workers/<name>/router/ 端点模块聚合 + 每个操作的 RPC 实现骨架
|
||||||
# 生成 Pydantic 模型
|
workers/<name>/config/workers/<name>/config.toml 插件注册(module=workers.<name>, load_mode=external)
|
||||||
3. 编辑 src/plugins/myplugin/router/<operation>.py 实现业务逻辑
|
workers/<name>/config/workers/<name>/<op>.toml 每个操作的 RPC 端点定义
|
||||||
4. 编辑 src/plugins/myplugin/__init__.py 的 on_init / on_cleanup
|
workers/<name>/config/zenoh_config.json5 Zenoh 会话配置(自仓库根复制)
|
||||||
5. uv run serve # 启动验证
|
workers/<name>/config.toml 最小根配置(仅 [zenoh] 节)
|
||||||
|
workers/<name>/pyproject.toml uv 依赖清单(chongming-worker + orjson)
|
||||||
|
workers/<name>/start.sh 入口脚本(uv sync + worker_serve)
|
||||||
|
|
||||||
生成内容:
|
生成内容(默认 inline):
|
||||||
config/workers/<name>/config.toml Worker 注册 + 运行时配置
|
config/workers/<name>/config.toml Worker 注册 + 运行时配置
|
||||||
config/workers/<name>/<op>.toml 每个操作的 RPC 端点定义
|
config/workers/<name>/<op>.toml 每个操作的 RPC 端点定义
|
||||||
src/plugins/<name>/__init__.py 基于 Plugin 基类的统一入口
|
src/plugins/<name>/__init__.py 基于 Plugin 基类的统一入口
|
||||||
@@ -31,7 +36,9 @@
|
|||||||
src/plugins/<name>/router/<op>.py 每个操作的 RPC 实现骨架
|
src/plugins/<name>/router/<op>.py 每个操作的 RPC 实现骨架
|
||||||
src/plugins/<name>/models/ 由 gen_models 生成(见步骤 2)
|
src/plugins/<name>/models/ 由 gen_models 生成(见步骤 2)
|
||||||
"""
|
"""
|
||||||
|
import argparse
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -41,9 +48,15 @@ from ._root import find_project_root
|
|||||||
ROOT = find_project_root()
|
ROOT = find_project_root()
|
||||||
CONFIG_DIR = ROOT / "config" / "workers"
|
CONFIG_DIR = ROOT / "config" / "workers"
|
||||||
PLUGIN_DIR = ROOT / "src" / "plugins"
|
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_]*$")
|
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("_"))
|
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 插件配置
|
return f'''# {name} Worker 插件配置
|
||||||
|
|
||||||
[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} 操作
|
return f'''# {name}.{operation} — {operation} 操作
|
||||||
# 修改后需重新运行: uv run gen_models config/workers/{name} src/plugins/{name}
|
# 修改后需重新运行: uv run gen_models {gen_models_cmd}
|
||||||
[GET]
|
[GET]
|
||||||
cacheable = true
|
cacheable = true
|
||||||
summary = "{operation} 操作描述"
|
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)
|
cls = snake_to_camel(name)
|
||||||
return f'''"""{name.capitalize()} Worker 插件
|
return f'''"""{name.capitalize()} Worker 插件
|
||||||
|
|
||||||
统一风格模板 — 基于 chongming_worker.plugin.Plugin 基类。
|
统一风格模板 — 基于 chongming_worker.plugin.Plugin 基类。
|
||||||
|
|
||||||
工作流:
|
工作流:
|
||||||
1. 编辑 config/workers/{name}/*.toml 定义 RPC 端点
|
1. 编辑 {config_rel}/*.toml 定义 RPC 端点
|
||||||
2. uv run gen_models config/workers/{name} src/plugins/{name}
|
2. uv run gen_models {config_rel} {code_rel}
|
||||||
3. 在 router/<操作>.py 中实现业务逻辑
|
3. 在 router/<操作>.py 中实现业务逻辑
|
||||||
4. 在 on_init / on_cleanup 中初始化 / 清理资源
|
4. 在 on_init / on_cleanup 中初始化 / 清理资源
|
||||||
"""
|
"""
|
||||||
@@ -200,11 +240,11 @@ def _gen_router_init(name: str, operations: list[str]) -> str:
|
|||||||
return "\n".join(lines)
|
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)
|
op_camel = snake_to_camel(operation)
|
||||||
return f'''"""RPC 端点: {name}.{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
|
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 + <op>.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)}")
|
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/ 下已有的操作文件(排除 __init__.py)。"""
|
||||||
router_dir = plugin_dir / "router"
|
router_dir = code_dir / "router"
|
||||||
if not router_dir.exists():
|
if not router_dir.exists():
|
||||||
return []
|
return []
|
||||||
ops = []
|
ops = []
|
||||||
@@ -249,66 +378,112 @@ def _existing_operations(plugin_dir: Path) -> list[str]:
|
|||||||
return ops
|
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 表示失败。"""
|
"""生成插件骨架。返回 0 表示成功,1 表示失败。"""
|
||||||
if not NAME_RE.match(name):
|
if not NAME_RE.match(name):
|
||||||
print(f"❌ 插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
print(f"❌ 插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
config_dir = CONFIG_DIR / name
|
if process:
|
||||||
plugin_dir = PLUGIN_DIR / name
|
project_dir = PROCESS_DIR / name
|
||||||
is_new = not plugin_dir.exists()
|
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:
|
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:
|
for op in operations:
|
||||||
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op))
|
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op, gen_models_cmd))
|
||||||
_write(plugin_dir / "__init__.py", _gen_plugin_init(name))
|
_write(code_dir / "__init__.py", _gen_plugin_init(
|
||||||
_write(plugin_dir / "global_var.py", _gen_global_var(name))
|
name, config_dir.relative_to(ROOT), code_dir.relative_to(ROOT)
|
||||||
(plugin_dir / "models" / "__init__.py").parent.mkdir(parents=True, exist_ok=True)
|
))
|
||||||
(plugin_dir / "models" / "__init__.py").write_text(
|
_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',
|
'"""模型导出 — 由 gen_models 生成,请勿手动修改"""\n',
|
||||||
encoding="utf-8",
|
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(已存在文件不覆盖)----
|
# ---- 增量: 仅补 operations(已存在文件不覆盖)----
|
||||||
existing = _existing_operations(plugin_dir)
|
existing = _existing_operations(code_dir)
|
||||||
added = [op for op in operations if op not in existing]
|
added = [op for op in operations if op not in existing]
|
||||||
if added:
|
if added:
|
||||||
for op in added:
|
for op in added:
|
||||||
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op))
|
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op, gen_models_cmd))
|
||||||
_write(plugin_dir / "router" / f"{op}.py", _gen_router_operation(name, op))
|
_write(code_dir / "router" / f"{op}.py", _gen_router_operation(name, op, str(config_dir.relative_to(ROOT))))
|
||||||
|
|
||||||
# ---- 重写 router/__init__.py(聚合所有操作)----
|
# ---- 重写 router/__init__.py(聚合所有操作)----
|
||||||
all_ops = _existing_operations(plugin_dir)
|
all_ops = _existing_operations(code_dir)
|
||||||
if all_ops:
|
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.parent.mkdir(parents=True, exist_ok=True)
|
||||||
init_file.write_text(_gen_router_init(name, all_ops), encoding="utf-8")
|
init_file.write_text(_gen_router_init(name, all_ops), encoding="utf-8")
|
||||||
print(f"✅ 更新: {init_file.relative_to(ROOT)}")
|
print(f"✅ 更新: {init_file.relative_to(ROOT)}")
|
||||||
|
|
||||||
print(f"\n📋 插件 '{name}' 已就绪,下一步:")
|
mode = "进程插件项目" if process else "inline 插件"
|
||||||
print(f" 1. uv run gen_models config/workers/{name} src/plugins/{name}")
|
print(f"\n📋 {mode} '{name}' 已就绪,下一步:")
|
||||||
print(f" 2. 编辑 src/plugins/{name}/router/ 实现业务逻辑")
|
print(f" 1. uv run gen_models {gen_models_cmd}")
|
||||||
print(" 3. uv run serve")
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def _rel(path: Path) -> Path | str:
|
||||||
args = sys.argv[1:]
|
"""相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。"""
|
||||||
if not args:
|
try:
|
||||||
print("用法: uv run gen_plugin <name> [operation ...]")
|
return path.relative_to(ROOT)
|
||||||
print("示例:")
|
except ValueError:
|
||||||
print(" uv run gen_plugin myplugin")
|
return path
|
||||||
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"]
|
def main() -> None:
|
||||||
sys.exit(generate_plugin(name, operations))
|
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/<name>/,自包含,可打包部署)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
operations = args.operations or ["ping"]
|
||||||
|
sys.exit(generate_plugin(args.name, operations, process=args.process))
|
||||||
|
|
||||||
|
|
||||||
def gen_plugin():
|
def gen_plugin():
|
||||||
|
|||||||
Reference in New Issue
Block a user