新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地
This commit is contained in:
211
crates/df-nodes/src/task_workflow_templates.rs
Normal file
211
crates/df-nodes/src/task_workflow_templates.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
//! 任务推进链 DAG 模板(F-260616-06 阶段2 / D-260616-03)
|
||||
//!
|
||||
//! 推进链前向转换的工作流拓扑描述(声明式,纯数据)。DagDef 只描述节点与边,
|
||||
//! 执行逻辑靠 DagExecutor 驱动各 Node trait 的 execute —— 模板本身不跑逻辑。
|
||||
//!
|
||||
//! 三模板对应推进链三个前向转换点:
|
||||
//! - "in_progress": todo→in_progress,单 AiNode 执行任务
|
||||
//! - "testing": in_review→testing,AiNode 自审 → HumanNode 核对(human reject 走 Err
|
||||
//! 触发工作流 failed → ②-4 回调退回 in_review)
|
||||
//! - "done": testing→done,单 HumanNode 最终核对
|
||||
//!
|
||||
//! 不建 workflow_defs 表(模板少且稳定,硬编码;tasks.workflow_def_id 留 None)。
|
||||
//!
|
||||
//! config 设计:节点级 config 不硬编码 task_id / provider 等运行时参数 —— 全部留空。
|
||||
//! 运行时 run_workflow(task_id, target_status) 调用方注入全局 config(task_id /
|
||||
//! target_status / provider 配置),由 ④-1 deep_merge 与节点级 config 合并
|
||||
//! (节点级覆盖全局)。模板只描述拓扑,不绑定具体任务。
|
||||
|
||||
use df_workflow::dag_def::DagDef;
|
||||
|
||||
/// 按目标状态返回推进链工作流模板。
|
||||
///
|
||||
/// `target_status` 取推进链前向目标状态(in_progress / testing / done)。
|
||||
/// 未匹配返回 None —— 调用方降级(如直接 IPC 推进,不启工作流),不 panic。
|
||||
///
|
||||
/// 返回的 DagDef 节点级 config 为空对象或仅含拓扑级元信息(label 等),
|
||||
/// 运行时参数由调用方全局 config 注入 + deep_merge。
|
||||
pub fn template_for(target_status: &str) -> Option<DagDef> {
|
||||
match target_status {
|
||||
"in_progress" => Some(in_progress_template()),
|
||||
"testing" => Some(testing_template()),
|
||||
"done" => Some(done_template()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// todo → in_progress:单 AiNode 执行任务。
|
||||
///
|
||||
/// 拓扑:1 个 ai 节点。完成回调(②-3)推进 status 到 in_progress。
|
||||
fn in_progress_template() -> DagDef {
|
||||
let mut dag = DagDef::new();
|
||||
// config 留空:prompt / provider / task_id 均由 run_workflow 全局 config 注入
|
||||
// (④-1 deep_merge 合并),模板只描述拓扑。
|
||||
dag.add_node(
|
||||
"ai_execute",
|
||||
"ai",
|
||||
serde_json::json!({}),
|
||||
);
|
||||
dag
|
||||
}
|
||||
|
||||
/// in_review → testing:AiNode 自审 → HumanNode 核对。
|
||||
///
|
||||
/// 拓扑:ai → human 串行。HumanNode reject 走 Err(②-5 已做)→ 工作流 failed →
|
||||
/// ②-4 失败回调退回 in_review(review_rounds+=1)。
|
||||
/// 通过则完成回调(②-3)推进 status 到 testing。
|
||||
fn testing_template() -> DagDef {
|
||||
let mut dag = DagDef::new();
|
||||
dag.add_node("ai_self_review", "ai", serde_json::json!({}));
|
||||
dag.add_node(
|
||||
"human_review",
|
||||
"human",
|
||||
serde_json::json!({
|
||||
// 拓扑级元信息(非运行时参数):审批卡片默认文案。options 含「拒绝」
|
||||
// 触发 ②-5 reject → Err → 工作流 failed。
|
||||
"title": "核对 AI 自审结果",
|
||||
"options": ["同意", "拒绝"],
|
||||
}),
|
||||
);
|
||||
dag.add_edge("ai_self_review", "human_review");
|
||||
dag
|
||||
}
|
||||
|
||||
/// testing → done:单 HumanNode 最终核对。
|
||||
///
|
||||
/// 拓扑:1 个 human 节点。通过则完成回调(②-3)推进 status 到 done。
|
||||
/// reject 同理走 Err → failed → ②-4 退回。
|
||||
fn done_template() -> DagDef {
|
||||
let mut dag = DagDef::new();
|
||||
dag.add_node(
|
||||
"human_final_review",
|
||||
"human",
|
||||
serde_json::json!({
|
||||
"title": "最终核对",
|
||||
"options": ["同意", "拒绝"],
|
||||
}),
|
||||
);
|
||||
dag
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 单元测试 — 模板拓扑断言
|
||||
// ============================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unknown_status_returns_none() {
|
||||
assert!(template_for("unknown").is_none());
|
||||
assert!(template_for("").is_none());
|
||||
assert!(template_for("merged").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_progress_template_single_ai_node() {
|
||||
let dag = template_for("in_progress").expect("in_progress 模板存在");
|
||||
assert_eq!(dag.nodes.len(), 1, "in_progress: 单节点");
|
||||
assert!(dag.edges.is_empty(), "in_progress: 无边");
|
||||
let node = dag.nodes.get("ai_execute").expect("ai_execute 节点存在");
|
||||
assert_eq!(node.node_type, "ai");
|
||||
// config 留空(运行时注入)
|
||||
assert!(
|
||||
node.config.as_object().map(|o| o.is_empty()).unwrap_or(true),
|
||||
"ai_execute config 应为空对象"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn testing_template_ai_to_human_serial() {
|
||||
let dag = template_for("testing").expect("testing 模板存在");
|
||||
assert_eq!(dag.nodes.len(), 2, "testing: ai + human 两节点");
|
||||
assert_eq!(dag.edges.len(), 1, "testing: 单串行边");
|
||||
// 节点类型
|
||||
let ai = dag.nodes.get("ai_self_review").expect("ai_self_review 存在");
|
||||
assert_eq!(ai.node_type, "ai");
|
||||
let human = dag.nodes.get("human_review").expect("human_review 存在");
|
||||
assert_eq!(human.node_type, "human");
|
||||
// 边方向:ai → human
|
||||
let edge = &dag.edges[0];
|
||||
assert_eq!(edge.source, "ai_self_review");
|
||||
assert_eq!(edge.target, "human_review");
|
||||
// human options 含「拒绝」(触发 ②-5 reject)
|
||||
let options = human
|
||||
.config
|
||||
.get("options")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("human_review options 存在");
|
||||
let opts: Vec<&str> = options.iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(opts.contains(&"拒绝"), "options 应含「拒绝」触发 reject");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn done_template_single_human_node() {
|
||||
let dag = template_for("done").expect("done 模板存在");
|
||||
assert_eq!(dag.nodes.len(), 1, "done: 单节点");
|
||||
assert!(dag.edges.is_empty(), "done: 无边");
|
||||
let node = dag.nodes.get("human_final_review").expect("human_final_review 存在");
|
||||
assert_eq!(node.node_type, "human");
|
||||
}
|
||||
|
||||
// ---------- ②-3 / ②-4 回调语义对齐(CR-06 观察项 ② 测试补强) ----------
|
||||
//
|
||||
// 工作流完成(②-3)/失败(②-4)回调在 src-tauri/src/commands/workflow.rs spawn 闭包里,
|
||||
// 回调的核心计算是:template_for 选模板 + regression_target(target) 推退回态 +
|
||||
// advance_task_atomic 落库。这里锁定 template_for 三模板对回调语义的约束,
|
||||
// 保证回调侧(workflow.rs)的 target_status 取值与模板存在性一致 ——
|
||||
// 即回调只会对 template_for 返回 Some 的三个 target 触发,其余 target 模板不存在、
|
||||
// 工作流压根不会启动,回调自然不触发。
|
||||
|
||||
/// 三模板对应的 target_status 集合(workflow.rs ②-3/②-4 回调仅对这些 target 触发)
|
||||
const CALLBACK_TARGETS: &[&str] = &["in_progress", "testing", "done"];
|
||||
|
||||
#[test]
|
||||
fn callback_targets_all_have_templates() {
|
||||
// ②-3/②-4 回调依赖 template_for(target) 非空 —— 凡是能进回调的 target 必有模板。
|
||||
// 若新增一个回调 target 但忘了加模板,这里会先红。
|
||||
for target in CALLBACK_TARGETS {
|
||||
assert!(
|
||||
template_for(target).is_some(),
|
||||
"回调 target {target:?} 必须有对应模板(template_for 非 None)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_callback_targets_have_no_template() {
|
||||
// 反向:非推进链 target(todo/in_review/blocked/cancelled/未知) 无模板,
|
||||
// 工作流不启动,②-3/②-4 回调不触发 —— 锁定回调边界。
|
||||
for non_target in ["todo", "in_review", "blocked", "cancelled", "merged", ""] {
|
||||
assert!(
|
||||
template_for(non_target).is_none(),
|
||||
"非推进链 target {non_target:?} 不应有模板(否则回调边界被扩大)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn testing_and_done_templates_have_reject_path() {
|
||||
// ②-4 失败回调依赖模板内 human reject 走 Err → 工作流 failed → regression_target。
|
||||
// testing(done 同理)模板的 human 节点 options 必须含「拒绝」,否则失败回调不可达。
|
||||
// (in_progress 模板只有 ai 节点,失败是 ai 异常,无 reject 路径 —— 单独验证。)
|
||||
for target in ["testing", "done"] {
|
||||
let dag = template_for(target).unwrap_or_else(|| panic!("{target} 模板应存在"));
|
||||
let human = dag.nodes.values().find(|n| n.node_type == "human").unwrap_or_else(|| {
|
||||
panic!("{target} 模板应含 human 节点(承载 reject → failed → ②-4 回调)")
|
||||
});
|
||||
let options = human
|
||||
.config
|
||||
.get("options")
|
||||
.and_then(|v| v.as_array())
|
||||
.unwrap_or_else(|| panic!("{target} human 节点 config 应有 options 数组"));
|
||||
let opts: Vec<&str> = options.iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(
|
||||
opts.contains(&"拒绝"),
|
||||
"{target} human options 应含「拒绝」(否则 ②-4 失败回调路径不可达)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user