重构: 后端 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:
2026-06-15 05:14:42 +08:00
parent 04032a2a8d
commit 2de0c6ecb7
37 changed files with 2457 additions and 484 deletions

View File

@@ -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
);
}
}