重构: AiChat第三批TopBar + audit第二批restore/finalize(strategy并行)

- AiChat 第三批: 新建 ai/TopBar.vue(499行, header+provider bar+切换bar+scoped样式), AiChat.vue 3128→2937(-191); props4+emits13+store共享; toast单一源emit provider-switched
- audit 第二批: 新建 audit/restore.rs(183行 restore_pending_approvals+2单测) + audit/finalize.rs(79行 audit_tool_call/audit_finalize), audit/mod.rs 812→586(-226); re-export路径透明, F-09 per_conv保留
主代兜底: cargo check --workspace 0 + test 98 + vue-tsc 0
strategy: 单批1-2文件原子, 并行(AiChat前端vue-tsc/audit后端cargo独立域)
git add指定(非-A, 避免覆盖其他会话改动)
This commit is contained in:
2026-06-19 04:16:57 +08:00
parent e0ffd98223
commit ed35c51a47
4 changed files with 485 additions and 237 deletions

View File

@@ -0,0 +1,79 @@
//! 工具调用审计记录写/回填(audit_tool_call 写入 + audit_finalize 审批后状态回填)
//!
//! 第二批从 audit/mod.rs 抽出(行为零变更,纯 fn 搬迁)。re-export 经 audit/mod.rs
//! `pub use finalize::{audit_tool_call, audit_finalize};` 保持 `commands::ai::audit::*`
//! 路径透明(调用方零变更)。
use df_ai::ai_tools::RiskLevel;
use df_storage::crud::AiToolExecutionRepo;
use df_storage::models::AiToolExecutionRecord;
use df_types::types::new_id;
use crate::commands::now_millis;
use crate::state::AppState;
use super::risk_str;
/// 写一条工具执行审计记录insert 失败不阻断主流程,故 `let _ =`
///
/// `decided_by` 有值auto/human= 已决策执行 → 记 executed_at
/// `None`pending 待审批)→ executed_at 留空,待 audit_finalize 回填。
pub(crate) async fn audit_tool_call(
repo: &AiToolExecutionRepo,
conv_id: &str,
tool_call_id: &str,
tool_name: &str,
arguments: &str,
status: &str,
risk_level: RiskLevel,
result: Option<String>,
decided_by: Option<&str>,
) {
let executed_at = if decided_by.is_some() { Some(now_millis()) } else { None };
let _ = repo
.insert(AiToolExecutionRecord {
id: new_id(),
conversation_id: Some(conv_id.to_string()),
tool_call_id: tool_call_id.to_string(),
tool_name: tool_name.to_string(),
arguments: arguments.to_string(),
result,
status: status.to_string(),
risk_level: risk_str(risk_level).to_string(),
requested_at: now_millis(),
executed_at,
decided_by: decided_by.map(|s| s.to_string()),
})
.await;
}
/// 审批后更新审计记录状态(按 tool_call_id 定位 pending 记录,回填 status/decided_by=human/executed_at/result
///
/// 走专用 find_by_tool_call_id —— 通用 query 宏硬编码 ORDER BY created_at DESC
/// 而 ai_tool_executions 无该列,调用会报 "no such column: created_at" 被 unwrap_or_default 吞掉,
/// 导致审批后审计记录永久卡 pending。
pub(crate) async fn audit_finalize(state: &AppState, tool_call_id: &str, status: &str, result: Option<String>) {
// 区分 Err(DB 故障)与 Ok(None)(真无记录):原 unwrap_or_default 把 Err 压成 None,
// DB 故障被「未找到」日志掩盖,审批后审计记录卡 pending 无确诊线索(B-260617-17 同款吞错)。
let mut rec = match state.ai_tool_executions.find_by_tool_call_id(tool_call_id).await {
Ok(Some(rec)) => rec,
Ok(None) => {
tracing::warn!("audit_finalize: 未找到 tool_call_id={} 的审计记录", tool_call_id);
return;
}
Err(e) => {
tracing::error!("audit_finalize: 查询 tool_call_id={} 审计记录失败(DB 故障,状态未回填): {}", tool_call_id, e);
return;
}
};
rec.status = status.to_string();
rec.decided_by = Some("human".to_string());
rec.executed_at = Some(now_millis());
if let Some(r) = result {
rec.result = Some(r);
}
if let Err(e) = state.ai_tool_executions.update_full(&rec).await {
tracing::error!("audit_finalize: 回填 tool_call_id={} 审计状态失败: {}", tool_call_id, e);
}
}