modify: rust 插件

This commit is contained in:
2026-08-31 17:18:03 +08:00
commit e59ecfc0e3
22 changed files with 3689 additions and 0 deletions
+310
View File
@@ -0,0 +1,310 @@
//! 配置读取(与 Python SDK process.py 完全对位)
//!
//! - 全局配置 `config.toml`:仅读取 `[zenoh].json5_path`(默认 `config/zenoh_config.json5`)
//! - 插件配置 `config/workers/<type>/config.toml` 的 `[workers]` 节:展平 + mqtt_topics
use std::path::Path;
use serde_json::Value;
/// 全局配置(仅 zenoh 会话配置路径)
pub struct GlobalConfig {
pub zenoh_json5_path: String,
}
/// 插件配置([workers] 节,语义对齐 Python load_plugin_config)
pub struct PluginConfig {
pub worker_type: String,
pub module: String,
pub factory: String,
pub cleanup: String,
pub count: usize,
pub load_mode: String,
pub mqtt_topics: Vec<Value>,
/// 展平后的运行时配置(`[workers.config]` 等,供插件自用)
pub runtime: Value,
}
/// 读取全局 config.toml 的 `[zenoh].json5_path`
///
/// `json5_path` 若为相对路径,则基于 config.toml 所在目录解析为绝对路径——这样
/// worker 从任何 CWD 启动(含容器部署)都能定位到与之同目录的 zenoh 会话配置,
/// 与 Python 进程插件"配置自包含在插件根"的约定一致。
pub fn load_global_config(path: &str) -> Result<GlobalConfig, String> {
let cfg_path = std::path::Path::new(path);
let text =
std::fs::read_to_string(cfg_path).map_err(|e| format!("读取全局配置 {path} 失败: {e}"))?;
let cfg: toml::Value =
toml::from_str(&text).map_err(|e| format!("解析全局配置 {path} 失败: {e}"))?;
let json5_path = cfg
.get("zenoh")
.and_then(|z| z.get("json5_path"))
.and_then(toml::Value::as_str)
.unwrap_or("config/zenoh_config.json5");
let base_dir = cfg_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let resolved = std::path::Path::new(json5_path);
let zenoh_json5_path = if resolved.is_absolute() {
resolved.to_path_buf()
} else {
base_dir.join(resolved)
};
Ok(GlobalConfig {
zenoh_json5_path: zenoh_json5_path.to_string_lossy().into_owned(),
})
}
/// 读取 `config/workers/<type>/config.toml` 的 `[workers]` 节并展平
/// (展平规则与 Python SDK load_plugin_config 一致,worker 侧独立实现)
pub fn load_plugin_config(worker_type: &str, config_dir: &str) -> Result<PluginConfig, String> {
let path = Path::new(config_dir).join(worker_type).join("config.toml");
let text = std::fs::read_to_string(&path)
.map_err(|e| format!("未找到插件配置 {}: {e}", path.display()))?;
let plugin_conf: toml::Value =
toml::from_str(&text).map_err(|e| format!("解析插件配置 {} 失败: {e}", path.display()))?;
let workers = plugin_conf
.get("workers")
.and_then(toml::Value::as_table)
.ok_or_else(|| format!("{} 缺少 [workers] 节", path.display()))?;
let plugin_type = workers
.get("type")
.and_then(toml::Value::as_str)
.unwrap_or(worker_type)
.to_string();
let load_mode = workers
.get("load_mode")
.and_then(toml::Value::as_str)
.unwrap_or("inline")
.to_string();
// mqtt_topics:仅保留带 topic 的 dict,并补 worker 字段(与 Python 一致)
let mut mqtt_topics: Vec<Value> = Vec::new();
if let Some(topics) = workers.get("mqtt_topics").and_then(toml::Value::as_array) {
for t in topics {
let has_topic = t.get("topic").map(toml::Value::is_str).unwrap_or(false);
if !has_topic {
continue;
}
let mut t = t.clone();
let has_worker = t.get("worker").map(toml::Value::is_str).unwrap_or(false);
if !has_worker {
if let Some(table) = t.as_table_mut() {
table.insert(
"worker".to_string(),
toml::Value::String(plugin_type.clone()),
);
}
}
mqtt_topics.push(toml_to_json(&t));
}
}
// 展平:插件全量配置 + [workers] 节(除 config/mqtt_topics/load_mode)+ [workers.config]
let mut runtime: toml::Value = plugin_conf.clone();
if let Some(flat) = runtime.as_table_mut() {
for (k, v) in workers {
if k == "config" || k == "mqtt_topics" || k == "load_mode" {
continue;
}
flat.insert(k.clone(), v.clone());
}
}
if let Some(cfg) = workers.get("config") {
if let Some(table) = runtime.as_table_mut() {
table.insert("config".to_string(), cfg.clone());
}
}
// 顶层保留完整 [workers](含 config),与 Python runtime_config 结构一致
let runtime_json = toml_to_json(&runtime);
Ok(PluginConfig {
worker_type: plugin_type,
module: workers
.get("module")
.and_then(toml::Value::as_str)
.unwrap_or(&format!("plugins.{worker_type}"))
.to_string(),
factory: workers
.get("factory")
.and_then(toml::Value::as_str)
.unwrap_or("create_workers")
.to_string(),
cleanup: workers
.get("cleanup")
.and_then(toml::Value::as_str)
.unwrap_or("cleanup")
.to_string(),
count: workers
.get("count")
.and_then(toml::Value::as_integer)
.map(|c| c.max(1) as usize)
.unwrap_or(1),
load_mode,
mqtt_topics,
runtime: runtime_json,
})
}
/// toml::Value → serde_json::Value(插件运行时配置直接以 JSON 暴露)
pub fn toml_to_json(v: &toml::Value) -> Value {
match v {
toml::Value::String(s) => Value::String(s.clone()),
toml::Value::Integer(i) => Value::from(*i),
toml::Value::Float(f) => Value::from(*f),
toml::Value::Boolean(b) => Value::Bool(*b),
toml::Value::Datetime(d) => Value::String(d.to_string()),
toml::Value::Array(a) => Value::Array(a.iter().map(toml_to_json).collect()),
toml::Value::Table(t) => Value::Object(
t.iter()
.map(|(k, v)| (k.clone(), toml_to_json(v)))
.collect(),
),
}
}
// ============================================================
// [workers.config] 运行时读写(对齐 Python runtime.py 的 config 管理)
// ============================================================
/// 禁止通过 config_set 写入的保留键(破坏插件加载/路由语义)
pub const CONFIG_FORBIDDEN_KEYS: &[&str] = &[
"type",
"module",
"factory",
"cleanup",
"count",
"load_mode",
"mqtt_topics",
"workers",
"worker",
"defaults",
"registration",
];
/// 插件 config.toml 路径(config/workers/<type>/config.toml)
pub fn plugin_config_path(worker_type: &str, config_dir: &str) -> std::path::PathBuf {
std::path::Path::new(config_dir)
.join(worker_type)
.join("config.toml")
}
/// 读该类型 `[workers].config` 段(对象),缺失/损坏返回空对象
pub fn read_worker_config_section(worker_type: &str, config_dir: &str) -> Value {
let path = plugin_config_path(worker_type, config_dir);
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return Value::Object(Default::default()),
};
let data: toml::Value = toml::from_str(&text).unwrap_or(toml::Value::Table(Default::default()));
match data.get("workers").and_then(toml::Value::as_table) {
Some(w) => match w.get("config") {
Some(cfg) => toml_to_json(cfg),
None => Value::Object(Default::default()),
},
None => Value::Object(Default::default()),
}
}
/// 把 updates 合并写回该类型 `[workers].config`(仅 index0 落盘)。
/// 返回合并后的段;校验保留键失败抛 Err。
pub fn persist_worker_config_section(
worker_type: &str,
config_dir: &str,
updates: &Value,
) -> Result<Value, String> {
if !updates.is_object() {
return Err("config_set 需要 data.config 字段(非空对象)".into());
}
let bad: Vec<String> = updates
.as_object()
.unwrap()
.keys()
.filter(|k| CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()))
.cloned()
.collect();
if !bad.is_empty() {
return Err(format!("禁止修改保留配置键: {bad:?}"));
}
let path = plugin_config_path(worker_type, config_dir);
let current = merge_confirmed(read_worker_config_section(worker_type, config_dir), updates);
// 写回 toml 文件:[workers].config = merged(缺 [workers] 节则创建)
let text = std::fs::read_to_string(&path).unwrap_or_default();
let parsed: toml::Value =
toml::from_str(&text).unwrap_or_else(|_| toml::Value::Table(toml::value::Table::new()));
let mut data: toml::Value = if parsed.as_table().is_some() {
parsed
} else {
toml::Value::Table(toml::value::Table::new())
};
// 定位/创建 [workers] 表
if data
.get("workers")
.and_then(toml::Value::as_table)
.is_none()
{
let t = data.as_table_mut().unwrap();
t.insert(
"workers".into(),
toml::Value::Table(toml::value::Table::new()),
);
}
let workers_tbl = data.get_mut("workers").unwrap().as_table_mut().unwrap();
workers_tbl.insert("config".into(), json_to_toml(&current));
let out = toml::to_string(&data).map_err(|e| format!("序列化 config.toml 失败: {e}"))?;
// 原子写:临时文件 + rename
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, out).map_err(|e| format!("写入配置失败: {e}"))?;
std::fs::rename(&tmp, &path).map_err(|e| format!("原子替换配置失败: {e}"))?;
Ok(current)
}
/// json::Value → toml::Value
pub fn json_to_toml(v: &Value) -> toml::Value {
match v {
Value::Null => toml::Value::String(String::new()),
Value::Bool(b) => toml::Value::Boolean(*b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
toml::Value::Integer(i)
} else if let Some(f) = n.as_f64() {
toml::Value::Float(f)
} else {
toml::Value::String(n.to_string())
}
}
Value::String(s) => toml::Value::String(s.clone()),
Value::Array(a) => toml::Value::Array(a.iter().map(json_to_toml).collect()),
Value::Object(m) => toml::Value::Table(
m.iter()
.map(|(k, v)| (k.clone(), json_to_toml(v)))
.collect(),
),
}
}
/// 基于 current 合并 updates(过滤保留键)
pub fn merge_confirmed(current: Value, updates: &Value) -> Value {
let mut obj = current.as_object().cloned().unwrap_or_default();
for (k, v) in updates.as_object().unwrap_or(&Default::default()) {
if !CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()) {
obj.insert(k.clone(), v.clone());
}
}
Value::Object(obj)
}
/// 计算 updates 中被允许(非保留键)的键个数
pub fn count_allowed(updates: &Value) -> usize {
updates
.as_object()
.map(|m| {
m.keys()
.filter(|k| !CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()))
.count()
})
.unwrap_or(0)
}