Files
DevFlow/src-tauri/src/commands/task.rs
绝尘 4a87c552cc 新增: F-260619-01 任务可关联灵感(tasks.idea_id 1对1 单向)
- df-storage V20 迁移:tasks 加 idea_id TEXT REFERENCES ideas(id)(填预留空位,
  幂等 column_exists 探测)+ V9_SQL 同步 + TaskRecord.idea_id + task_repo SQL/SELECT + 白名单
- AI 工具 create_task 加 idea_id 参数;update_task 经白名单自动支持
- IPC commands/task.rs CreateTaskInput 加 idea_id
- 前端 types TaskRecord/CreateTaskInput idea_id + TaskDetail.vue 关联灵感展示
  (router-link 友好 title 非裸 id)+ i18n
- 补 idea_id 字段:df-mcp tools / df-nodes 测试 helper
- 单向 1对1,Option 可空,复用 projects.idea_id 模式,老任务 None 兼容

自验: df-storage 47 passed(含 V20 2)+ workspace EXIT 0 + vue-tsc EXIT 0
2026-06-19 21:03:18 +08:00

180 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 任务相关命令
use serde::Deserialize;
use tauri::State;
use df_types::types::{new_id, TaskStatus};
use df_storage::models::TaskRecord;
use crate::state::AppState;
use super::{err_str, now_millis};
/// 创建任务入参
#[derive(Debug, Deserialize)]
pub struct CreateTaskInput {
pub project_id: String,
pub title: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_priority")]
pub priority: i32,
pub branch_name: Option<String>,
pub assignee: Option<String>,
/// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。
/// 空字符串视为不关联(与 AI 工具层一致)。
pub idea_id: Option<String>,
}
fn default_priority() -> i32 {
2 // medium — 新任务默认中优先级(非 high),符合常识
}
/// 列出未删除任务(deleted_at IS NULL),可按 project_id 过滤。
///
/// 软删对标 projects:默认过滤回收站(语义与 list_projects 一致)。
/// 无 project_id 时调 list_active(WHERE deleted_at IS NULL);有 project_id 时
/// 先 list_active 再内存过滤 project_id(任务量小,无需 SQL 下推,避免新增专用查询方法)。
/// 注意:不能用通用 query 宏——它不带 deleted_at 过滤,会把回收站任务也返回。
#[tauri::command]
pub async fn list_tasks(
state: State<'_, AppState>,
project_id: Option<String>,
) -> Result<Vec<TaskRecord>, String> {
let mut tasks = state.tasks.list_active().await.map_err(err_str)?;
if let Some(pid) = project_id {
tasks.retain(|t| t.project_id == pid);
}
Ok(tasks)
}
/// 按 id 查任务,找不到返回 Err(供前端详情页)
#[tauri::command]
pub async fn get_task_by_id(
state: State<'_, AppState>,
id: String,
) -> Result<TaskRecord, String> {
state
.tasks
.get_by_id(&id)
.await
.map_err(err_str)?
.ok_or_else(|| format!("任务 {} 不存在", id))
}
/// 创建任务,返回完整记录
#[tauri::command]
pub async fn create_task(
state: State<'_, AppState>,
input: CreateTaskInput,
) -> Result<TaskRecord, String> {
let now = now_millis();
let record = TaskRecord {
id: new_id(),
project_id: input.project_id,
title: input.title,
description: input.description,
status: "todo".to_string(),
priority: input.priority,
branch_name: input.branch_name,
assignee: input.assignee,
workflow_def_id: None,
base_branch: None,
review_rounds: 0,
output_json: None,
// F-260619-01:空字符串视为不关联(与 AI 工具层一致)
idea_id: input.idea_id.filter(|s| !s.is_empty()),
created_at: now.clone(),
updated_at: now,
};
state
.tasks
.insert(record.clone())
.await
.map_err(err_str)?;
Ok(record)
}
/// 更新任务单个字段(字段名走 df-storage 白名单校验status 值走枚举校验)
#[tauri::command]
pub async fn update_task(
state: State<'_, AppState>,
id: String,
field: String,
value: String,
) -> Result<bool, String> {
// status 值校验保留:仅对「非法值」(拼写错 in-progess / "in progress" / 大小写错 / 越界)
// 给出友好早错误(先于 crud.rs 白名单那串冷冰冰的「字段不在白名单」拒)。合法 status 值
// 不可经本 IPC 写入——crud.rs tasks 白名单已收口移除 status(F-03 batch64 b94e74a /
// D-260616-04),所有 status 改动须走 advance_task_atomic 状态机(CAS + can_transition +
// review_rounds 累加,唯一 status 写入路径)。即此 is_valid 校验的「通过」分支永不会触达
// update_field 的 status 写入(白名单会先拒);它只为非法值兜底 UX不承担合法值写入职责。
// priority 值校验同理补在下方:拦截 "abc" / 999 等脏数据静默落库。
if field == "status" && !TaskStatus::is_valid(&value) {
return Err(format!(
"非法 status 值 {:?},合法值: {:?}",
value,
TaskStatus::valid_values()
));
}
// priority 值域 0..=30=critical, 1=high, 2=medium, 3=low与前端 <select> 及
// constants/project.ts 的 PRIORITY_LABELS 一致。拦截 "abc" / 999 等脏数据。
if field == "priority" {
match value.parse::<i32>() {
Ok(p) if (0..=3).contains(&p) => {}
_ => {
return Err(format!(
"非法 priority 值 {:?},合法值: 整数 0..=30=critical, 1=high, 2=medium, 3=low",
value
));
}
}
}
// project_id 跨表存在性校验(B-260616-16 收尾)。
// crud.rs tasks 白名单保留 project_id(支持跨项目移动),跨表约束由本命令层兜底。
// 拦截移动到不存在的项目(手输脏 id / 已物理删除的项目),避免 tasks.project_id 悬空。
if field == "project_id" {
let exists = state.projects.get_by_id(&value).await.map_err(err_str)?;
if exists.is_none() {
return Err(format!("非法 project_id 值 {:?},目标项目不存在", value));
}
}
state
.tasks
.update_field(&id, &field, &value)
.await
.map_err(err_str)
}
/// 删除任务(软删 → 回收站,可恢复)。对标 delete_project(SET deleted_at=now)。
#[tauri::command]
pub async fn delete_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
state.tasks.soft_delete(&id).await.map_err(err_str)
}
/// 恢复任务(从回收站还原,清 deleted_at)。对标 restore_project。
#[tauri::command]
pub async fn restore_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
state.tasks.restore(&id).await.map_err(err_str)
}
/// 推进任务状态(任务推进链 F-260616-02,推进链唯一 status 写入路径)。
///
/// thin 入口(D-260616-03):业务逻辑(状态机校验 + 原子 CAS + review_rounds 累加)
/// 落 df-nodes::task_advance_node::advance_task_atomic,本命令只做参数转发与错误串化。
///
/// 流程:读当前态 → can_transition 校验 → 下沉 SQL `WHERE id AND status=expected`
/// 防 TOCTOU → 退回转换一并 review_rounds+=1。失败均返回 Err(状态机/TOCTOU/任务不存在)。
///
/// 返回:推进成功后的最新 TaskRecord(含新 status / 累加后的 review_rounds)。
#[tauri::command]
pub async fn advance_task(
state: State<'_, AppState>,
id: String,
target_status: String,
) -> Result<TaskRecord, String> {
df_nodes::task_advance_node::advance_task_atomic(&state.tasks, &id, &target_status)
.await
.map_err(err_str)
}