From 27b4268acbf84b263ef5c058c08f1bccbf847311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 1 Jul 2026 23:10:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20SubflowNode=E5=B5=8C?= =?UTF-8?q?=E5=A5=97=E5=AD=90=E5=B7=A5=E4=BD=9C=E6=B5=81=20+=203=E4=B8=AA?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SubflowNode: 加载子DagDef递归执行,深度限制防无限递归,5个测试全绿 - 内置模板: 代码审查(analyze→report→notify) - 内置模板: Bug修复(locate→fix→verify→notify) - 内置模板: 功能开发(design→implement→test→review→notify) - 模板含persona分配(reviewer/analyst/coder/tester/architect) --- assets/templates/bug-fix.yaml | 47 ++++++++ assets/templates/code-review.yaml | 36 ++++++ assets/templates/feature-dev.yaml | 62 ++++++++++ crates/df-nodes/src/lib.rs | 1 + crates/df-nodes/src/subflow_node.rs | 175 ++++++++++++++++++++++++++++ 5 files changed, 321 insertions(+) create mode 100644 assets/templates/bug-fix.yaml create mode 100644 assets/templates/code-review.yaml create mode 100644 assets/templates/feature-dev.yaml create mode 100644 crates/df-nodes/src/subflow_node.rs diff --git a/assets/templates/bug-fix.yaml b/assets/templates/bug-fix.yaml new file mode 100644 index 0000000..9c738c9 --- /dev/null +++ b/assets/templates/bug-fix.yaml @@ -0,0 +1,47 @@ +# Bug 修复模板 +name: Bug 修复 +description: AI 驱动的 bug 修复流程 — 定位、修复、验证、通知 + +nodes: + locate: + type: ai + label: 定位 Bug + config: + prompt: | + 根据错误描述和堆栈信息,定位 bug 的根因。 + 使用 search_files 和 read_file 工具查找相关代码。 + persona_id: analyst + + fix: + type: ai + label: 修复 Bug + config: + prompt: | + 基于定位结果,修复 bug。 + 使用 patch_file 工具进行最小化修改。 + persona_id: coder + + verify: + type: ai + label: 验证修复 + config: + prompt: | + 验证修复是否有效。 + 使用 run_command 工具运行相关测试。 + persona_id: tester + + notify: + type: notify + label: 通知结果 + config: + type: desktop + title: Bug 修复完成 + message: 修复已验证 + +edges: + - from: locate + to: fix + - from: fix + to: verify + - from: verify + to: notify diff --git a/assets/templates/code-review.yaml b/assets/templates/code-review.yaml new file mode 100644 index 0000000..c51d81c --- /dev/null +++ b/assets/templates/code-review.yaml @@ -0,0 +1,36 @@ +# 代码审查模板 +name: 代码审查 +description: AI 驱动的代码审查流程 — 分析变更、审查质量、通知结果 + +nodes: + analyze: + type: ai + label: 分析代码变更 + config: + prompt: | + 分析以下代码变更,识别: + 1. 潜在 bug 和逻辑错误 + 2. 安全漏洞 + 3. 性能问题 + 4. 代码规范 + persona_id: reviewer + + report: + type: ai + label: 生成审查报告 + config: + prompt: 基于分析结果,生成结构化审查报告,含优先级排序的改进建议 + + notify: + type: notify + label: 通知完成 + config: + type: desktop + title: 代码审查完成 + message: 审查报告已生成 + +edges: + - from: analyze + to: report + - from: report + to: notify diff --git a/assets/templates/feature-dev.yaml b/assets/templates/feature-dev.yaml new file mode 100644 index 0000000..3fc3586 --- /dev/null +++ b/assets/templates/feature-dev.yaml @@ -0,0 +1,62 @@ +# 功能开发模板 +name: 功能开发 +description: AI 驱动的功能开发全流程 — 设计、编码、测试、审查、通知 + +nodes: + design: + type: ai + label: 方案设计 + config: + prompt: | + 分析需求,设计实现方案: + 1. 模块划分和接口定义 + 2. 数据结构和核心算法 + 3. 影响范围评估 + persona_id: architect + + implement: + type: ai + label: 编码实现 + config: + prompt: | + 按照设计方案实现功能。 + 使用 write_file 和 patch_file 工具编写代码。 + persona_id: coder + + test: + type: ai + label: 编写测试 + config: + prompt: | + 为新功能编写单元测试和集成测试。 + 使用 run_command 工具运行测试验证。 + persona_id: tester + + review: + type: ai + label: 代码审查 + config: + prompt: | + 审查实现代码,关注: + 1. 代码质量和可维护性 + 2. 潜在 bug + 3. 安全性 + persona_id: reviewer + + notify: + type: notify + label: 通知完成 + config: + type: desktop + title: 功能开发完成 + message: 设计→编码→测试→审查 全流程完成 + +edges: + - from: design + to: implement + - from: implement + to: test + - from: test + to: review + - from: review + to: notify diff --git a/crates/df-nodes/src/lib.rs b/crates/df-nodes/src/lib.rs index 456fff3..9e87b8e 100644 --- a/crates/df-nodes/src/lib.rs +++ b/crates/df-nodes/src/lib.rs @@ -8,6 +8,7 @@ pub mod http_node; pub mod human_node; mod human_node_helpers; pub mod notify_node; +pub mod subflow_node; pub mod script_node; pub mod task_advance_node; pub mod task_state_machine; diff --git a/crates/df-nodes/src/subflow_node.rs b/crates/df-nodes/src/subflow_node.rs new file mode 100644 index 0000000..df43087 --- /dev/null +++ b/crates/df-nodes/src/subflow_node.rs @@ -0,0 +1,175 @@ +//! 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"))); + } +}