//! 任务相关命令 use serde::Deserialize; use tauri::State; use df_types::types::{new_id, TaskStatus}; use df_storage::crud::TaskQuery; 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, pub assignee: Option, /// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。 /// 空字符串视为不关联(与 AI 工具层一致)。 pub idea_id: Option, } fn default_priority() -> i32 { 2 // medium — 新任务默认中优先级(非 high),符合常识 } /// 列出未删除任务(deleted_at IS NULL)。 /// /// **向后兼容铁律**:`project_id` 与 `query` 两参都可选,旧调用方不传(或只传 project_id) /// 必须等价改造前的全量行为,零破坏。 /// /// F-260621-02 查询维度补全(机制优先 prompt 说教): /// - 优先走 `query`(`TaskQuery` 多维动态 WHERE):status(P1 下沉)/ keyword(P2 LIKE) /// /project_id/priority/assignee/order_by/limit/offset 任意组合。 /// - 旧调用方仍可直接传 `project_id`(单维度),此时走 list_active_by_project /// (SQL 下推,命中 idx_tasks_project_id),保持等价行为。 /// - query 与 project_id 同时传时:query 优先(其内含 project_id 维度,更全),project_id 忽略。 /// - 均不传时:全量未删任务(等价改造前 list_active 行为)。 /// /// status 值合法性兜底:TaskStatus::is_valid 拦截非法值(拼写错/越界),非法值返回 Err /// (与 update_task 的 status 校验一致),不静默返回空结果误导调用方。 #[tauri::command] pub async fn list_tasks( state: State<'_, AppState>, project_id: Option, query: Option, ) -> Result, String> { if let Some(q) = &query { // status 值兜底校验(非法值早 Err,不进 DB 层) if let Some(status) = &q.status { if !TaskStatus::is_valid(status) { return Err(format!( "非法 status 值 {:?},合法值: {:?}", status, TaskStatus::valid_values() )); } } return state.tasks.list_by_query(q).await.map_err(err_str); } // 旧调用方:单维度 project_id(SQL 下推,fallback 等价改造前行为) let tasks = match project_id { Some(pid) => state .tasks .list_active_by_project(&pid) .await .map_err(err_str)?, None => state.tasks.list_active().await.map_err(err_str)?, }; Ok(tasks) } /// 按 id 查任务,找不到返回 Err(供前端详情页) #[tauri::command] pub async fn get_task_by_id( state: State<'_, AppState>, id: String, ) -> Result { 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 { 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 { // 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..=3(0=critical, 1=high, 2=medium, 3=low),与前端