优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)

This commit is contained in:
2026-06-18 22:57:19 +08:00
parent 0ca5d9805f
commit a2871a66e0
87 changed files with 5720 additions and 3012 deletions

View File

@@ -21,6 +21,13 @@ use crate::commands::{err_str, now_millis};
use super::{AiChatEvent, AiSession, PendingApproval, ToolCallDraft};
use super::tool_registry::generate_diff;
/// Med/High 工具挂起审批时回填的占位 tool_result 内容。
///
/// 单一常量避免「写入处」(process_tool_calls) 与「去重排查处」
/// (find_cached_high_risk_result) 各持一份字面量致耦合——若两者漂移,
/// 去重会把 pending 占位误判为已落定结果命中缓存,污染 LLM 上下文。
const PENDING_APPROVAL_PLACEHOLDER: &str = "需要用户审批,等待确认";
/// RiskLevel → 审计记录字符串low/medium/high
pub(crate) fn risk_str(r: RiskLevel) -> &'static str {
match r {
@@ -265,14 +272,16 @@ pub async fn restore_pending_approvals(state: &AppState) {
let mut session = state.ai_session.lock().await;
for rec in pending {
let args: serde_json::Value = serde_json::from_str(&rec.arguments).unwrap_or_default();
let Some(risk) = risk_from_str(&rec.risk_level) else { continue };
// 过滤 risk_level 解析失败的损坏记录(语义保留:数据异常不恢复)·不再绑定 risk(PendingApproval.risk_level 已删)
if risk_from_str(&rec.risk_level).is_none() {
continue;
}
session.pending_approvals.insert(
rec.tool_call_id.clone(),
PendingApproval {
tool_call_id: rec.tool_call_id,
tool_name: rec.tool_name,
arguments: args,
risk_level: risk,
conversation_id: rec.conversation_id,
recovered: true,
// AE-2025-03: 重启恢复的审批不重读旧文件——审批可能跨重启,
@@ -325,14 +334,18 @@ pub(crate) async fn audit_tool_call(
/// 而 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>) {
let Some(mut rec) = state
.ai_tool_executions
.find_by_tool_call_id(tool_call_id)
.await
.unwrap_or_default()
else {
tracing::warn!("audit_finalize: 未找到 tool_call_id={} 的审计记录", tool_call_id);
return;
// 区分 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());
@@ -340,7 +353,9 @@ pub(crate) async fn audit_finalize(state: &AppState, tool_call_id: &str, status:
if let Some(r) = result {
rec.result = Some(r);
}
let _ = state.ai_tool_executions.update_full(&rec).await;
if let Err(e) = state.ai_tool_executions.update_full(&rec).await {
tracing::error!("audit_finalize: 回填 tool_call_id={} 审计状态失败: {}", tool_call_id, e);
}
}
/// AR-11方案A工具名 → (entity, action) 映射,用于数据变更联动刷新。
@@ -386,6 +401,10 @@ pub(crate) fn emit_data_changed(app_handle: &AppHandle, tool_name: &str) {
/// F-260616-05高危工具去重根治 run_command 超时→重试→重新审批循环)。
///
/// ⚠ 性能注记(CR-260618-11#5):本函数在 session lock 持有期间对每个 high risk 工具
/// 串行查 AiToolExecutionRepo::find_by_tool_call_id,工具数多时锁持有线性增长。
/// 批量预取(改签名传 tool_call_ids 批量查)属架构改暂未做,后续 High risk 工具增多时优先处理。
///
/// **根因链**F-04 batch42 已做超时标注LLM 调 run_command 超时 → F-04 把超时标注成
/// tool_result 回传 LLM → LLM不可靠仍重试同命令 → 新 tool_call_idprovider 每轮新 id
/// → `process_tool_calls` 重新 insert pending(audit.rs:418) → 用户被迫重新审批,循环。
@@ -403,11 +422,12 @@ pub(crate) fn emit_data_changed(app_handle: &AppHandle, tool_name: &str) {
/// - 返回 None 表示无缓存命中(走原审批流程)。
///
/// `session` 只读扫描 messages不写调用方据返回值决定是否跳过 insert pending。
fn find_cached_high_risk_result(
async fn find_cached_high_risk_result(
session: &AiSession,
audit_repo: &AiToolExecutionRepo,
tool_name: &str,
args: &serde_json::Value,
) -> Option<String> {
) -> Option<(String, String)> {
use df_ai::provider::MessageRole;
// 规范化新调用的 args 为可比字符串(排序键,键序无关)
@@ -454,11 +474,20 @@ fn find_cached_high_risk_result(
if msg.tool_call_id.as_deref() != Some(old_id.as_str()) {
continue;
}
// 命中旧 tool_result排除 pending 占位(内容固定为「需要用户审批,等待确认」
if msg.content == "需要用户审批,等待确认" {
// 命中旧 tool_result排除 pending 占位(内容固定为 PENDING_APPROVAL_PLACEHOLDER
if msg.content == PENDING_APPROVAL_PLACEHOLDER {
return None;
}
return Some(msg.content.clone());
// SW-260618-16: 查审计表拿缓存来源真实 status(completed/rejected/failed),透传给
// audit_tool_call 而非固定 completed(审计语义与结果内容一致,防"rejected/failed 结果
// 记 completed"误导安全追溯)。审计记录缺失/查询失败 fallback completed(不阻塞去重,降级原行为)。
let status = audit_repo
.find_by_tool_call_id(old_id.as_str())
.await
.ok()
.and_then(|opt| opt.map(|rec| rec.status))
.unwrap_or_else(|| "completed".to_string());
return Some((msg.content.clone(), status));
}
None
}
@@ -609,7 +638,7 @@ pub(crate) async fn process_tool_calls(
}
// F-05仅 High 查去重缓存Med 保持原审批流程
if matches!(risk_level, RiskLevel::High) {
if let Some(cached) = find_cached_high_risk_result(session, &draft.name, &args) {
if let Some((cached, status)) = find_cached_high_risk_result(session, &audit_repo, &draft.name, &args).await {
// 命中:把缓存结果作为新 tool_call_id 的 tool_result 回传,跳过审批
tracing::info!(
tool = %draft.name,
@@ -622,8 +651,8 @@ pub(crate) async fn process_tool_calls(
result: serde_json::Value::String(cached.clone()),
conversation_id: Some(conv_id.to_string()),
});
// 审计:去重命中记一条 completed(decided_by=auto_dedup),不进 pending
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "completed", risk_level, Some(cached), Some("auto_dedup")).await;
// 审计:去重命中记一条(status 透传缓存来源 completed/rejected/failed,SW-260618-16;decided_by=auto_dedup),不进 pending
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, &status, risk_level, Some(cached), Some("auto_dedup")).await;
continue;
}
}
@@ -641,12 +670,11 @@ pub(crate) async fn process_tool_calls(
tool_call_id: draft.id.clone(),
tool_name: draft.name.clone(),
arguments: args.clone(),
risk_level,
conversation_id: Some(conv_id.to_string()),
recovered: false,
diff: approval_diff.clone(),
});
session.messages.push(ChatMessage::tool_result(&draft.id, "需要用户审批,等待确认"));
session.messages.push(ChatMessage::tool_result(&draft.id, PENDING_APPROVAL_PLACEHOLDER));
let reason = build_approval_reason(&draft.name, &args, risk_level, db).await;
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiApprovalRequired {
id: draft.id.clone(),