新增: 父子任务支持(数据→后端→前端全链路,会话前基线收尾)

- df-nodes task_advance_node(父聚合推进)+ task.rs 命令(create parent_id 支持/delete 级联软删子任务)+ task_graph 工具

- 前端 Tasks 树形列表(折叠箭头/子进度徽章/缩进)+ 新建弹窗父任务下拉 + TaskDetail 父面包屑/子任务面板

- 设计文档: 父子任务支持设计-2026-08-04
This commit is contained in:
lxy
2026-08-05 22:15:01 +08:00
parent 71fdaac1b4
commit 28de5d6143
13 changed files with 1097 additions and 307 deletions
+100
View File
@@ -89,6 +89,106 @@ pub async fn advance_task_atomic(
})
}
// ============================================================
// 父任务聚合 — 父任务 status 重算 + 推进联动(知识图谱 Phase 1 V29,设计 §2.1)
// ============================================================
/// 父任务 status 重算(容器模型,不走状态机)。
///
/// 聚合规则(设计 §2.1 父聚合规则,优先级从高到低):
/// 1. 任一子 blocked → 父 blocked(阻塞优先,避免掩盖卡点)
/// 2. 任一子 in_progress → 父 in_progress(执行中)
/// 3. 全子 done/cancelled → 父 done(全部完成/取消)
/// 4. 全子 todo → 父 todo(尚未开始)
/// 5. 其他混合态(如 todo+done)→ 父 in_progress(进行中,有进展未全完)
///
/// 无子任务(悬空)→ 不重算,返回当前 status。
/// 数据源 `repo.count_children_by_status`(一次 GROUP BY 查询,数据量小无压力);
/// 写入 `repo.set_status_for_aggregation`(父任务 status 唯一非状态机写入路径)。
/// 状态相同则不写(避免无谓 updated_at 抖动)。
///
/// 返回:重算后的父任务最新 status。
pub async fn recompute_parent_status(
repo: &TaskRepo,
parent_id: &str,
) -> df_types::error::Result<String> {
let counts = repo
.count_children_by_status(parent_id)
.await?;
// 无子任务(parent_id 悬空,理论上不该发生):不重算,返当前 status
if counts.is_empty() {
return repo
.get_by_id(parent_id)
.await?
.map(|t| t.status.as_str().to_string())
.ok_or_else(|| df_types::error::Error::NotFound(format!("父任务 {parent_id} 不存在")));
}
// 转 HashMap<status, count> 便于按规则判定
let map: std::collections::HashMap<String, i64> = counts.into_iter().collect();
let total: i64 = map.values().sum();
let blocked = map.get("blocked").copied().unwrap_or(0);
let in_progress = map.get("in_progress").copied().unwrap_or(0);
let todo = map.get("todo").copied().unwrap_or(0);
let done = map.get("done").copied().unwrap_or(0);
let cancelled = map.get("cancelled").copied().unwrap_or(0);
// 聚合规则判定(优先级从高到低,首个命中即定)
let new_status = if blocked > 0 {
"blocked".to_string()
} else if in_progress > 0 {
"in_progress".to_string()
} else if (done + cancelled) == total {
// 全 done/cancelled → done(终端态聚合为 done)
"done".to_string()
} else if todo == total {
// 全 todo → todo(尚未开始)
"todo".to_string()
} else {
// 其他混合态(如 todo+done, in_review+done 等)→ in_progress(进行中)
"in_progress".to_string()
};
// 读当前父 status,相同则不写(避免无谓 updated_at 抖动)
let current = repo
.get_by_id(parent_id)
.await?
.ok_or_else(|| df_types::error::Error::NotFound(format!("父任务 {parent_id} 不存在")))?;
if current.status.as_str() == new_status {
return Ok(new_status);
}
repo.set_status_for_aggregation(parent_id, &new_status).await?;
Ok(new_status)
}
/// 推进任务 + 若为子任务则触发父聚合(父聚合失败仅 warn 不阻断,宽容语义)。
///
/// 推进链唯一 status 写入路径的两段式(设计 D3 统一,IPC/AI 工具/MCP 同源):
/// 1. 推进自身:调 `advance_task_atomic`(状态机校验 + 原子 CAS + review_rounds 累加)。
/// 2. 父聚合:推进成功后若 `updated.parent_id` 有值,触发 `recompute_parent_status` 重算
/// 父 status(父任务=容器模型,聚合规则见 recompute_parent_status)。父聚合失败仅
/// tracing::warn 不阻断——子任务推进成功是主结果,父 status 漂移可后续修正。
///
/// 返回:推进成功后的最新 TaskRecord(含新 status / 累加后的 review_rounds)。
pub async fn advance_task_with_parent(
repo: &TaskRepo,
id: &str,
target_status: &str,
) -> df_types::error::Result<TaskRecord> {
let updated = advance_task_atomic(repo, id, target_status).await?;
if let Some(pid) = &updated.parent_id {
if let Err(e) = recompute_parent_status(repo, pid).await {
tracing::warn!(
task_id = %id,
parent_id = %pid,
error = %e,
"[父聚合] 重算父任务 status 失败(不阻断子任务推进)"
);
}
}
Ok(updated)
}
// ============================================================
// DAG 工作流节点 — TaskAdvanceNode(推进链在 DAG 内的形态)
// ============================================================