From f645c9df650af7cd9826137a5a9940862cdd2599 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Mon, 31 Aug 2026 16:30:15 +0800 Subject: [PATCH] =?UTF-8?q?modify:=20rust=20=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/chongming_cli/build_plugin.py | 14 +- src/chongming_cli/gen_plugin.py | 788 +++++++++++++++++++++++++++++- 3 files changed, 791 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 13f6a39..e9a5041 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "chongming-cli" -version = "0.2.2" +version = "0.2.4" description = "chongming 命令脚本集合:构建打包 / 进程插件打包 / 插件脚手架 / 模型生成" readme = "README.md" requires-python = ">=3.12" diff --git a/src/chongming_cli/build_plugin.py b/src/chongming_cli/build_plugin.py index 68263ad..c808db6 100644 --- a/src/chongming_cli/build_plugin.py +++ b/src/chongming_cli/build_plugin.py @@ -20,7 +20,8 @@ └── start.sh 容器入口: uv sync + uv run chongming-worker --type 说明: - - 打包自动排除 __pycache__ / *.pyc / .venv / *.egg-info / .git + - 打包自动排除运行时/构建产物: __pycache__ / *.pyc / .venv / *.egg-info / .git / + cache_data(diskcache) / data(worker data_dir) / dist(构建输出) / logs / *.log - start.sh 权限强制为 0o755(防止 git 丢执行位导致容器无法启动) - 新项目骨架用 `uv run gen_plugin --process` 生成 """ @@ -36,12 +37,14 @@ from ._root import find_project_root NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$") # 打包排除的路径模式(目录名 / glob) -EXCLUDE_DIRS = {"__pycache__", ".venv", ".git"} -EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".egg-info") +# - 运行时/构建产物:cache_data(diskcache)/ data(worker data_dir)/ dist(构建输出)/ logs +EXCLUDE_DIRS = {"__pycache__", ".venv", ".git", "cache_data", "data", "dist", "logs", ".pytest_cache"} +EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".egg-info", ".log") # 项目根目录(CHONGMING_ROOT 环境变量 > CWD 向上查找,见 _root.py) ROOT = find_project_root() -WORKERS_DIR = ROOT / "workers" +# WORKERS_DIR = ROOT / "workers" +WORKERS_DIR = ROOT def _rel(path: Path) -> Path | str: @@ -119,7 +122,8 @@ def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path: if not NAME_RE.match(name): raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)") - project_dir = WORKERS_DIR / name + # project_dir = WORKERS_DIR / name + project_dir = WORKERS_DIR if not project_dir.is_dir(): raise FileNotFoundError(f"未找到插件项目目录: {_rel(project_dir)}/") diff --git a/src/chongming_cli/gen_plugin.py b/src/chongming_cli/gen_plugin.py index 2dab89d..4276dca 100644 --- a/src/chongming_cli/gen_plugin.py +++ b/src/chongming_cli/gen_plugin.py @@ -2,7 +2,7 @@ 插件脚手架生成器 — 生成风格统一的 Worker 插件模板 用法: - uv run gen_plugin [operation ...] [--process] + uv run gen_plugin [operation ...] [--process] [--rust] 示例: # 生成 inline 插件骨架(默认,代码在 src/plugins/,配置在 config/workers/): @@ -11,6 +11,9 @@ # 生成进程插件完整项目(自包含,代码 + 配置 + 依赖清单 + 入口脚本): uv run gen_plugin myplugin --process + # 生成 Rust 进程插件完整项目(自包含,配置在插件根,build.sh 打包): + uv run gen_plugin myplugin --rust + # 指定初始操作: uv run gen_plugin myplugin analyze count --process @@ -27,6 +30,23 @@ workers//pyproject.toml uv 依赖清单(chongming-worker + orjson) workers//start.sh 入口脚本(uv sync + chongming-worker) +生成内容(--rust): workers// 自包含 Rust 项目根(= 可打包 zip 的镜像,工程化分层, + 结构对位 workers/rsdemo): + workers//Cargo.toml crate(path 依赖 chongming-worker-rs;可选 redis-cache / database feature) + workers//src/lib.rs 库层:声明各模块(args/locator/util/app/handlers/generated_models) + workers//src/main.rs 入口(薄):CLI 解析 + 日志 + runtime + App::init + register + serve + dispose + workers//src/args.rs clap 命令行参数(--type/--count/--config/--config_dir/--instance) + workers//src/locator.rs 插件根自定位 plugin_root() + 配置路径解析(Locator) + workers//src/util.rs 通用小工具 now_ts / deser(含单测) + workers//src/app.rs 装配 App:数据库生命周期 App::init/dispose + 端点注册 + 模型指纹校验 + workers//src/generated_models.rs 模型(gen_models 生成,指纹对齐端点 TOML) + workers//src/handlers/.rs 每个端点的业务实现(每端点一文件) + workers//config/workers//*.toml 插件注册 + 端点定义(事实来源) + workers//config/zenoh_config.json5 Zenoh 会话配置(自仓库根复制) + workers//config.toml 最小根配置(仅 [zenoh] 节) + workers//build.sh 一键打包(构建 release + 打包插件根含配置) + workers//README.md / .gitignore + 生成内容(默认 inline): config/workers//config.toml Worker 注册 + 运行时配置 config/workers//.toml 每个操作的 RPC 端点定义 @@ -39,8 +59,10 @@ import argparse import re import shutil +import subprocess import sys from pathlib import Path +from string import Template from ._root import find_project_root @@ -81,6 +103,22 @@ count = 1 # 经 Zenoh 注册路由,网关动态挂载;与 src/plugins/ 下的 inline 插件可同时存在 load_mode = "external" +[workers.cache] +backend = "diskcache" # "diskcache"(默认) | "redis" +cache_store_path = "./cache_data" + +# 启动时该类 worker 的全局副本数优先来源(指 config/workers//config.toml 的 count): +# "toml" (默认) = 经典模式:启动时用 toml 的 count 覆盖 cache 并写回为新的全局总量 +# "cache" = 全局模式:chongming-cache 为单一事实来源,toml 仅在 cache 缺失时引导 +launch_worker_count = "toml" + +# redis 后端专用(backend = "redis" 时生效,均有默认值) +# redis_host = "127.0.0.1" +# redis_port = 6379 +# redis_db = 0 +# redis_password = "" +# redis_prefix = "chongming:" + [worker] name = "{name}" version = "0.1.0" @@ -279,10 +317,13 @@ def _gen_pyproject_toml(name: str) -> str: uv sync 会跳过其入口点;worker 进程入口由 SDK 的 chongming-worker 脚本提供。 """ deps = [ - "chongming-cache", "chongming-worker", ] + deps_dev = [ + "chongming-cli", + ] deps_lines = "\n".join(f' "{d}",' for d in deps) + deps_dev_lines = "\n".join(f' "{d}",' for d in deps_dev) return f"""# {name} 进程插件依赖清单(扩展依赖直接在此追加,如 "minio") [project] name = "{name}-worker" @@ -301,7 +342,7 @@ url = "{PYPI_MIRROR}" [dependency-groups] dev = [ - "chongming-cli", +{deps_dev_lines} ] """ @@ -350,9 +391,10 @@ uv sync # 首次:安装依赖(项目独立 venv,与容 uv run chongming-worker --type {name} ``` -## 打包(在仓库根执行) +## 打包(在插件根目录执行) ```bash +cd workers/{name} uv run build_plugin {name} # → dist/{name}.zip(= 本目录的直接压缩) ``` @@ -360,6 +402,648 @@ uv run build_plugin {name} # → dist/{name}.zip(= 本目录的直接压缩 """ +# ============================================================ +# Rust 进程插件模板(--rust,基于 utils/worker-rs SDK + workers/rsdemo 参考实现) +# ============================================================ + +def _gen_rust_config_toml(name: str) -> str: + """插件注册 + [workers] 配置([workers.cache] 默认注释禁用)。""" + return f'''# ====== 插件注册信息(Rust 进程插件:独立进程运行) ====== +[workers] +type = "{name}" +module = "{name}" +factory = "create_workers" +cleanup = "cleanup" +count = 1 +# 进程插件:由 Docker/K8s 编排拉起独立进程(容器入口 ./start.sh / 二进制 --type {name}), +# 经 Zenoh 注册路由;load_mode = "external" 必须保留,网关跳过进程内加载 +load_mode = "external" + +[worker] +name = "{name}" +version = "0.1.0" +description = "{name} 进程插件(Rust)— 独立进程运行,经 Zenoh 注册路由" +config_version = "1.0" + +[worker.defaults] +timeout = 30.0 +cacheable = false +auth_required = false +internal = false +method = "GET" + +[worker.registration] +router_prefix = "/{name}" +tags = ["{name}", "process-plugin", "rust"] + +# ====== Worker 运行时配置(传给 create_workers) ====== +[workers.config] +greeting_prefix = "{name}" + +# ====== 数据库(可选,对齐 Python chongming_database) ====== +# 取消注释并编译开启 database feature 后可用(见 build.sh / Cargo.toml [features]): +# SQLite(type = "sqlite",database_path)或 PostgreSQL(type = "postgresql",url)。 +# 未配置 database 段时插件不建库(对齐 Python `db_name = None`)。 +# [workers.config.database] +# type = "sqlite" +# database_path = "./data/{name}.db" +# # postgresql 用 url: type = "postgresql"; url = "postgres://user:pwd@127.0.0.1:5432/db" + +# ====== 共享缓存(多实例收敛,可选) ====== +# 未配置 / backend 缺失时退化为"无缓存"(单实例语义):副本数与 [workers.config] +# 直接读写本地 config.toml。多实例部署**必须**启用 Redis 共享缓存,使副本数与配置 +# 跨实例单一事实来源收敛。启用后需以 --features redis-cache 编译(见 build.sh)。 +# [workers.cache] +# backend = "redis" # 仅 "redis" 支持跨实例 +# url = "redis://127.0.0.1:6379/0" +# # 也可用分段配置(与 url 二选一): +# # host = "127.0.0.1" +# # port = 6379 +# # db = 0 +# launch_worker_count = "cache" +''' + + +def _gen_rust_operation_toml(name: str, operation: str) -> str: + """端点 TOML:声明请求参数与响应模型(含 worker_id/timestamp,模板 handler 依此填充)。""" + return f'''# {name}.{operation} — {operation} 操作 +[GET] +summary = "{operation} 操作描述" +params = ["value: str"] + +[GET.response_model] +result = {{ type = "str", required = true }} +worker_id = {{ type = "str", required = false, default = "" }} +timestamp = {{ type = "float", required = true }} +''' + + +def _gen_rust_handler_op(operation: str, camel_op: str, name: str) -> str: + """单个端点的 handler 模块(src/handlers/.rs):反序列化 → 组装 → Reply::Json。""" + return f'''//! `{operation}` 端点:GET。 +use chongming_worker_rs::prelude::*; + +use crate::generated_models::{name} as models; +use crate::util::{{deser, now_ts}}; + +pub async fn {operation}(request: Request) -> Result {{ + let req: models::{camel_op}GetRequest = deser(&request.data)?; + let resp = models::{camel_op}GetResponse {{ + result: format!("hello: {{}}", req.value), + worker_id: Some(request.worker_id), + timestamp: now_ts(), + }}; + Ok(Reply::Json(serde_json::to_value(&resp).map_err(|e| e.to_string())?)) +}} +''' + + +def _gen_rust_handlers_mod(operations: list[str]) -> str: + mods = "\n".join(f"pub mod {op};" for op in operations) + return f"""//! 业务端点实现:每端点一文件,纯函数风格(依赖经参数注入,便于复用/单测)。 + +{mods} +""" + + +def _gen_rust_lib_rs() -> str: + return '''//! # chongming 进程插件(Rust)— 库层 +//! +//! 按 Rust 工程惯例组织:`lib.rs` 暴露各模块并承载可测逻辑,`main.rs` 仅做装配 +//! (CLI 解析 + 日志 + runtime + serve)。路由描述符/线协议见 docs/plugin-wire-protocol.md。 +//! +//! 模块职责: +//! - `args`:命令行参数(clap derive) +//! - `locator`:插件根定位 + 配置路径解析(配置自包含在插件根) +//! - `util`:通用小工具(时间戳 / 参数解析) +//! - `app`:插件装配(端点注册 + 模型指纹校验) +//! - `handlers`:业务端点实现(每端点一文件) +//! - `generated_models`:由端点 TOML 生成的请求/响应模型(勿手动修改) +//! +//! 模型一致性:端点 TOML 是唯一事实来源,改 TOML 后须重新运行 gen_models——启动时 +//! [`app::check_models_fingerprint`] 会校验指纹并告警。 +#![allow(dead_code)] + +pub mod app; +pub mod args; +pub mod generated_models; +pub mod handlers; +pub mod locator; +pub mod util; +''' + + +def _gen_rust_args_rs(name: str) -> str: + return f'''//! 命令行参数(clap derive)。 + +use clap::Parser; + +/// chongming 进程插件 Rust 实现 +#[derive(Parser, Debug)] +#[command(name = "{name}", about = "chongming 进程插件 Rust 实现")] +pub struct Args {{ + /// 插件类型(与 config/workers// 目录对应) + #[arg(long, default_value = "{name}")] + pub r#type: String, + + /// 副本数(覆盖插件配置中的 count) + #[arg(long)] + pub count: Option, + + /// 全局配置文件路径(读取 [zenoh] 节;缺省 = 插件根/config.toml) + #[arg(long)] + pub config: Option, + + /// 插件配置目录(config/workers 的上层;缺省 = 插件根/config/workers) + #[arg(long)] + pub config_dir: Option, + + /// 实例 id(多实例全局分摊用;缺省 {{hostname}}:{{pid}}) + #[arg(long)] + pub instance: Option, +}} +''' + + +_RUST_LOCATOR_RS = '''//! 插件根定位与配置文件路径解析。 +//! +//! 配置**自包含在插件根**(与 Python 进程插件一致):`plugin_root()` 从可执行文件 +//! 所在目录向上找第一个含 `config.toml` 的目录。因此从任意 CWD(含容器部署)运行, +//! worker 都能命中自身 `config.toml` + `config/`,打包 zip 解压即运行。 + +use std::path::PathBuf; + +/// 定位插件根目录:从可执行文件所在目录向上,找第一个含 `config.toml` 的目录。 +/// 这样开发(target/{profile}/{type})与部署(binary 与 config.toml 同目录)都能命中插件根; +/// 找不到时回退到可执行文件所在目录。 +pub fn plugin_root() -> PathBuf { + let exe_dir = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|q| q.to_path_buf())) + .unwrap_or_else(|| PathBuf::from(".")); + let mut dir = exe_dir.clone(); + loop { + if dir.join("config.toml").is_file() { + return dir; + } + if !dir.pop() { + break; + } + } + exe_dir +} + +/// 解析出的插件路径(调用方据此构建 `ServeOptions`)。 +pub struct Locator { + /// 插件根目录 + pub root: PathBuf, + /// 全局配置 `config.toml` 路径(读取 [zenoh] 节) + pub config_path: String, + /// 插件配置目录(config/workers 的上层) + pub config_dir: String, +} + +impl Locator { + /// 用 CLI 中可选的 `--config` / `--config_dir` 覆盖插件根默认路径。 + pub fn from_cli(config: Option, config_dir: Option) -> Self { + let root = plugin_root(); + let config_path = + config.unwrap_or_else(|| root.join("config.toml").to_string_lossy().into_owned()); + let config_dir = config_dir.unwrap_or_else(|| { + root.join("config").join("workers").to_string_lossy().into_owned() + }); + Self { + root, + config_path, + config_dir, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_paths_point_inside_plugin_root() { + let loc = Locator::from_cli(None, None); + assert!(loc.config_path.ends_with("config.toml")); + assert!(loc.config_dir.ends_with("config/workers")); + } +} +''' + + +_RUST_UTIL_RS = '''//! 通用小工具:时间戳、请求参数解析。 + +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::Value; + +/// 当前 Unix 时间戳(秒,浮点),对齐网关 timestamp 字段语义。 +pub fn now_ts() -> f64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs_f64()) + .unwrap_or(0.0) +} + +/// 从请求端信封 `data` 反序列化生成模型(强类型请求)。失败返回清晰错误。 +pub fn deser(data: &Value) -> Result { + serde_json::from_value(data.clone()).map_err(|e| format!("参数解析失败: {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Deserialize, PartialEq, Debug)] + struct Req { + name: String, + count: i64, + } + + #[test] + fn deser_maps_json_to_typed_request() { + let v = serde_json::json!({"name": "chongming", "count": 2}); + assert_eq!( + deser::(&v).ok().unwrap(), + Req { name: "chongming".into(), count: 2 } + ); + } + + #[test] + fn deser_reports_missing_field() { + let v = serde_json::json!({"name": "chongming"}); // 缺 count + assert!(deser::(&v).is_err()); + } + + #[test] + fn now_ts_is_positive() { + assert!(now_ts() > 1_700_000_000.0); + } +} +''' + + +_RUST_APP_TPL = Template('''//! 插件装配:数据库生命周期、端点注册、模型指纹校验。 +//! +//! [`App`] 持有插件级共享资源(如数据库连接池),把各 handler 注册进 +//! `HandlerRegistry`,并在退出时释放资源。`main.rs` 只负责 CLI 解析、日志、runtime +//! 与调用本模块。 + +#[cfg(feature = "database")] +use std::sync::Arc; + +#[cfg(feature = "database")] +use chongming_worker_rs::database::{init_database, Database}; +use chongming_worker_rs::gen; +use chongming_worker_rs::HandlerRegistry; + +use crate::generated_models::${type} as models; +use crate::handlers; + +/// 建表 SQL(SQLite 方言;database feature + config 声明 [workers.config].database 时生效)。 +#[cfg(feature = "database")] +pub const CREATE_TABLE_SQL: &str = + "CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL)"; + +/// 插件运行时上下文:持有可共享资源,供 handler 与装配使用。 +pub struct App { + worker_type: String, + #[cfg(feature = "database")] + db: Option>, +} + +impl App { + /// 按插件类型与配置目录初始化(含可选数据库)。未编译 database feature 或 + /// `[workers.config].database` 未配置时 `db = None`,插件降级为不建库 + /// (对齐 Python `db_name = None`)。 + pub async fn init(worker_type: &str, config_dir: &str) -> Result { + #[cfg(feature = "database")] + { + let db = init_database(worker_type, config_dir, Some(CREATE_TABLE_SQL)) + .await? + .map(Arc::new); + return Ok(Self { + worker_type: worker_type.into(), + db, + }); + } + #[cfg(not(feature = "database"))] + { + let _ = config_dir; + Ok(Self { worker_type: worker_type.into() }) + } + } + + /// 把全部端点注册进 registry。subject 按 `.` 与端点 TOML 对齐。 + /// 依赖共享资源(如数据库连接池)的端点用闭包捕获 `Arc` 注入。 + pub fn register(&self, registry: &mut HandlerRegistry) { + let subject = |op: &str| format!("{}.{}", self.worker_type, op); +${registrations} + } + + /// 优雅关闭持有的共享资源(数据库连接池),对齐 Python cleanup 里的 dispose。 + pub async fn dispose(&self) { + #[cfg(feature = "database")] + if let Some(db) = &self.db { + db.dispose().await; + } + } +} + +/// 模型一致性校验:端点 TOML 是唯一事实来源,指纹不匹配时告警并提示重新生成。 +pub fn check_models_fingerprint(worker_type: &str, config_dir: &str) { + let fp = match gen::fingerprint(worker_type, config_dir) { + Ok(f) => f, + Err(e) => { + tracing::warn!("[{worker_type}] 计算模型指纹失败(跳过一致性校验): {e}"); + return; + } + }; + if fp != models::FINGERPRINT { + tracing::warn!( + "[{worker_type}] 模型指纹不匹配:端点 TOML 已变更但未重新生成模型。\\ + 运行: cargo run --manifest-path utils/worker-rs/Cargo.toml --bin gen_models -- \\ + --types {worker_type} --config-dir workers/${type}/config/workers \\ + --out workers/${type}/src/generated_models.rs" + ); + } else { + tracing::info!("[{worker_type}] 模型一致性校验通过(TOML ↔ 生成模型)"); + } +} +''') + + +_RUST_MAIN_TPL = Template('''//! ${type} 入口:只做装配——CLI 解析、日志初始化、构建 tokio runtime、调用库层 +//! (`App::init` / `register` / `serve` / `dispose`)。业务与结构逻辑见各子模块(src/lib.rs)。 +//! +//! 运行(任意 CWD 均可,self-locating config): +//! cargo run --manifest-path workers/${type}/Cargo.toml -- --type ${type} + +use std::sync::Arc; + +use chongming_worker_rs::{HandlerRegistry, ServeOptions}; +use clap::Parser; +use tracing_subscriber::EnvFilter; + +use ${type}::app; +use ${type}::args::Args; +use ${type}::locator::Locator; + +fn main() -> Result<(), String> { + let args = Args::parse(); + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + // 解析插件根与配置文件(支持 --config / --config_dir 覆盖) + let loc = Locator::from_cli(args.config, args.config_dir); + // 模型一致性校验:端点 TOML 变更后须重新运行 gen_models + app::check_models_fingerprint(&args.r#type, &loc.config_dir); + + let opts = ServeOptions { + config_path: loc.config_path, + config_dir: loc.config_dir.clone(), + count: args.count, + instance: args.instance, + }; + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| format!("创建 tokio runtime 失败: {e}"))? + .block_on(async { + // 装配:初始化共享资源(数据库连接池等,可选) + let app = app::App::init(&args.r#type, &loc.config_dir).await?; + + // 注册全部端点,随后进入 serve——阻塞直至 SIGTERM/SIGINT + let mut registry = HandlerRegistry::new(); + app.register(&mut registry); + let result = + chongming_worker_rs::serve::serve(&args.r#type, Arc::new(registry), opts).await; + + // 优雅退出后释放共享资源(对齐 Python cleanup 里的 dispose) + app.dispose().await; + result + }) +} +''') + + +def _gen_rust_main_rs(name: str, operations: list[str]) -> str: + return _RUST_MAIN_TPL.substitute(type=name) + + +def _gen_rust_app_rs(name: str, operations: list[str]) -> str: + registrations = "\n".join( + f' registry.register(&subject("{op}"), "GET", handlers::{op}::{op});' + for op in operations + ) + return _RUST_APP_TPL.substitute(type=name, registrations=registrations) + + +def _gen_rust_cargo_toml(name: str) -> str: + return '''[package] +name = "''' + name + '''" +version = "0.1.0" +edition = "2021" +description = "chongming 进程插件 Rust 实现(由 gen_plugin --rust 生成)" + +[dependencies] +chongming-worker-rs = { path = "../../utils/worker-rs" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +clap = { version = "4", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = "0.3" +# 生成流式端点时取消下方注释(SSE 逐块多回复需要 async-stream) +# async-stream = "0.3" +# 数据库后端(sqlite/postgres,sqlx;由 database feature 开启,与 SDK 同版本同 feature) +sqlx = { version = "0.8", default-features = false, features = [ + "runtime-tokio", + "sqlite", + "postgres", + "any", +], optional = true } + +[features] +# 启用共享缓存(Redis 后端,多实例全局收敛配置/副本数)。 +# 打包时: bash build.sh dist "redis-cache" +redis-cache = ["chongming-worker-rs/redis-cache"] +# 启用数据库(sqlx,对齐 Python chongming_database)。 +# 同时配置 config.toml 的 [workers.config.database] 段,并开启则生效。 +# 打包时: bash build.sh dist "database" +database = ["chongming-worker-rs/database", "sqlx"] +''' + + +def _gen_rust_readme(name: str) -> str: + return f"""# {name} 进程插件(Rust) + +独立进程运行的 Worker 插件(`load_mode = "external"`),与网关零导入关系、仅经 Zenoh 通讯。 +配置自包含在插件根(与 Python 进程插件一致),打包 zip 解压即运行。 + +## 目录结构(配置自包含,工程化分层) + +```text +workers/{name}/ +├── config.toml 最小根配置(仅 [zenoh].json5_path) +├── config/ +│ ├── zenoh_config.json5 Zenoh 会话配置 +│ └── workers/{name}/ 插件注册 + 端点配置(config.toml + .toml) +├── Cargo.toml 独立 crate(path 依赖 chongming-worker-rs;可选 redis-cache / database feature) +├── src/ +│ ├── lib.rs 库层:声明各模块(args/locator/util/app/handlers/generated_models) +│ ├── main.rs 入口(薄):CLI 解析 + 日志 + runtime + App::init + register + serve + dispose +│ ├── args.rs clap 命令行参数(--type/--count/--config/--config_dir/--instance) +│ ├── locator.rs 插件根自定位 `plugin_root()` + 配置路径解析(Locator) +│ ├── util.rs 通用小工具:`now_ts` / `deser`(含单测) +│ ├── app.rs 装配 `App`:数据库生命周期(App::init/dispose)+ 端点注册 + 模型指纹校验 +│ ├── generated_models.rs 模型(gen_models 生成,勿手动修改) +│ └── handlers/ +│ └── .rs 每个端点的业务实现(每端点一文件) +├── build.sh 一键打包(构建 release + 打包插件根,配置一并带上) +└── README.md +``` + +## 开发 + +```bash +# 在插件根目录 +cargo run --bin {name} -- --type {name} # 本地调试(任意 CWD,self-locating config) + +# 端点 TOML 变更后重新生成模型(在仓库根执行) +cargo run --manifest-path utils/worker-rs/Cargo.toml --bin gen_models -- \\ + --types {name} --config-dir workers/{name}/config/workers \\ + --out workers/{name}/src/generated_models.rs +``` + +新增端点流程:`config/workers/{name}/.toml`(事件来源)→ `src/handlers/.rs` +(实现)→ `src/app.rs` 的 `App::register` 注册 → 重新运行上方 gen_models。 + +## 数据库(可选,对齐 Python `chongming_database`) + +需开启 `database` feature 编译(`bash build.sh dist "database"`)并在 +`config.toml` 的 `[workers.config.database]` 段配置后端;未配置时插件不建库 +(对齐 Python `db_name = None`)。`App::init` 已包含数据库生命周期 +(`init_database` + 建表 + `dispose`),handler 用闭包捕获 `Arc` 注入即可。 + +## 打包(在插件根目录执行) + +```bash +cd workers/{name} +bash build.sh # → dist/{name}.zip(= 本目录的直接压缩,含 config) +bash build.sh dist "redis-cache" # 需要多实例共享缓存时 +bash build.sh dist "database" # 需要数据库(sqlx)时(配合 config.toml 的 [workers.config.database]) +``` + +产物 zip 根即容器工作目录:`{name}`(release 二进制)+ `config.toml` + `config/` + +`start.sh`,无需 Rust / Python 运行时,解压即运行(`./start.sh`)。 + +详见 docs/rust-plugin.md。 +""" + + +def _gen_rust_gitignore() -> str: + return """/target/ +/dist/ +*.zip +cache_data/ +data/ +logs/ +""" + + +_RUST_BUILD_SH = """#!/usr/bin/env bash +# Rust 进程插件打包器(自包含于插件根,与 Python `build_plugin` 对位) +# +# 用法(在插件根目录执行): +# bash build.sh # 构建 release + 打包 -> dist/.zip +# bash build.sh /tmp/pkg # 指定输出目录 +# bash build.sh dist "redis-cache" # 启用共享缓存(Redis 后端)编译 +# +# 产物 zip 根即容器工作目录:含 release 二进制 + config.toml + config/ +# (Zenoh 会话 + 端点 TOML)+ start.sh,配置随包一并带上,解压即运行(入口 ./start.sh)。 +# 排除 target(构建产物)与本地缓存目录。 +set -euo pipefail + +NAME="$(basename "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")" +OUTDIR="${1:-dist}" +CARGO_FEATURES="${2:-}" +PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ---- 1. 构建 release 二进制 ------------------------------------------- +if [ -n "$CARGO_FEATURES" ]; then + echo "▶ cargo build --release --features $CARGO_FEATURES" + cargo build --release --features "$CARGO_FEATURES" --manifest-path "$PLUGIN_DIR/Cargo.toml" +else + echo "▶ cargo build --release" + cargo build --release --manifest-path "$PLUGIN_DIR/Cargo.toml" +fi +BIN="$PLUGIN_DIR/target/release/$NAME" +test -x "$BIN" || { echo "❌ release 二进制不存在: $BIN"; exit 1; } + +# ---- 2. 校验插件根「配置自包含」---------------------------------------- +for f in "config.toml" "config/zenoh_config.json5" "config/workers/$NAME/config.toml"; do + test -f "$PLUGIN_DIR/$f" || { echo "❌ 插件根缺少 $f(配置需自包含在插件根)"; exit 1; } +done + +# ---- 3. 组装打包根(复用容器工作目录布局)------------------------------- +STAGE="$PLUGIN_DIR/target/package/${NAME}_stage" +rm -rf "$STAGE" +mkdir -p "$STAGE/config/workers/$NAME" + +cp "$BIN" "$STAGE/$NAME" +cp "$PLUGIN_DIR/config.toml" "$STAGE/config.toml" +cp "$PLUGIN_DIR/config/zenoh_config.json5" "$STAGE/config/zenoh_config.json5" +cp "$PLUGIN_DIR"/config/workers/"$NAME"/*.toml "$STAGE/config/workers/$NAME/" + +# start.sh:容器入口(与运行目录无关,二进制自身 self-locating config) +cat > "$STAGE/start.sh" </dev/null 2>&1; then + (cd "$STAGE" && zip -rq --symlinks "$ZIP" .) +else + python3 - "$STAGE" "$ZIP" <<'PY' +import os, sys, zipfile +stage, out = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf: + for root, _dirs, files in os.walk(stage): + for f in files: + src = os.path.join(root, f) + arc = os.path.relpath(src, stage) + zf.write(src, arc) +PY +fi + +SIZE=$(du -h "$ZIP" | cut -f1) +rm -rf "$STAGE" +echo "✅ Rust 插件打包完成: $ZIP ($SIZE)" +echo "📦 容器内部署: unzip $NAME.zip -d /app && cd /app && ./start.sh" +""" + + # ============================================================ # 主逻辑 # ============================================================ @@ -391,12 +1075,97 @@ def _gen_models_cmd(config_dir: Path, code_dir: Path) -> str: return f"{config_dir.relative_to(ROOT)} {code_dir.relative_to(ROOT)}" -def generate_plugin(name: str, operations: list[str], process: bool = False) -> int: +def _gen_rust_models(name: str, config_dir: Path, out: Path) -> bool: + """用 SDK 的 gen_models bin 从端点 TOML 生成 Rust 模型(generated_models.rs)。 + + 失败时返回 False(调用方写入空指纹占位并提示)。 + """ + cmd = [ + "cargo", "run", "--quiet", + "--manifest-path", str(ROOT / "utils" / "worker-rs" / "Cargo.toml"), + "--bin", "gen_models", + "--", "--types", name, + "--config-dir", str(config_dir), + "--out", str(out), + ] + r = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True) + if r.returncode == 0 and out.exists(): + print(f"✅ 生成: {out.relative_to(ROOT)}(endpoints → Rust 模型,指纹已对齐)") + return True + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text( + f'pub mod {name} {{\n #![allow(dead_code)]\n pub const FINGERPRINT: &str = "";\n}}\n', + encoding="utf-8", + ) + print(f"⚠️ gen_models 未执行成功,已生成空指纹占位({out.relative_to(ROOT)})。\n" + f" 请运行: {' '.join(cmd)}\n stderr: {r.stderr.strip()[:200]}") + return False + + +def _generate_rust_plugin(name: str, operations: list[str]) -> int: + """生成 Rust 进程插件完整项目(workers//,自包含 + 可打包,外部插件语义)。""" + project_dir = PROCESS_DIR / name + config_dir = project_dir / "config" / "workers" + src_dir = project_dir / "src" + + if project_dir.exists() and src_dir.exists(): + print(f"⏭️ 插件已存在: {project_dir.relative_to(ROOT)}/(Rust 暂不支持增量覆盖)") + print(f" • 新增端点: 在 {config_dir.relative_to(ROOT)}/{name}/ 加 .toml,") + print(" 在 src/handlers/ 加 .rs 并在 src/app.rs 的 App::register 注册,") + print(" 然后重新运行下方 gen_models 命令。") + return 0 + + for op in operations: + _write(config_dir / name / f"{op}.toml", _gen_rust_operation_toml(name, op)) + _write(config_dir / name / "config.toml", _gen_rust_config_toml(name)) + _write(project_dir / "config.toml", _gen_root_config_toml()) + _write(project_dir / "Cargo.toml", _gen_rust_cargo_toml(name)) + # 库 + 薄 bin 分层(工程化结构,与 workers/rsdemo 对位) + _write(src_dir / "lib.rs", _gen_rust_lib_rs()) + _write(src_dir / "main.rs", _gen_rust_main_rs(name, operations)) + _write(src_dir / "args.rs", _gen_rust_args_rs(name)) + _write(src_dir / "locator.rs", _RUST_LOCATOR_RS) + _write(src_dir / "util.rs", _RUST_UTIL_RS) + _write(src_dir / "app.rs", _gen_rust_app_rs(name, operations)) + _write(src_dir / "handlers" / "mod.rs", _gen_rust_handlers_mod(operations)) + for op in operations: + _write(src_dir / "handlers" / f"{op}.rs", + _gen_rust_handler_op(op, snake_to_camel(op), name)) + _write(project_dir / "build.sh", _RUST_BUILD_SH) + (project_dir / "build.sh").chmod(0o755) + _write(project_dir / "README.md", _gen_rust_readme(name)) + _write(project_dir / ".gitignore", _gen_rust_gitignore()) + + # Zenoh 会话配置自仓库根复制(后续可自行改为插件专用配置) + if ZENOH_CONFIG.exists(): + dest = project_dir / "config" / "zenoh_config.json5" + if not dest.exists(): + 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") + + # 生成模型 + 指纹(与端点 TOML 对齐,保证启动指纹校验通过) + # 注:gen_models --config_dir 是 config/workers 父目录(内含 / 子目录) + _gen_rust_models(name, config_dir, src_dir / "generated_models.rs") + + print(f"\n📋 Rust 进程插件 '{name}' 已就绪(外部插件,配置自包含在插件根),下一步:") + print(f" 1. cargo build --manifest-path {project_dir.relative_to(ROOT)}/Cargo.toml") + print(f" 2. cd {project_dir.relative_to(ROOT)} && cargo run --bin {name} -- --type {name}") + print(f" 3. 端点 TOML 变更后重新生成模型(见 README)") + print(f" 4. cd {project_dir.relative_to(ROOT)} && bash build.sh # 在插件根目录打包") + return 0 + + +def generate_plugin(name: str, operations: list[str], process: bool = False, rust: bool = False) -> int: """生成插件骨架。返回 0 表示成功,1 表示失败。""" if not NAME_RE.match(name): print(f"❌ 插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)") return 1 + if rust: + return _generate_rust_plugin(name, operations) + if process: project_dir = PROCESS_DIR / name config_dir = project_dir / "config" / "workers" / name @@ -465,7 +1234,7 @@ def generate_plugin(name: str, operations: list[str], process: bool = False) -> 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 chongming-worker --type {name}") - print(f" 4. uv run build_plugin {name}") + print(f" 4. cd {PROCESS_DIR.relative_to(ROOT)}/{name} && uv run build_plugin {name} # 在插件根目录打包") else: print(" 3. uv run serve") return 0 @@ -488,10 +1257,15 @@ def main() -> None: action="store_true", help="生成进程插件完整项目(workers//,自包含,可打包部署)", ) + parser.add_argument( + "--rust", + action="store_true", + help="生成 Rust 进程插件完整项目(workers//,配置自包含,build.sh 打包)", + ) args = parser.parse_args() operations = args.operations or ["ping"] - sys.exit(generate_plugin(args.name, operations, process=args.process)) + sys.exit(generate_plugin(args.name, operations, process=args.process, rust=args.rust)) def gen_plugin():