//! SubflowNode — 嵌套子工作流节点 //! //! 加载一个子模板(DagDef)并在当前执行上下文中递归执行。 //! 用于复用通用流程(如"代码审查"可作为任意主流程的子步骤)。 use async_trait::async_trait; use df_workflow::dag_def::DagDef; use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; use serde::{Deserialize, Serialize}; /// SubflowNode 配置 #[derive(Debug, Clone, Serialize, Deserialize)] struct SubflowConfig { /// 子工作流 DAG 定义(内联 JSON) dag: serde_json::Value, /// 嵌套深度限制(防无限递归,默认 MAX_DEPTH) #[serde(default)] max_depth: Option, } /// 最大递归深度(安全阀) const MAX_DEPTH: u32 = 10; /// SubflowNode — 嵌套子工作流 pub struct SubflowNode; #[async_trait] impl Node for SubflowNode { async fn execute(&self, ctx: NodeContext) -> NodeResult { tracing::info!("SubflowNode 执行: {}", ctx.node_id); let config: SubflowConfig = serde_json::from_value(ctx.config.clone()) .map_err(|e| anyhow::anyhow!("SubflowNode 配置解析失败: {}", e))?; let max_depth = config.max_depth.unwrap_or(MAX_DEPTH).min(MAX_DEPTH); // 反序列化子 DAG let sub_dag: DagDef = serde_json::from_value(config.dag) .map_err(|e| anyhow::anyhow!("子工作流 DAG 解析失败: {}", e))?; if sub_dag.nodes.is_empty() { anyhow::bail!("子工作流节点不能为空"); } if max_depth == 0 { anyhow::bail!("SubflowNode 超过最大嵌套深度"); } tracing::info!( node_count = sub_dag.nodes.len(), edge_count = sub_dag.edges.len(), max_depth, "SubflowNode 加载子工作流" ); // 返回子 DAG 的 JSON 快照供 DagExecutor 消费 Ok(NodeOutput::from_value(serde_json::json!({ "subflow": true, "node_count": sub_dag.nodes.len(), "edge_count": sub_dag.edges.len(), "max_depth": max_depth, "dag": sub_dag, }))) } fn schema(&self) -> NodeSchema { NodeSchema { params: serde_json::json!({ "type": "object", "properties": { "dag": { "type": "object", "description": "子工作流 DAG 定义(nodes + edges)", "properties": { "nodes": { "type": "object" }, "edges": { "type": "array" } } }, "max_depth": { "type": "integer", "description": "最大嵌套深度(默认10)", "default": 10 } }, "required": ["dag"] }), output: serde_json::json!({}), } } fn node_type(&self) -> &'static str { "subflow" } } #[cfg(test)] mod tests { use super::*; use df_workflow::eventbus::EventBus; use df_workflow::state::StateMachine; fn make_ctx(config: serde_json::Value) -> NodeContext { NodeContext { node_id: "test_subflow".into(), inputs: Default::default(), config, execution_id: "exec_1".into(), event_bus: EventBus::new(), node_status: StateMachine::new(), } } #[tokio::test] async fn sub_01_valid_dag_returns_metadata() { let node = SubflowNode; let ctx = make_ctx(serde_json::json!({ "dag": { "nodes": { "child": { "id": "child", "node_type": "script", "config": {} } }, "edges": [] } })); let result = node.execute(ctx).await.unwrap(); assert_eq!(result.data["subflow"], true); assert_eq!(result.data["node_count"], 1); } #[tokio::test] async fn sub_02_empty_dag_rejected() { let node = SubflowNode; let ctx = make_ctx(serde_json::json!({ "dag": { "nodes": {}, "edges": [] } })); let result = node.execute(ctx).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("不能为空")); } #[tokio::test] async fn sub_03_max_depth_zero_guard() { let node = SubflowNode; let ctx = make_ctx(serde_json::json!({ "dag": { "nodes": { "c": { "id": "c", "node_type": "script", "config": {} } }, "edges": [] }, "max_depth": 0 })); let result = node.execute(ctx).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("最大嵌套深度")); } #[tokio::test] async fn sub_04_missing_dag_field_errors() { let node = SubflowNode; let ctx = make_ctx(serde_json::json!({})); let result = node.execute(ctx).await; assert!(result.is_err()); } #[tokio::test] async fn sub_05_schema_has_required_fields() { let node = SubflowNode; let schema = node.schema(); let params = &schema.params; assert!(params["properties"]["dag"].is_object()); assert!(params["required"].as_array().unwrap().contains(&serde_json::json!("dag"))); } }