From 539b5ed63ea90841221114158c27fca944cacdbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 16 Jun 2026 22:29:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20TaskRecord=E5=8A=A0output?= =?UTF-8?q?=5Fjson=E4=BA=A7=E5=87=BA=E5=AD=97=E6=AE=B5+V17=E8=BF=81?= =?UTF-8?q?=E7=A7=BB(=E5=86=B3=E7=AD=96a=20AiNode=E8=87=AA=E5=AE=A1?= =?UTF-8?q?=E9=97=AD=E7=8E=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/df-nodes/src/task_advance_node.rs | 1 + crates/df-storage/src/crud.rs | 25 +++++++++++-------- crates/df-storage/src/migrations.rs | 19 +++++++++++++- crates/df-storage/src/models.rs | 8 ++++++ .../df-storage/tests/project_soft_delete.rs | 1 + src-tauri/src/commands/ai/tool_registry.rs | 1 + src-tauri/src/commands/task.rs | 1 + 7 files changed, 45 insertions(+), 11 deletions(-) diff --git a/crates/df-nodes/src/task_advance_node.rs b/crates/df-nodes/src/task_advance_node.rs index e6479ca..900a8d6 100644 --- a/crates/df-nodes/src/task_advance_node.rs +++ b/crates/df-nodes/src/task_advance_node.rs @@ -188,6 +188,7 @@ mod tests { workflow_def_id: None, base_branch: None, review_rounds: 0, + output_json: None, created_at: "0".to_string(), updated_at: "0".to_string(), } diff --git a/crates/df-storage/src/crud.rs b/crates/df-storage/src/crud.rs index be3b347..1c42b05 100644 --- a/crates/df-storage/src/crud.rs +++ b/crates/df-storage/src/crud.rs @@ -340,7 +340,11 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> { // update_task/update_field 白名单均不含)。后人勿把 review_rounds 补进白名单, // 否则破坏「review_rounds 唯一写入路径」收口、引入旁路写导致计数错乱。 "project_id", "title", "description", "priority", "branch_name", - "assignee", "workflow_def_id", "base_branch", "updated_at", + "assignee", "workflow_def_id", "base_branch", + // output_json:ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象 + // (决策 a:task 中心,产出跟 task 走)。非状态机收口字段,合法可写。 + "output_json", + "updated_at", // TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。 // 通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。 // project_id 当前可在白名单内改写,合法目标存在性由上层命令层校验。 @@ -485,6 +489,7 @@ fn task_from_row(row: &Row<'_>) -> std::result::Result |row| task_from_row(row), insert => |conn, rec| { conn.execute( - "INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", params![ rec.id, rec.project_id, rec.title, rec.description, rec.status, rec.priority, rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch, - rec.review_rounds, rec.created_at, rec.updated_at + rec.review_rounds, rec.output_json, rec.created_at, rec.updated_at ], ) }, update => |conn, rec| { conn.execute( - "UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, updated_at = ?11 WHERE id = ?12", + "UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, output_json = ?11, updated_at = ?12 WHERE id = ?13", params![ rec.project_id, rec.title, rec.description, rec.status, rec.priority, rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch, - rec.review_rounds, rec.updated_at, rec.id + rec.review_rounds, rec.output_json, rec.updated_at, rec.id ], ) } @@ -769,14 +774,14 @@ impl_repo!( impl TaskRepo { /// 列出未删除任务(deleted_at IS NULL)— 对标 ProjectRepo::list_active /// - /// 显式列出 13 个 TaskRecord 列名(同 ProjectRepo::list_active 写法), + /// 显式列出 14 个 TaskRecord 列名(同 ProjectRepo::list_active 写法), /// 不 SELECT deleted_at:TaskRecord 不带该字段,取了 from_row 会因未知列报错。 pub async fn list_active(&self) -> Result> { let conn = self.conn.clone(); tokio::task::spawn_blocking(move || { let guard = conn.blocking_lock(); let mut stmt = guard - .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC") + .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC") .map_err(storage_err)?; let rows = stmt .query_map([], |row| task_from_row(row)) @@ -877,7 +882,7 @@ impl TaskRepo { } // 回读更新后的记录(含新 status / 累加后的 review_rounds / 新 updated_at)。 let mut stmt = guard - .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, created_at, updated_at FROM tasks WHERE id = ?1") + .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE id = ?1") .map_err(storage_err)?; let row = stmt .query_row(params![id], |row| task_from_row(row)) @@ -898,7 +903,7 @@ impl TaskRepo { tokio::task::spawn_blocking(move || { let guard = conn.blocking_lock(); let mut stmt = guard - .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC") + .prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC") .map_err(storage_err)?; let rows = stmt .query_map([], |row| task_from_row(row)) diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs index e5474c6..2f8d57c 100644 --- a/crates/df-storage/src/migrations.rs +++ b/crates/df-storage/src/migrations.rs @@ -34,7 +34,7 @@ pub fn run(conn: &Connection) -> Result<()> { // 迁移步骤链: 顺序执行,跳过已应用的版本(current_version < N 才跑)。 // 新增版本时,在此数组追加一项 (N, migrate_vN) 即可,无需改逻辑。 - let steps: [(i32, fn(&Connection) -> Result<()>); 16] = [ + let steps: [(i32, fn(&Connection) -> Result<()>); 17] = [ (1, migrate_v1), (2, migrate_v2), (3, migrate_v3), @@ -51,6 +51,7 @@ pub fn run(conn: &Connection) -> Result<()> { (14, migrate_v14), (15, migrate_v15), (16, migrate_v16), + (17, migrate_v17), ]; for (version, migrate_fn) in steps { @@ -295,6 +296,22 @@ fn migrate_v16(conn: &Connection) -> Result<()> { Ok(()) } +/// V17: 幂等补 tasks.output_json 列(AiNode 自审闭环产出,决策 a:task 中心) +/// +/// 任务产出 JSON 字符串:ai_execute 写产出 / ai_self_review 读产出做自审 / human_review 展示对象。 +/// TEXT NULL 向后兼容(老库行默认 NULL,TaskRecord 字段为 Option), +/// 经通用 update_field 白名单写入(非 status 状态机收口字段,status 收口 F-03 不变)。 +/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v4/v5/v6/v8/v10/v11/v14/v15/v16 模式),对新库/老库均安全。 +fn migrate_v17(conn: &Connection) -> Result<()> { + if !column_exists(conn, "tasks", "output_json") { + conn.execute("ALTER TABLE tasks ADD COLUMN output_json TEXT", [])?; + tracing::info!("v17: 补建 tasks.output_json 列(AiNode 自审闭环产出)"); + } + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [17])?; + tracing::info!("迁移 v17 完成"); + Ok(()) +} + /// V1 建表 SQL const V1_SQL: &str = " -- 想法表 diff --git a/crates/df-storage/src/models.rs b/crates/df-storage/src/models.rs index 61bad48..ae08e4c 100644 --- a/crates/df-storage/src/models.rs +++ b/crates/df-storage/src/models.rs @@ -66,6 +66,14 @@ pub struct TaskRecord { /// #[serde(default)] 兼容旧前端无该字段的 JSON(老任务记录默认 0)。 #[serde(default)] pub review_rounds: i32, + /// 任务产出 JSON 字符串(AiNode 自审闭环,决策 a:task 中心,产出跟 task 走)。 + /// + /// 三方读写约定:ai_execute 写产出 / ai_self_review 读产出做自审 / human_review 展示产出对象。 + /// 经通用 update_field 写白名单(非 status 那种状态机收口字段),TEXT NULL 向后兼容。 + /// #[serde(skip_serializing_if)] 无产出不序列化(省前端字段空值),#[serde(default)] 兼容旧 JSON。 + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub output_json: Option, pub created_at: String, pub updated_at: String, } diff --git a/crates/df-storage/tests/project_soft_delete.rs b/crates/df-storage/tests/project_soft_delete.rs index 0529f46..95b0d7f 100644 --- a/crates/df-storage/tests/project_soft_delete.rs +++ b/crates/df-storage/tests/project_soft_delete.rs @@ -42,6 +42,7 @@ fn task(id: &str, project_id: &str) -> TaskRecord { workflow_def_id: None, base_branch: None, review_rounds: 0, + output_json: None, created_at: now_ts(), updated_at: now_ts(), } diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index 84a2535..adb02c1 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -350,6 +350,7 @@ pub fn build_ai_tool_registry(db: &Arc) -> AiToolRegistry { status: "todo".to_string(), priority: args["priority"].as_i64().unwrap_or(2) as i32, branch_name: None, assignee: None, workflow_def_id: None, base_branch: None, review_rounds: 0, + output_json: None, created_at: now_millis(), updated_at: now_millis(), }; let id = record.id.clone(); diff --git a/src-tauri/src/commands/task.rs b/src-tauri/src/commands/task.rs index 72ff7f0..b82e181 100644 --- a/src-tauri/src/commands/task.rs +++ b/src-tauri/src/commands/task.rs @@ -78,6 +78,7 @@ pub async fn create_task( workflow_def_id: None, base_branch: None, review_rounds: 0, + output_json: None, created_at: now.clone(), updated_at: now, };