重构: 后端 df-ai/commands 拆分+df-nodes/workflow 改造+P0 bug 修复
- df-ai: context 历史中毒三档自愈 sanitize_messages(AC3)+anthropic_compat tool_use_id None 跳过(AC1/AC2)+删 router/stream 死码
- df-core: events 加 select_type+decisions 多选审批契约(F-260615-01)
- df-execute: shell run_command 工具复用(F-260615-05)
- df-nodes: human_node 多选校验+2 端到端测(F-01)+取消跳 set_failed(B-03b-R1/R2/R8)
- df-workflow: executor/dag/state cancel 闭环(B-06/07/03a/b)+provider approve options(R-PD-5)
- df-storage: find_path_conflict 抽公共(R-PD-11)+COLS 常量断言
- df-ideas: 删 IdeaPromoter/PromotionPolicy 死码(R-PD-14)
- src-tauri/commands/ai: secret keyring 迁移(FR-S1/R-PD-4)+GeneratingGuard RAII+disarm(B-09/26)+newConversation 软复位(B-10)+stream 心跳/stop select/空回复判错(B-02/04/05/15)+run_command(F-05)+mask audit(AR-3)
- src-tauri/commands/{project,task,workflow,mod,lib,state}: task detail IPC(F-02)+approve decisions+task list 联动(B-29)
- Cargo.lock+Cargo.toml 依赖同步
This commit is contained in:
@@ -61,24 +61,6 @@ impl Dag {
|
||||
});
|
||||
}
|
||||
|
||||
/// 获取指定节点的所有上游节点 ID
|
||||
pub fn predecessors(&self, node_id: &NodeId) -> Vec<NodeId> {
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|e| &e.target == node_id)
|
||||
.map(|e| e.source.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 获取指定节点的所有下游节点 ID
|
||||
pub fn successors(&self, node_id: &NodeId) -> Vec<NodeId> {
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|e| &e.source == node_id)
|
||||
.map(|e| e.target.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 拓扑排序 — 返回按执行顺序排列的节点 ID 层级
|
||||
///
|
||||
/// 返回 Vec<Vec<NodeId>>,每层内的节点可以并行执行。
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::dag::Dag;
|
||||
|
||||
/// DAG 定义 — 可序列化/反序列化,用于持久化和模板
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DagDef {
|
||||
@@ -73,28 +71,6 @@ impl DagDef {
|
||||
condition: Some(condition.into()),
|
||||
});
|
||||
}
|
||||
|
||||
/// 从已有的运行时 Dag 提取定义(忽略节点实例,仅保留元数据)
|
||||
///
|
||||
/// 注意:此方法只能提取边的定义,节点配置无法从 trait object 反推
|
||||
pub fn from_dag_edges(dag: &Dag) -> Self {
|
||||
let edges: Vec<EdgeDef> = dag.edges.iter().map(|e| EdgeDef {
|
||||
source: e.source.clone(),
|
||||
target: e.target.clone(),
|
||||
condition: e.condition.clone(),
|
||||
}).collect();
|
||||
|
||||
let nodes: HashMap<String, NodeDef> = dag.nodes.iter().map(|(id, node)| {
|
||||
(id.clone(), NodeDef {
|
||||
id: id.clone(),
|
||||
node_type: node.node_type().to_string(),
|
||||
config: serde_json::Value::Null,
|
||||
label: None,
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Self { nodes, edges }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DagDef {
|
||||
|
||||
@@ -123,7 +123,13 @@ impl DagExecutor {
|
||||
for (node_id, result, duration) in results {
|
||||
match result {
|
||||
Ok(output) => {
|
||||
self.state_machine.set_completed(node_id.clone())?;
|
||||
// 已取消的节点跳过 set_completed:
|
||||
// Ok 后 join_all 让出执行权,窗口内 cancel_workflow_node IPC → set_cancelled 改 Cancelled,
|
||||
// 随后此处 set_completed 走 transition 命中(Cancelled→Completed)非法 → bail 致已批准审批报失败(R-P1-3 TOCTOU)
|
||||
// 与 Err 分支 is_cancelled 短路对称:已取消节点保持 Cancelled 终态
|
||||
if !self.state_machine.is_cancelled(&node_id) {
|
||||
self.state_machine.set_completed(node_id.clone())?;
|
||||
}
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::NodeCompleted {
|
||||
node_id: node_id.clone(),
|
||||
@@ -314,4 +320,53 @@ mod tests {
|
||||
NodeStatus::Cancelled
|
||||
);
|
||||
}
|
||||
|
||||
/// 测试节点:执行中通过共享 node_status 自取消(模拟外部 IPC set_cancelled),返回 Ok
|
||||
/// 模拟 HumanNode select! 收合法 HumanApprovalResponse 后 return Ok,但 join_all 让出窗口内
|
||||
/// cancel_workflow_node IPC 把节点改 Cancelled 的 TOCTOU 场景。
|
||||
struct CancelSelfThenOkNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for CancelSelfThenOkNode {
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult {
|
||||
ctx.node_status.set_cancelled(ctx.node_id.clone());
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::Value::Null,
|
||||
output: serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"cancel_self_then_ok"
|
||||
}
|
||||
}
|
||||
|
||||
/// R-P1-3:Ok 后取消的 TOCTOU 防护。节点返回 Ok 但执行期间已被 set_cancelled,
|
||||
/// executor Ok 分支必须跳过 set_completed(transition Cancelled→Completed 非法会 bail),
|
||||
/// 状态保持 Cancelled,run 返回 Ok(已批准审批不应误报失败)。
|
||||
#[tokio::test]
|
||||
async fn test_cancelled_node_skips_set_completed() {
|
||||
use df_core::types::NodeStatus;
|
||||
let mut dag = Dag::new();
|
||||
dag.add_node("y".to_string(), Box::new(CancelSelfThenOkNode));
|
||||
|
||||
let mut executor = DagExecutor::new(EventBus::new(), "test-cancel-ok".to_string());
|
||||
let result = executor.run(&dag, serde_json::Value::Null).await;
|
||||
|
||||
// run 返回 Ok —— Ok 节点不应因 TOCTOU 取消被误判为失败
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Ok 后取消应短路 set_completed 而非 bail,实际 err: {:?}",
|
||||
result.err()
|
||||
);
|
||||
// 状态保持 Cancelled(未被 set_completed 覆盖为 Completed,也不 bail)—— R-P1-3 修法核心验证
|
||||
assert_eq!(
|
||||
executor.state_machine.get(&"y".to_string()),
|
||||
NodeStatus::Cancelled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,23 @@ impl NodeRegistry {
|
||||
pub fn build_dag(&self, def: &DagDef) -> anyhow::Result<Dag> {
|
||||
let mut dag = Dag::new();
|
||||
|
||||
// 创建所有节点实例
|
||||
// 创建所有节点实例,同时收集节点 ID 集合用于边校验
|
||||
let mut node_ids: std::collections::HashSet<&String> = std::collections::HashSet::new();
|
||||
for (id, node_def) in &def.nodes {
|
||||
let node = self.create(&node_def.node_type, &node_def.config)?;
|
||||
dag.add_node(id.clone(), node);
|
||||
node_ids.insert(id);
|
||||
}
|
||||
|
||||
// 添加所有边
|
||||
// 添加所有边 — 校验两端节点均存在,野边早返回
|
||||
for edge_def in &def.edges {
|
||||
if !node_ids.contains(&edge_def.source) || !node_ids.contains(&edge_def.target) {
|
||||
anyhow::bail!(
|
||||
"边 source/target 节点不存在: {} -> {}",
|
||||
edge_def.source,
|
||||
edge_def.target
|
||||
);
|
||||
}
|
||||
match &edge_def.condition {
|
||||
Some(cond) => dag.add_edge_with_condition(
|
||||
edge_def.source.clone(),
|
||||
@@ -62,16 +71,6 @@ impl NodeRegistry {
|
||||
|
||||
Ok(dag)
|
||||
}
|
||||
|
||||
/// 检查是否已注册指定类型
|
||||
pub fn is_registered(&self, type_name: &str) -> bool {
|
||||
self.factories.contains_key(type_name)
|
||||
}
|
||||
|
||||
/// 列出所有已注册的节点类型
|
||||
pub fn registered_types(&self) -> Vec<&str> {
|
||||
self.factories.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
// 不实现 Default:原 Default 注册了一个会 panic 的 script 工厂(unimplemented!),
|
||||
|
||||
@@ -83,26 +83,15 @@ impl StateMachine {
|
||||
self.transition(node_id, NodeStatus::Failed)
|
||||
}
|
||||
|
||||
/// 设置节点为等待中(暂未纳入转换校验)
|
||||
pub fn set_waiting(&self, node_id: NodeId) {
|
||||
self.states
|
||||
.lock()
|
||||
.expect("状态机锁中毒")
|
||||
.insert(node_id, NodeStatus::Waiting);
|
||||
}
|
||||
|
||||
/// 设置节点为已跳过(暂未纳入转换校验)
|
||||
pub fn set_skipped(&self, node_id: NodeId) {
|
||||
self.states
|
||||
.lock()
|
||||
.expect("状态机锁中毒")
|
||||
.insert(node_id, NodeStatus::Skipped);
|
||||
}
|
||||
|
||||
/// 设置节点为已取消(暂未纳入转换校验,同 set_waiting/set_skipped 模式)
|
||||
/// 设置节点为已取消 — 唯一受控旁路:不经 transition 合法性校验
|
||||
///
|
||||
/// 人工审批取消场景由前端 cancel_workflow_node IPC 触发;
|
||||
/// 阻塞节点(如 HumanNode)在 select! 轮询分支通过 is_cancelled 检测到后返回 Err。
|
||||
/// 设计理由:人工审批取消场景由前端 cancel_workflow_node IPC 异步触发,
|
||||
/// 此时节点可能处于 Running 之外的任意态(如 Pending 排队中、阻塞等待审批中),
|
||||
/// 若走 transition 校验会被 is_legal(Pending→Cancelled) 拒绝。
|
||||
/// 故 set_cancelled 作为唯一受控旁路直接置位 Cancelled,
|
||||
/// 阻塞节点(如 HumanNode)在 select! 轮询分支通过 is_cancelled 检测后返回 Err。
|
||||
///
|
||||
/// 注:原 set_waiting/set_skipped 同为旁路置位但全仓零调用,已删除。
|
||||
pub fn set_cancelled(&self, node_id: NodeId) {
|
||||
self.states
|
||||
.lock()
|
||||
@@ -177,7 +166,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_set_cancelled_marks_node_cancelled() {
|
||||
// set_cancelled 直接置位,不经转换校验(同 set_waiting/set_skipped 模式)
|
||||
// set_cancelled 为唯一受控旁路,不经转换校验直接置位 Cancelled
|
||||
let sm = StateMachine::new();
|
||||
let id = "human-1".to_string();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user