From ba811ead96b670b539e31fddddc0bfe0b45270f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 24 Jun 2026 17:48:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20TD-260621-05=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E7=8A=B6=E6=80=81=E5=8F=8C=E8=BD=A8=E7=BB=9F=E4=B8=80?= =?UTF-8?q?(executed=E2=86=92completed+V27=E8=BF=81=E7=A7=BB+=E6=B5=8B?= =?UTF-8?q?=E8=AF=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/df-storage/src/migrations.rs | 105 ++++++++++++++++++++- src-tauri/src/commands/ai/commands/chat.rs | 20 ++-- 2 files changed, 114 insertions(+), 11 deletions(-) diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs index 3fcbfe0..0eac50b 100644 --- a/crates/df-storage/src/migrations.rs +++ b/crates/df-storage/src/migrations.rs @@ -29,7 +29,10 @@ pub fn run(conn: &Connection) -> Result<()> { // V24 = ideas.related_ids 列(灵感间关联关系持久化打底); // V25 = idea_evaluations (idea_id, version) 唯一约束(评估版本并发重复兜底); // V26 = F-260621-02 任务索引缺口补全(priority/assignee,对齐 idx_tasks_status 同类索引)。 - let steps: [(i32, fn(&Connection) -> Result<()>); 26] = [ + // V27 = TD-260621-05 审批状态统一(ai_tool_executions.status executed→completed, + // 对齐 DTO 契约 audit/mod.rs:53 只列 completed + 前端 i18n auditLog.status 无 executed 键 + + // 治 AuditLog executed 记录显示错位蓝pending标签+raw"executed")。 + let steps: [(i32, fn(&Connection) -> Result<()>); 27] = [ (1, migrate_v1), (2, migrate_v2), (3, migrate_v3), @@ -56,6 +59,7 @@ pub fn run(conn: &Connection) -> Result<()> { (24, migrate_v24), (25, migrate_v25), (26, migrate_v26), + (27, migrate_v27), ]; for (version, migrate_fn) in steps { @@ -657,6 +661,24 @@ fn migrate_v26(conn: &Connection) -> Result<()> { Ok(()) } +/// V27: 审批状态统一 executed→completed(TD-260621-05) +/// +/// chat.rs ai_approve/ai_authorize_dir 审批通过后工具执行成功历史写 "executed",audit 内联执行 +/// (低风险无审批)写 "completed",双轨并存致:① DTO 文档(audit/mod.rs:53)只列 completed 契约失配; +/// ② 前端 AuditLog statusClass/i18n auditLog.status 无 executed 键 → executed 记录显示蓝pending +/// 标签 + raw"executed"文案矛盾(用户见"待审批 executed");③ 未来 WHERE status='completed' +/// 统计会漏 executed 路径。统一为 completed(find_cached SW-16 透传已兼容双值,无破坏)。 +/// 数据迁移:存量 executed→completed,新库/老库均跑(UPDATE 0 行也安全)。 +fn migrate_v27(conn: &Connection) -> Result<()> { + conn.execute( + "UPDATE ai_tool_executions SET status = 'completed' WHERE status = 'executed'", + [], + )?; + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [27])?; + tracing::info!("迁移 v27 完成(审批状态 executed→completed 统一)"); + Ok(()) +} + /// V21 建表 SQL — 消息拆分存储 ai_messages 表 /// /// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。 @@ -1261,4 +1283,85 @@ mod tests { .unwrap(); assert_eq!(v, 20); } + + /// V27:审批状态统一 executed→completed(TD-260621-05) + /// 存量 executed 记录转 completed,rejected/completed/failed 不变(只动 executed) + #[test] + fn v27_unifies_executed_to_completed() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch( + "CREATE TABLE schema_version (version INTEGER PRIMARY KEY); + CREATE TABLE ai_tool_executions ( + id TEXT PRIMARY KEY, + tool_call_id TEXT, + tool_name TEXT, + status TEXT NOT NULL, + requested_at TEXT, + executed_at TEXT + );", + ) + .expect("create ai_tool_executions"); + + // 混合状态:2 executed(待转)+ completed/rejected/failed(应不变) + conn.execute_batch( + "INSERT INTO ai_tool_executions (id, tool_call_id, tool_name, status) VALUES + ('e1', 'tc1', 'write_file', 'executed'), + ('e2', 'tc2', 'read_file', 'executed'), + ('c1', 'tc3', 'list_directory', 'completed'), + ('r1', 'tc4', 'write_file', 'rejected'), + ('f1', 'tc5', 'run_command', 'failed');", + ) + .unwrap(); + + migrate_v27(&conn).expect("v27 应成功统一状态"); + + // executed 全部转 completed + let executed_left: i64 = conn + .query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'executed'", [], |r| r.get(0)) + .unwrap(); + assert_eq!(executed_left, 0, "executed 应全部转为 completed"); + + let completed: i64 = conn + .query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'completed'", [], |r| r.get(0)) + .unwrap(); + assert_eq!(completed, 3, "原 2 executed + 1 completed = 3 completed"); + + // 其他状态不受影响 + let rejected: i64 = conn + .query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'rejected'", [], |r| r.get(0)) + .unwrap(); + assert_eq!(rejected, 1, "rejected 不变"); + let failed: i64 = conn + .query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'failed'", [], |r| r.get(0)) + .unwrap(); + assert_eq!(failed, 1, "failed 不变"); + + let v: i64 = conn + .query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0)) + .unwrap(); + assert_eq!(v, 27, "应写入版本号 27"); + } + + /// V27 幂等:无 executed 时再跑不崩(UPDATE 0 行),版本号照写 + #[test] + fn v27_idempotent_no_executed() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch( + "CREATE TABLE schema_version (version INTEGER PRIMARY KEY); + CREATE TABLE ai_tool_executions (id TEXT PRIMARY KEY, status TEXT NOT NULL);", + ) + .expect("create tables"); + conn.execute( + "INSERT INTO ai_tool_executions (id, status) VALUES ('c1', 'completed')", + [], + ) + .unwrap(); + + migrate_v27(&conn).expect("v27 无 executed 时应幂等成功(UPDATE 0 行)"); + + let v: i64 = conn + .query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0)) + .unwrap(); + assert_eq!(v, 27); + } } diff --git a/src-tauri/src/commands/ai/commands/chat.rs b/src-tauri/src/commands/ai/commands/chat.rs index 6e4c5cc..08abd50 100644 --- a/src-tauri/src/commands/ai/commands/chat.rs +++ b/src-tauri/src/commands/ai/commands/chat.rs @@ -457,7 +457,7 @@ pub async fn ai_approve( // Err 压成 None,DB 故障被「未找到」误导(实为可重试故障),对齐 audit.rs audit_finalize 三路分流。 match state.ai_tool_executions.find_by_tool_call_id(&tool_call_id).await { Ok(Some(rec)) => { - if rec.status == "executed" || rec.status == "rejected" || rec.status == "failed" { + if rec.status == "completed" || rec.status == "rejected" || rec.status == "failed" { return Ok(format!("已处理({})", rec.status)); } } @@ -534,7 +534,7 @@ pub async fn ai_approve( // session_trust —— 下次同会话同类操作(同工具+同目录 TrustKey)自动放行,跳过 pending + 二次确认。 // 仅首批工具(write_file/run_command)命中 trust_key_for 的 Some,其余工具 noop(None 不写)。 // F-260616-09 B 批4:per_conv.session_trust 唯一真相源(conv_id 来源 approval.conversation_id)。 - // 无 conv_id(无主审批 R-9 异常数据)时无 per_conv 可写,信任不记(审计仍记 executed)。 + // 无 conv_id(无主审批 R-9 异常数据)时无 per_conv 可写,信任不记(审计仍记 completed)。 if let Some(key) = super::super::trust_key_for(&approval.tool_name, &approval.arguments) { let inserted = if let Some(ref cid) = conv_id { session.conv(cid).session_trust.insert(key.clone()) @@ -581,7 +581,7 @@ pub async fn ai_approve( // 工具失败不 return Err:把错误包成 tool_result,落库 + emit completed + 续循环全走通。 // 否则前端 approveToolCall 的 catch 会回滚 pending_approval,审批按钮卡死无法消除。 let (audit_status, result_val) = match &exec_result { - Ok(val) => ("executed", val.clone()), + Ok(val) => ("completed", val.clone()), Err(e) => ("failed", serde_json::Value::String(e.to_string())), }; // 审计:人工审批后无论成败回填(决策者=human) @@ -601,7 +601,7 @@ pub async fn ai_approve( // 重新获取锁,替换占位 tool_result 为真实结果(失败时为错误信息,LLM 据此决定下一步) // F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源 approval.conversation_id)。 - // 无 conv_id 的无主审批不入 messages(审计仍记 executed/failed)。 + // 无 conv_id 的无主审批不入 messages(审计仍记 completed/failed)。 let mut session = state.ai_session.lock().await; if let Some(ref cid) = conv_id { session.conv(cid).messages.replace_tool_result_content(&id, &result_val.to_string()); @@ -627,11 +627,11 @@ pub async fn ai_approve( // F-260619-04 P2(方案 B): create_idea source 消息级溯源补全。 // create_idea 是 Medium risk → 经审批执行(本路径),非 process_tool_calls 内联执行, - // 故补全点放此(工具已 executed,可拿到 result.id + args.source)。 + // 故补全点放此(工具已 completed,可拿到 result.id + args.source)。 // message_id 从 per_conv.messages 取末条 assistant id(同 process_tool_calls 的 current_message_id 语义); // assistant_with_tools 消息持久驻留 messages,审批时仍可读。 - // 仅 executed(成功)且 create_idea 才补;失败/其他工具 noop(helper 内判定,不污染本路径主流程)。 - if audit_status == "executed" { + // 仅 completed(成功)且 create_idea 才补;失败/其他工具 noop(helper 内判定,不污染本路径主流程)。 + if audit_status == "completed" { let message_id = { let session = state.ai_session.lock().await; conv_id.as_deref().and_then(|cid| { @@ -665,7 +665,7 @@ pub async fn ai_approve( // 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续) try_continue_agent_loop(&app, &state, &cont_conv_id, start_iter).await; - Ok("executed".to_string()) + Ok("completed".to_string()) } /// F-260620 临时诊断:授权/审批 IPC 调用链文件日志(不依赖终端 stderr,读 authz-debug.log 定位)。 @@ -882,11 +882,11 @@ pub async fn ai_authorize_dir( }; tracing::debug!( ok = exec_result.is_ok(), - status = if exec_result.is_ok() { "executed" } else { "failed" }, + status = if exec_result.is_ok() { "completed" } else { "failed" }, "[AI-AUTHZ] execute 后" ); let (audit_status, mut result_val) = match &exec_result { - Ok(val) => ("executed", val.clone()), + Ok(val) => ("completed", val.clone()), Err(e) => ("failed", serde_json::Value::String(e.to_string())), }; // 阶段4(容错/恢复):跨盘预检 → tool_result 提示。