新增: 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
This commit is contained in:
@@ -392,6 +392,7 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
|||||||
base_branch: None,
|
base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
|
idea_id: None,
|
||||||
created_at: now.clone(),
|
created_at: now.clone(),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
};
|
};
|
||||||
@@ -444,6 +445,7 @@ fn update_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
|||||||
base_branch: existing.base_branch,
|
base_branch: existing.base_branch,
|
||||||
review_rounds: existing.review_rounds,
|
review_rounds: existing.review_rounds,
|
||||||
output_json: existing.output_json,
|
output_json: existing.output_json,
|
||||||
|
idea_id: existing.idea_id,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -699,6 +699,7 @@ mod tests {
|
|||||||
base_branch: None,
|
base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: task_output_json.map(String::from),
|
output_json: task_output_json.map(String::from),
|
||||||
|
idea_id: None,
|
||||||
created_at: "0".to_string(),
|
created_at: "0".to_string(),
|
||||||
updated_at: "0".to_string(),
|
updated_at: "0".to_string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ mod tests {
|
|||||||
base_branch: None,
|
base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
|
idea_id: None,
|
||||||
created_at: "0".to_string(),
|
created_at: "0".to_string(),
|
||||||
updated_at: "0".to_string(),
|
updated_at: "0".to_string(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> {
|
|||||||
// output_json:ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象
|
// output_json:ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象
|
||||||
// (决策 a:task 中心,产出跟 task 走)。非状态机收口字段,合法可写。
|
// (决策 a:task 中心,产出跟 task 走)。非状态机收口字段,合法可写。
|
||||||
"output_json",
|
"output_json",
|
||||||
|
// idea_id(F-260619-01 任务关联灵感,1对1 单向):任务可关联/解关联一条灵感,
|
||||||
|
// 非状态机收口字段,合法可写(idea_id 存在性由外键约束 + 上层校验兜底)。
|
||||||
|
"idea_id",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
// TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。
|
// TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。
|
||||||
// 通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。
|
// 通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn task_from_row(row: &Row<'_>) -> std::result::Result<TaskRecord, rusqlite::Err
|
|||||||
base_branch: row.get("base_branch")?,
|
base_branch: row.get("base_branch")?,
|
||||||
review_rounds: row.get("review_rounds")?,
|
review_rounds: row.get("review_rounds")?,
|
||||||
output_json: row.get("output_json")?,
|
output_json: row.get("output_json")?,
|
||||||
|
idea_id: row.get("idea_id")?,
|
||||||
created_at: row.get("created_at")?,
|
created_at: row.get("created_at")?,
|
||||||
updated_at: row.get("updated_at")?,
|
updated_at: row.get("updated_at")?,
|
||||||
})
|
})
|
||||||
@@ -48,22 +49,22 @@ impl_repo!(
|
|||||||
from_row => |row| task_from_row(row),
|
from_row => |row| task_from_row(row),
|
||||||
insert => |conn, rec| {
|
insert => |conn, rec| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"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)
|
"INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||||
params![
|
params![
|
||||||
rec.id, rec.project_id, rec.title, rec.description, rec.status, rec.priority,
|
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.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||||
rec.review_rounds, rec.output_json, rec.created_at, rec.updated_at
|
rec.review_rounds, rec.output_json, rec.idea_id, rec.created_at, rec.updated_at
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
update => |conn, rec| {
|
update => |conn, rec| {
|
||||||
conn.execute(
|
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, output_json = ?11, updated_at = ?12 WHERE id = ?13",
|
"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, idea_id = ?12, updated_at = ?13 WHERE id = ?14",
|
||||||
params![
|
params![
|
||||||
rec.project_id, rec.title, rec.description, rec.status, rec.priority,
|
rec.project_id, rec.title, rec.description, rec.status, rec.priority,
|
||||||
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||||
rec.review_rounds, rec.output_json, rec.updated_at, rec.id
|
rec.review_rounds, rec.output_json, rec.idea_id, rec.updated_at, rec.id
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,7 +80,7 @@ impl TaskRepo {
|
|||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let guard = conn.blocking_lock();
|
let guard = conn.blocking_lock();
|
||||||
let mut stmt = guard
|
let mut stmt = guard
|
||||||
.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")
|
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC")
|
||||||
.map_err(storage_err)?;
|
.map_err(storage_err)?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], |row| task_from_row(row))
|
.query_map([], |row| task_from_row(row))
|
||||||
@@ -180,7 +181,7 @@ impl TaskRepo {
|
|||||||
}
|
}
|
||||||
// 回读更新后的记录(含新 status / 累加后的 review_rounds / 新 updated_at)。
|
// 回读更新后的记录(含新 status / 累加后的 review_rounds / 新 updated_at)。
|
||||||
let mut stmt = guard
|
let mut stmt = guard
|
||||||
.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")
|
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at FROM tasks WHERE id = ?1")
|
||||||
.map_err(storage_err)?;
|
.map_err(storage_err)?;
|
||||||
let row = stmt
|
let row = stmt
|
||||||
.query_row(params![id], |row| task_from_row(row))
|
.query_row(params![id], |row| task_from_row(row))
|
||||||
@@ -201,7 +202,7 @@ impl TaskRepo {
|
|||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let guard = conn.blocking_lock();
|
let guard = conn.blocking_lock();
|
||||||
let mut stmt = guard
|
let mut stmt = guard
|
||||||
.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")
|
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC")
|
||||||
.map_err(storage_err)?;
|
.map_err(storage_err)?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], |row| task_from_row(row))
|
.query_map([], |row| task_from_row(row))
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
|||||||
|
|
||||||
// 迁移步骤链: 顺序执行,跳过已应用的版本(current_version < N 才跑)。
|
// 迁移步骤链: 顺序执行,跳过已应用的版本(current_version < N 才跑)。
|
||||||
// 新增版本时,在此数组追加一项 (N, migrate_vN) 即可,无需改逻辑。
|
// 新增版本时,在此数组追加一项 (N, migrate_vN) 即可,无需改逻辑。
|
||||||
// V20 预留给 F-260619-01(任务关联灵感),跳过;V21 = 消息拆分存储 + audit message_id。
|
// V20 = F-260619-01(任务关联灵感 idea_id);V21 = 消息拆分存储 + audit message_id。
|
||||||
let steps: [(i32, fn(&Connection) -> Result<()>); 20] = [
|
let steps: [(i32, fn(&Connection) -> Result<()>); 21] = [
|
||||||
(1, migrate_v1),
|
(1, migrate_v1),
|
||||||
(2, migrate_v2),
|
(2, migrate_v2),
|
||||||
(3, migrate_v3),
|
(3, migrate_v3),
|
||||||
@@ -44,6 +44,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
|||||||
(17, migrate_v17),
|
(17, migrate_v17),
|
||||||
(18, migrate_v18),
|
(18, migrate_v18),
|
||||||
(19, migrate_v19),
|
(19, migrate_v19),
|
||||||
|
(20, migrate_v20),
|
||||||
(21, migrate_v21),
|
(21, migrate_v21),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -350,6 +351,26 @@ fn migrate_v19(conn: &Connection) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// V20:幂等补 tasks.idea_id 列(F-260619-01 任务关联灵感,1对1 单向)
|
||||||
|
///
|
||||||
|
/// 任务可关联到一条灵感(任务→灵感单向),复用 projects.idea_id 模式
|
||||||
|
/// (REFERENCES ideas(id) 外键)。TEXT NULL 向后兼容:老库行默认 NULL,TaskRecord
|
||||||
|
/// 字段为 Option<String>(未关联灵感的任务为 None)。
|
||||||
|
/// 用 PRAGMA 探测列存在性,缺失才 ALTER(同 v11/v14/v17 模式),对新库/老库均安全。
|
||||||
|
/// 新库已在 V9_SQL(tasks 建表)直接带 idea_id 列,此处只补老库。
|
||||||
|
fn migrate_v20(conn: &Connection) -> Result<()> {
|
||||||
|
if !column_exists(conn, "tasks", "idea_id") {
|
||||||
|
conn.execute(
|
||||||
|
"ALTER TABLE tasks ADD COLUMN idea_id TEXT REFERENCES ideas(id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
tracing::info!("v20: 补建 tasks.idea_id 列(任务关联灵感,F-260619-01)");
|
||||||
|
}
|
||||||
|
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [20])?;
|
||||||
|
tracing::info!("迁移 v20 完成");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// V21:消息拆分存储(ai_messages 表 + 全量迁移)+ ai_tool_executions.message_id 列
|
/// V21:消息拆分存储(ai_messages 表 + 全量迁移)+ ai_tool_executions.message_id 列
|
||||||
///
|
///
|
||||||
/// **一次原子迁移**(决策 V21 合并,不拆 V21a/V21b):
|
/// **一次原子迁移**(决策 V21 合并,不拆 V21a/V21b):
|
||||||
@@ -554,6 +575,8 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|||||||
priority INTEGER NOT NULL DEFAULT 2,
|
priority INTEGER NOT NULL DEFAULT 2,
|
||||||
branch_name TEXT,
|
branch_name TEXT,
|
||||||
assignee TEXT,
|
assignee TEXT,
|
||||||
|
-- F-260619-01 任务关联灵感(1对1 单向,复用 projects.idea_id 模式)。老库由 V20 迁移补列。
|
||||||
|
idea_id TEXT REFERENCES ideas(id),
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -997,4 +1020,72 @@ mod tests {
|
|||||||
"迁移后应有 message_id 列"
|
"迁移后应有 message_id 列"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// V20 迁移幂等安全(F-260619-01 任务关联灵感)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// 构造最小老库 schema:tasks 表(无 idea_id 列,模拟 V1 建表老形态)+ schema_version。
|
||||||
|
fn setup_legacy_tasks_db() -> Connection {
|
||||||
|
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE schema_version (version INTEGER PRIMARY KEY);
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'todo',
|
||||||
|
priority INTEGER NOT NULL DEFAULT 2,
|
||||||
|
branch_name TEXT,
|
||||||
|
assignee TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);",
|
||||||
|
)
|
||||||
|
.expect("create legacy tasks table");
|
||||||
|
conn
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 老库无 idea_id 列:迁移应补建 + 写版本号 20
|
||||||
|
#[test]
|
||||||
|
fn v20_legacy_db_adds_idea_id_column() {
|
||||||
|
let conn = setup_legacy_tasks_db();
|
||||||
|
assert!(
|
||||||
|
!column_exists(&conn, "tasks", "idea_id"),
|
||||||
|
"迁移前应无 idea_id 列"
|
||||||
|
);
|
||||||
|
|
||||||
|
migrate_v20(&conn).expect("v20 应在老库补建 idea_id 列");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
column_exists(&conn, "tasks", "idea_id"),
|
||||||
|
"迁移后应有 idea_id 列"
|
||||||
|
);
|
||||||
|
let v: i64 = conn
|
||||||
|
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(v, 20, "应写入版本号 20");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 幂等重跑:列已存在时跳过 ALTER,版本号不重复写(PK 冲突防)
|
||||||
|
/// 注:migrate_v20 用普通 INSERT(非 IGNORE),重跑会因 PK 冲突报错——这是预期行为,
|
||||||
|
/// run() 正常流程下 current_version<20 只调一次;此处验证列存在时 ALTER 被短路(不报 duplicate column)。
|
||||||
|
#[test]
|
||||||
|
fn v20_column_exists_skips_alter() {
|
||||||
|
let conn = setup_legacy_tasks_db();
|
||||||
|
// 先跑一次补列
|
||||||
|
migrate_v20(&conn).expect("首次迁移");
|
||||||
|
assert!(column_exists(&conn, "tasks", "idea_id"));
|
||||||
|
|
||||||
|
// 手动回退版本号模拟「列已存在但版本号未写」场景,验证 ALTER 被短路不报 duplicate column
|
||||||
|
conn.execute("DELETE FROM schema_version WHERE version = 20", [])
|
||||||
|
.unwrap();
|
||||||
|
// 此时列存在但版本号 20 缺失 → migrate_v20 应跳过 ALTER 只补版本号
|
||||||
|
migrate_v20(&conn).expect("列存在时应跳过 ALTER 不报错");
|
||||||
|
let v: i64 = conn
|
||||||
|
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(v, 20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ pub struct TaskRecord {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub output_json: Option<String>,
|
pub output_json: Option<String>,
|
||||||
|
/// 关联灵感 ID(F-260619-01,1对1 单向,任务→灵感)。
|
||||||
|
/// 复用 projects.idea_id 模式(REFERENCES ideas(id) 外键),可空(任务可不关联灵感)。
|
||||||
|
/// 老任务无 idea_id → None。#[serde(default)] 兼容旧前端 JSON(无该字段时为 None)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub idea_id: Option<String>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ fn task(id: &str, project_id: &str) -> TaskRecord {
|
|||||||
base_branch: None,
|
base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
|
idea_id: None,
|
||||||
created_at: now_ts(),
|
created_at: now_ts(),
|
||||||
updated_at: now_ts(),
|
updated_at: now_ts(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -630,8 +630,8 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
|||||||
})},
|
})},
|
||||||
);
|
);
|
||||||
registry.register(
|
registry.register(
|
||||||
"create_task", "在指定项目下创建新任务",
|
"create_task", "在指定项目下创建新任务,可选传 idea_id 关联灵感(1对1 单向)",
|
||||||
df_ai::ai_tools::object_schema(vec![("project_id", "string", true), ("title", "string", true), ("description", "string", false), ("priority", "integer", false)]),
|
df_ai::ai_tools::object_schema(vec![("project_id", "string", true), ("title", "string", true), ("description", "string", false), ("priority", "integer", false), ("idea_id", "string", false)]),
|
||||||
RiskLevel::Medium,
|
RiskLevel::Medium,
|
||||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
@@ -647,6 +647,8 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
|||||||
branch_name: None, assignee: None, workflow_def_id: None, base_branch: None,
|
branch_name: None, assignee: None, workflow_def_id: None, base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
|
// F-260619-01 可选关联灵感(空字符串/缺省视为不关联)
|
||||||
|
idea_id: args.get("idea_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
||||||
created_at: now_millis(), updated_at: now_millis(),
|
created_at: now_millis(), updated_at: now_millis(),
|
||||||
};
|
};
|
||||||
let id = record.id.clone();
|
let id = record.id.clone();
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ pub struct CreateTaskInput {
|
|||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
pub branch_name: Option<String>,
|
pub branch_name: Option<String>,
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
|
/// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。
|
||||||
|
/// 空字符串视为不关联(与 AI 工具层一致)。
|
||||||
|
pub idea_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_priority() -> i32 {
|
fn default_priority() -> i32 {
|
||||||
@@ -79,6 +82,8 @@ pub async fn create_task(
|
|||||||
base_branch: None,
|
base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
|
// F-260619-01:空字符串视为不关联(与 AI 工具层一致)
|
||||||
|
idea_id: input.idea_id.filter(|s| !s.is_empty()),
|
||||||
created_at: now.clone(),
|
created_at: now.clone(),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ export interface TaskRecord {
|
|||||||
* 旧任务无产出时为 undefined。前端 parsedOutput try/catch 兜底解析。
|
* 旧任务无产出时为 undefined。前端 parsedOutput try/catch 兜底解析。
|
||||||
*/
|
*/
|
||||||
output_json?: string
|
output_json?: string
|
||||||
|
/** 关联灵感 ID(F-260619-01,1对1 单向,任务→灵感)。未关联时为 undefined。 */
|
||||||
|
idea_id?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -128,6 +130,8 @@ export interface CreateTaskInput {
|
|||||||
priority?: number
|
priority?: number
|
||||||
branch_name?: string
|
branch_name?: string
|
||||||
assignee?: string
|
assignee?: string
|
||||||
|
/** 关联灵感 ID(1对1 单向,可空)。空字符串视为不关联(与后端一致)。 */
|
||||||
|
idea_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default {
|
|||||||
status: 'Status',
|
status: 'Status',
|
||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
project: 'Project',
|
project: 'Project',
|
||||||
|
relatedIdea: 'Related Idea',
|
||||||
branch: 'Branch',
|
branch: 'Branch',
|
||||||
assignee: 'Assignee',
|
assignee: 'Assignee',
|
||||||
baseBranch: 'Base Branch',
|
baseBranch: 'Base Branch',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default {
|
|||||||
status: '状态',
|
status: '状态',
|
||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
project: '关联项目',
|
project: '关联项目',
|
||||||
|
relatedIdea: '关联灵感',
|
||||||
branch: '分支',
|
branch: '分支',
|
||||||
assignee: '负责人',
|
assignee: '负责人',
|
||||||
baseBranch: '基础分支',
|
baseBranch: '基础分支',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function createTasksStore() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string }) {
|
async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string; idea_id?: string }) {
|
||||||
try {
|
try {
|
||||||
const record = await taskApi.create(input)
|
const record = await taskApi.create(input)
|
||||||
state.tasks.push(record)
|
state.tasks.push(record)
|
||||||
|
|||||||
@@ -104,6 +104,16 @@
|
|||||||
<span v-else>—</span>
|
<span v-else>—</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- F-260619-01:关联灵感(1对1 单向,idea_id 解析为友好 title,非裸 id) -->
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">{{ $t('taskDetail.relatedIdea') }}</span>
|
||||||
|
<span class="value">
|
||||||
|
<router-link v-if="task.idea_id" :to="`/ideas/${task.idea_id}`" class="project-link">
|
||||||
|
{{ ideaTitle }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">{{ $t('taskDetail.branch') }}</span>
|
<span class="label">{{ $t('taskDetail.branch') }}</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
@@ -146,7 +156,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { taskApi, projectApi } from '@/api'
|
import { taskApi, projectApi, ideaApi } from '@/api'
|
||||||
import { workflowApi } from '@/api'
|
import { workflowApi } from '@/api'
|
||||||
import { formatDate } from '@/utils/time'
|
import { formatDate } from '@/utils/time'
|
||||||
import { useRendered } from '@/composables/useMarkdown'
|
import { useRendered } from '@/composables/useMarkdown'
|
||||||
@@ -156,7 +166,7 @@ import {
|
|||||||
priorityLabel,
|
priorityLabel,
|
||||||
priorityClass,
|
priorityClass,
|
||||||
} from '../constants/project'
|
} from '../constants/project'
|
||||||
import type { TaskRecord, ProjectRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
|
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
|
||||||
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
|
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -166,6 +176,8 @@ const loading = ref(true)
|
|||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const task = ref<TaskRecord | null>(null)
|
const task = ref<TaskRecord | null>(null)
|
||||||
const projects = ref<ProjectRecord[]>([])
|
const projects = ref<ProjectRecord[]>([])
|
||||||
|
// F-260619-01:灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式)
|
||||||
|
const ideas = ref<IdeaRecord[]>([])
|
||||||
const advancing = ref(false)
|
const advancing = ref(false)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -204,6 +216,14 @@ const projectName = computed(() => {
|
|||||||
return p?.name ?? task.value?.project_id ?? '—'
|
return p?.name ?? task.value?.project_id ?? '—'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// F-260619-01:解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击)
|
||||||
|
const ideaTitle = computed(() => {
|
||||||
|
const ideaId = task.value?.idea_id
|
||||||
|
if (!ideaId) return '—'
|
||||||
|
const idea = ideas.value.find(i => i.id === ideaId)
|
||||||
|
return idea?.title ?? ideaId
|
||||||
|
})
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// F-05 推进按钮 — 状态机合法下一态映射
|
// F-05 推进按钮 — 状态机合法下一态映射
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
@@ -379,13 +399,16 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
try {
|
try {
|
||||||
const [t, ps] = await Promise.all([
|
const [t, ps, ideasList] = await Promise.all([
|
||||||
taskApi.get(taskId.value),
|
taskApi.get(taskId.value),
|
||||||
// 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store)
|
// 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store)
|
||||||
projectApi.list(),
|
projectApi.list(),
|
||||||
|
// F-260619-01:灵感列表用于解析 idea_id → 灵感 title
|
||||||
|
ideaApi.list(),
|
||||||
])
|
])
|
||||||
task.value = t
|
task.value = t
|
||||||
projects.value = ps
|
projects.value = ps
|
||||||
|
ideas.value = ideasList
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
task.value = null
|
task.value = null
|
||||||
errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') })
|
errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') })
|
||||||
@@ -417,7 +440,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
|
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
|
||||||
const { entity } = event.payload
|
const { entity } = event.payload
|
||||||
if (entity === 'task' || entity === 'project') {
|
if (entity === 'task' || entity === 'project' || entity === 'idea') {
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user