新增: 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

@@ -8,8 +8,11 @@ use tauri::{AppHandle, Emitter, State};
use tokio::sync::broadcast::error::RecvError;
use df_core::events::{WorkflowEvent, HumanApprovalResponse, SelectType};
use df_core::types::new_id;
use df_storage::crud::WorkflowRepo;
use df_core::types::{new_id, NodeStatus};
use df_nodes::task_advance_node::advance_task_atomic;
// F-260616-06 ①-1: 空 dag + target_status 时按目标态自动选推进链模板
use df_nodes::task_workflow_templates::template_for;
use df_storage::crud::{TaskRepo, WorkflowRepo};
use df_storage::models::WorkflowRecord;
use df_workflow::dag_def::DagDef;
use df_workflow::executor::DagExecutor;
@@ -27,11 +30,36 @@ struct WorkflowEventPayload {
event: WorkflowEvent,
}
/// F-260616-06 ②-4: 工作流失败时按 target_status 推算任务退回态。
///
/// 映射表(失败退一步):
/// - testing → in_review
/// - in_review → in_progress
/// - in_progress → todo
/// - 其他(done/blocked/cancelled/todo 等) → None(无退回映射,跳过回调)
///
/// 设计选择:in_progress → todo(而非 None)。理由:in_progress 是执行态,失败退回 todo
/// 符合"执行未达预期 → 回起点重排"的直觉语义;若选 None 会丢失"执行失败"信号。
/// 起点状态 todo 无可退态 → None。
fn regression_target(target: &str) -> Option<&str> {
match target {
"testing" => Some("in_review"),
"in_review" => Some("in_progress"),
"in_progress" => Some("todo"),
_ => None,
}
}
/// 触发工作流执行(核心命令)
///
/// 流程build_dag 校验 → 写入执行记录(status=running) → 后台异步执行 →
/// 事件经 EventBus 转发到前端 → 完成后更新执行记录状态。
/// 立即返回执行记录 ID前端凭此关联后续事件。
///
/// F-260616-06 阶段2 工作流联动(②-2):
/// - `task_id` / `target_status` 同时 Some 时,完成后按 target_status 推进任务(②-3),
/// 失败时按 regression_target 退回(②-4)。
/// - Option 向后兼容:旧调用方不传即 None,不触发任何任务回调,零行为破坏。
#[tauri::command]
pub async fn run_workflow(
app: AppHandle,
@@ -39,7 +67,27 @@ pub async fn run_workflow(
name: String,
dag: DagDef,
config: serde_json::Value,
// F-260616-06 ②-2: 工作流联动任务的关联 ID 与目标态(同时 Some 才联动)
task_id: Option<String>,
target_status: Option<String>,
) -> Result<String, String> {
// 0. F-260616-06 ①-1: DagDef 来源选模板(方案A·最小)。
// 规则:
// - dag.nodes 非空 → 用传入 dag(向后兼容,旧调用方/编辑器自定义工作流不变)
// - dag.nodes 为空 + target_status Some → 调 template_for 自动选推进链模板;
// 模板不存在(target 非三合法态) → Err「无对应工作流模板」
// - dag.nodes 为空 + target_status None → 原行为(空 dag 让 build_dag 自行报错,零新分支)
// 前端选 target_status 后传空 dag,零模板知识零新 IPC,后端收敛选模板逻辑。
let dag = if dag.nodes.is_empty() {
match target_status.as_deref() {
Some(target) => template_for(target)
.ok_or_else(|| format!("无对应工作流模板: target_status={}", target))?,
None => dag, // 空dag+无target: 走原路径由 build_dag 校验
}
} else {
dag
};
// 1. 先构建运行时 DAG校验失败直接返回不落库
let runtime_dag = state.registry.build_dag(&dag).map_err(err_str)?;
@@ -53,7 +101,7 @@ pub async fn run_workflow(
status: "running".to_string(),
triggered_by: Some("manual".to_string()),
project_id: None,
task_id: None,
task_id: task_id.clone(),
created_at: now_millis(),
completed_at: None,
};
@@ -180,6 +228,9 @@ pub async fn run_workflow(
let db = state.db.clone();
let exec_id = execution_id.clone();
let state_registry = state.workflow_state_registry.clone();
// F-260616-06 ②-2: move task_id / target_status 进闭包供完成/失败回调使用
let cb_task_id = task_id.clone();
let cb_target_status = target_status.clone();
tauri::async_runtime::spawn(async move {
let mut executor = DagExecutor::new(event_bus.clone(), exec_id.clone());
// 注册执行器状态机StateMachine 内部 Arc<Mutex>clone 共享底层 HashMap
@@ -207,13 +258,58 @@ pub async fn run_workflow(
tracing::error!("更新工作流完成时间失败: {}", e);
}
// F-260616-06 ②-3 / ②-4: 工作流联动任务推进回调
// - 成功(completed): task_id 与 target_status 都 Some 时推进到 target_status
// - 失败(failed): 按 regression_target 推算退回态推进;无映射则跳过
// task_id / target_status 任一 None → 跳过(向后兼容,旧工作流不联动任务)
// 回调失败仅 tracing::warn!,不回滚(工作流成功语义与任务推进解耦 — 回调失败不撤销
// 已完成工作流,前端可后续手动处理)
match (cb_task_id.as_ref(), cb_target_status.as_deref()) {
(Some(tid), Some(target)) => {
let advance_target = match status {
"completed" => Some(target),
"failed" => regression_target(target),
_ => None,
};
if let Some(to) = advance_target {
let repo = TaskRepo::new(&db);
if let Err(e) = advance_task_atomic(&repo, tid, to).await {
tracing::warn!(
execution_id = %exec_id,
task_id = %tid,
target_status = %to,
workflow_status = %status,
"工作流联动推进任务失败(不回滚): {}",
e
);
} else {
tracing::info!(
execution_id = %exec_id,
task_id = %tid,
target_status = %to,
workflow_status = %status,
"工作流联动推进任务成功"
);
}
} else {
// failed 且无退回映射:warn 跳过(常见:target 是 todo 起点无可退态)
tracing::warn!(
execution_id = %exec_id,
task_id = %tid,
target_status = %target,
"工作流失败但 target_status 无退回映射,跳过任务回调"
);
}
}
_ => {}
}
// 执行结束(成功/失败)清理状态注册表,防内存泄漏
state_registry.lock().await.remove(&exec_id);
// 执行失败时补发 WorkflowFailed执行器内部只发 NodeFailed
// failed_node 从状态机取首个失败/取消节点(R9⑪: 原 String::new() 空值)
if let Some(error) = error {
use df_core::types::NodeStatus;
let failed_node = executor
.state_machine()
.snapshot()
@@ -337,6 +433,11 @@ pub async fn approve_human_approval(
/// 从执行器状态机注册表取出共享引用,调 `set_cancelled` 置目标节点为 Cancelled。
/// StateMachine 内部 Arc<Mutex> 共享底层 HashMapIPC 写入直达运行中阻塞节点HumanNode
/// 的 select! 轮询分支,其 is_cancelled 检测到后返回 Err "人工审批被取消"。
///
/// 终态前置守卫:仅 Pending排队中/ Running执行中含阻塞等审批允许取消
/// 终态节点Completed/Failed/Skipped/Cancelled返 Err避免静默覆盖终态。
/// 守卫放 IPC 层(非 set_cancelled 内):保留 set_cancelled 作为 executor 内部受控旁路语义,
/// 让状态变更的合法入口收敛到 IPC 这一道。
#[tauri::command]
pub async fn cancel_workflow_node(
state: State<'_, AppState>,
@@ -349,11 +450,25 @@ pub async fn cancel_workflow_node(
Some(sm) => sm,
None => return Err(format!("工作流 {} 不存在或已结束", execution_id)),
};
sm.set_cancelled(node_id.clone());
tracing::info!(
"工作流节点取消execution_id={}, node_id={}",
execution_id,
node_id
);
Ok(())
// 终态守卫get() 返回缺失条目默认 Pending尚未执行→ 允许取消
let current = sm.get(&node_id);
match current {
NodeStatus::Pending | NodeStatus::Running | NodeStatus::Waiting => {
sm.set_cancelled(node_id.clone());
tracing::info!(
"工作流节点取消execution_id={}, node_id={}",
execution_id,
node_id
);
Ok(())
}
// 终态:不允许取消(避免静默覆盖终态)
NodeStatus::Completed | NodeStatus::Failed | NodeStatus::Skipped | NodeStatus::Cancelled => {
Err(format!(
"节点 {} 已终态({}),无法取消",
node_id,
current.as_str()
))
}
}
}