新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地

This commit is contained in:
2026-06-16 12:41:13 +08:00
parent 212a927eee
commit 7d5cd4c89a
62 changed files with 4576 additions and 248 deletions

View File

@@ -22,6 +22,12 @@ pub struct Dag {
pub nodes: HashMap<NodeId, Box<dyn Node>>,
/// 边集合
pub edges: Vec<Edge>,
/// 节点级配置DagDef.nodes[id].config 提取自 build_dagrun 时与全局 config deep_merge
///
/// 语义节点级覆盖全局级。DagExecutor 构造 NodeContext 时执行
/// `deep_merge(initial_config, node_configs[id])`,节点定义里写的同名 key 优先于
/// run_workflow 传入的全局 config。
pub node_configs: HashMap<NodeId, serde_json::Value>,
}
impl Dag {
@@ -30,6 +36,7 @@ impl Dag {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
node_configs: HashMap::new(),
}
}
@@ -38,6 +45,19 @@ impl Dag {
self.nodes.insert(id, node);
}
/// 添加节点(附带节点级配置)
///
/// 节点级配置在 DagExecutor 构造 NodeContext 时覆盖全局 configdeep_merge
pub fn add_node_with_config(
&mut self,
id: NodeId,
node: Box<dyn Node>,
config: serde_json::Value,
) {
self.nodes.insert(id.clone(), node);
self.node_configs.insert(id, config);
}
/// 添加边
pub fn add_edge(&mut self, source: NodeId, target: NodeId) {
self.edges.push(Edge {
@@ -135,3 +155,85 @@ impl Default for Dag {
Self::new()
}
}
/// 深度合并两个 JSON 值:节点级 `node` 覆盖全局级 `global`。
///
/// 规则:
/// - 两端均为 Object递归合并同 key 时节点级覆盖全局级,新增 key 各自保留)。
/// - 非两端 Object节点级直接覆盖全局级包括 `Null`,符合 JSON 显式空语义)。
/// - 节点级为 `Null` 的语义是「显式置空」,按非 Object 规则覆盖(直接返回 Null
/// 若调用方希望「跳过节点配置」,应在构造 Dag 时不写入该 keynode_configs 缺失即用全局)。
///
/// 用途DagExecutor 构造 NodeContext 时 `deep_merge(initial_config, node_config)`
/// 让节点定义里写的 NodeDef.config 优先于 run_workflow 传入的全局 config。
pub fn deep_merge(global: &serde_json::Value, node: &serde_json::Value) -> serde_json::Value {
use serde_json::Value;
match (global, node) {
(Value::Object(g), Value::Object(n)) => {
let mut merged = g.clone();
for (k, nv) in n {
// 同 key 递归(支持嵌套对象局部覆盖);不同类型时递归退化为「节点级直接覆盖」
let gv = merged.get(k).cloned();
let merged_v = match gv {
Some(gv) => deep_merge(&gv, nv),
None => nv.clone(),
};
merged.insert(k.clone(), merged_v);
}
Value::Object(merged)
}
// 非两端 Object节点级覆盖全局级含 Null、标量、数组
_ => node.clone(),
}
}
#[cfg(test)]
mod deep_merge_tests {
use super::deep_merge;
use serde_json::json;
#[test]
fn node_overrides_global_scalar() {
assert_eq!(deep_merge(&json!({"a": 1}), &json!({"a": 2})), json!({"a": 2}));
}
#[test]
fn node_adds_new_key() {
assert_eq!(
deep_merge(&json!({"a": 1}), &json!({"b": 2})),
json!({"a": 1, "b": 2})
);
}
#[test]
fn nested_object_recursive_merge() {
assert_eq!(
deep_merge(&json!({"o": {"x": 1, "y": 2}}), &json!({"o": {"y": 9}})),
json!({"o": {"x": 1, "y": 9}})
);
}
#[test]
fn empty_node_config_is_noop() {
// 核心保现有调用方行为:节点 config 为空对象 → 结果等同全局(零行为破坏)
let global = json!({"title": "确认", "options": ["同意"]});
assert_eq!(deep_merge(&global, &json!({})), global);
}
#[test]
fn missing_node_config_treated_as_noop() {
// DagExecutor 对缺失 key 的节点不调 deep_merge直接用 global此处覆盖语义对照
// 若强行把缺失视作 Null应覆盖显式空语义与「缺失」不同缺失走 if let Some 分支)。
let global = json!({"a": 1});
assert_eq!(deep_merge(&global, &json!(null)), json!(null));
}
#[test]
fn array_replaces_not_concat() {
// 非两端 Object数组→ 节点级直接覆盖,不拼接
assert_eq!(
deep_merge(&json!({"arr": [1, 2]}), &json!({"arr": [3]})),
json!({"arr": [3]})
);
}
}