新增: TaskRecord加output_json产出字段+V17迁移(决策a AiNode自审闭环)

This commit is contained in:
2026-06-16 22:29:21 +08:00
parent b5a9557b50
commit 539b5ed63e
7 changed files with 45 additions and 11 deletions

View File

@@ -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(),
}

View File

@@ -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<TaskRecord, rusqlite::Err
workflow_def_id: row.get("workflow_def_id")?,
base_branch: row.get("base_branch")?,
review_rounds: row.get("review_rounds")?,
output_json: row.get("output_json")?,
created_at: row.get("created_at")?,
updated_at: row.get("updated_at")?,
})
@@ -745,22 +750,22 @@ impl_repo!(
from_row => |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<Vec<TaskRecord>> {
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))

View File

@@ -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<String>),
/// 经通用 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 = "
-- 想法表

View File

@@ -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<String>,
pub created_at: String,
pub updated_at: String,
}

View File

@@ -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(),
}