优化: aichat效率剩余(压缩后台化防阻塞/审计批量事务/只读缓存轮内去重/流式增量渲染/AiCommandOutput合批/双渲染合并) + 跨端加固(df-project路径保留大小写/tunnel文档更正supervisor重连/relay固定时间比较与帧上限/启动校验) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:59 +08:00
parent fbd8fae44b
commit 11f4978ec1
15 changed files with 841 additions and 193 deletions
+114 -6
View File
@@ -41,7 +41,7 @@ use approval::{detect_retry_count, handle_approval_tool};
// list_tool_executions 是 #[tauri::command]ToolExecutionDto 供前端 DTO 序列化)。
pub mod record;
#[allow(unused_imports)]
pub(crate) use record::{audit_tool_call, query_audit_history, record_audit};
pub(crate) use record::{audit_tool_call, build_audit_record, query_audit_history, record_audit};
#[allow(unused_imports)]
pub use record::{
list_tool_executions, tool_failure_stats, ToolExecutionDto, ToolExecutionPage, ToolExecQuery,
@@ -70,7 +70,7 @@ pub(crate) use finalize::audit_finalize;
// cache(audit/cache.rs):高危工具去重缓存 + 只读工具缓存。
// 第三批从本文件抽离,行为零变更。
mod cache;
pub(super) use cache::{cache_hit_warning, detect_listing_bypass, find_cached_readonly_result, insert_listing_bypass_warning, pending_placeholder_for};
pub(super) use cache::{cache_hit_warning, detect_listing_bypass, find_cached_readonly_result, insert_listing_bypass_warning, pending_placeholder_for, readonly_cache_args_key};
// data_change(audit/data_change.rs):AR-11 数据变更联动刷新。
// 第四批从本文件抽离,行为零变更。pub(crate) use 保持 emit_data_changed 对 crate 内可见
@@ -594,16 +594,37 @@ pub(crate) async fn process_tool_calls(
// 同参只读工具成功执行过,命中则直接回填缓存结果跳过真执行,断 LLM 失忆死循环。
// 安全边界见 find_cached_readonly_result 文档(仅白名单只读工具 + 仅 completed 成功结果)。
//
// AC-EFF-T1-2(2026-08-09)轮内去重:同轮重复 (tool, args_key) 只真执行一次。
// LLM 单轮可能发多条同参只读调用(失忆重调),原实现首查未命中(本轮首个尚未落 messages)
// 致重复全部真执行(重复 I/O + 重复 tool_result 回灌)。现进程内暂存首个结果
// (in_round_cache),后续同 key 复用——语义同跨轮缓存,仅因首个在本轮未落库故内存暂存。
// 命中计数递增威慑(cache_hit_warning 同口径);key 与跨轮缓存同源(readonly_cache_args_key),
// 防两处 key 口径漂移。跨轮重复仍走 find_cached_readonly_result(历史扫描,未变)。
//
// find_cached_readonly_result 内部短 lock + 锁外 DB 查,本段不持锁。
// in_round_cache:同轮 (tool, args_key) -> (首个结果内容, 已见同参次数)。首个出现:
// 跨轮缓存命中 → 存 (content, hit_count);未命中 → 真执行,backfill 后存 (content, 1)。
// in_round_dupes:同轮重复的 draft(延后到 backfill 后复用 in_round_cache,防双执行)。
let mut in_round_cache: HashMap<(String, String), (String, u32)> = HashMap::new();
let mut seen_round_keys: HashSet<(String, String)> = HashSet::new();
let mut in_round_dupes: Vec<(ToolCallDraft, RiskLevel, String)> = Vec::new();
let mut low_risk_uncached: Vec<(ToolCallDraft, serde_json::Value, RiskLevel)> = Vec::with_capacity(low_risk.len());
for (draft, args, risk_level) in low_risk {
let args_key = readonly_cache_args_key(&draft.name, &args);
let key = (draft.name.clone(), args_key.clone());
if !seen_round_keys.insert(key) {
// 轮内重复:延后复用首个结果(首个已在 seen/uncached/backfill),不重复历史扫描+DB 查。
in_round_dupes.push((draft, risk_level, args_key));
continue;
}
let cached = find_cached_readonly_result(session_arc, conv_id, &audit_repo, &draft.name, &args).await;
if let Some((cached_content, hit_count)) = cached {
// 缓存命中:直接 push tool_result + 审计(decided_by=cache_hit 标记缓存来源),
// 不走真执行 + 不重emit Started/Completed(避免误导前端工具又执行了一次)
// 缓存命中:存轮内缓存(供后续同 key 复用)+ 直接 push tool_result + 审计
// (decided_by=cache_hit 标记缓存来源),不走真执行 + 不重emit Started/Completed。
// AC-1 根治:弱模型不知道结果来自缓存,仍死循环重调同参工具。此处给回填的
// tool_result 前置「重复调用拦截」警告头(机制化提示,LLM 能看到 tool_result),
// 命中次数递增威慑,告知勿再重复调用相同参数的工具。
in_round_cache.insert((draft.name.clone(), args_key), (cached_content.clone(), hit_count));
let warned_content = format!("{}{}", cache_hit_warning(&draft.name, hit_count), cached_content);
// emit Completed 携带缓存结果供前端折叠卡片展示(与 find_cached_high_risk_result 一致)。
let ev = AiChatEvent::AiToolCallCompleted {
@@ -680,7 +701,12 @@ pub(crate) async fn process_tool_calls(
}
})).await;
// 串行回填 tool_result + 审计短 lock push + 锁外 audit
// 串行回填 tool_result + 审计收集(短 lock push + 锁外收集),审计改单事务批量插入。
// AC-EFF-T1-1(2026-08-09):原每结果 audit_tool_call(每次 spawn_blocking + 单行 INSERT,
// 单连接 Mutex 串行)合并为 build_audit_record 收集 + insert_batch 一次事务批量,
// 砍 N 次串行 INSERT 尾巴(治 aichat 效率走查 T1-1)。审计失败不阻断主流程(记日志)。
let mut audit_records: Vec<df_storage::models::AiToolExecutionRecord> =
Vec::with_capacity(results.len());
for (draft, raw_result, risk_level, outcome) in results {
let (status, content) = match outcome {
Ok(c) => ("completed", c),
@@ -717,7 +743,89 @@ pub(crate) async fn process_tool_calls(
RiskLevel::Medium => "auto_takeover_medium",
RiskLevel::High => "auto_takeover_all",
};
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, status, risk_level, Some(content), Some(decided_by), current_message_id).await;
// AC-EFF-T1-2 轮内去重回填:真执行结果存入 in_round_cache,供同轮后续同 key 复用
// (首个执行 → count=1,轮内重复命中时递增;key 与跨轮缓存同源)。
{
let args_val = serde_json::from_str(&draft.args).unwrap_or(serde_json::Value::Null);
let exec_key = (draft.name.clone(), readonly_cache_args_key(&draft.name, &args_val));
in_round_cache.entry(exec_key).or_insert_with(|| (content.clone(), 1));
}
audit_records.push(build_audit_record(
conv_id, &draft.id, &draft.name, &draft.args, status, risk_level,
Some(content), Some(decided_by), current_message_id,
));
}
// 单事务批量 INSERT(一次 spawn_blocking,砍 N 次串行尾巴;失败仅记日志不阻断)。
if !audit_records.is_empty() {
let batch_len = audit_records.len();
if let Err(e) = audit_repo.insert_batch(audit_records).await {
tracing::error!(
conv_id = %conv_id,
batch = batch_len,
"[ai] insert_batch 批量写审计记录失败(已回滚,共 {} 条): {}",
batch_len,
e
);
}
}
// AC-EFF-T1-2 轮内重复处理:复用首个结果(已回填 in_round_cache),跳过重复真执行。
// 语义同跨轮缓存命中:emit Completed + push tool_result + 审计(decided_by=cache_hit),
// 命中计数递增威慑(cache_hit_warning 同口径)。首个结果来自跨轮命中(前段已存)或
// 本轮真执行 backfill(上段已存),故必命中;防御性缺失则丢弃(不重复执行,LLM 下轮可重调)。
let mut dupe_audit_records: Vec<df_storage::models::AiToolExecutionRecord> =
Vec::with_capacity(in_round_dupes.len());
for (draft, risk_level, args_key) in in_round_dupes {
let key = (draft.name.clone(), args_key);
let Some((content, count)) = in_round_cache.get(&key).cloned() else {
tracing::warn!(
conv_id = %conv_id,
tool = %draft.name,
tc_id = %draft.id,
"[ai] 轮内去重:首个结果缺失(理论不可达),丢弃重复调用(防御降级)"
);
continue;
};
let new_count = count + 1;
in_round_cache.insert(key, (content.clone(), new_count));
let warned_content = format!("{}{}", cache_hit_warning(&draft.name, new_count), content);
// emit Completed 携带缓存结果供前端折叠卡片展示。
let ev = AiChatEvent::AiToolCallCompleted {
id: draft.id.clone(),
result: serde_json::Value::String(warned_content.clone()),
conversation_id: Some(conv_id.to_string()),
};
let _ = app_handle.emit("ai-chat-event", ev.clone());
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
// 短 lock 段:push tool_result(纯写,无 await)
{
let mut session = session_arc.lock().await;
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &warned_content));
}
dupe_audit_records.push(build_audit_record(
conv_id, &draft.id, &draft.name, &draft.args, "completed", risk_level,
Some(warned_content), Some("cache_hit"), current_message_id,
));
tracing::info!(
conv_id = %conv_id,
tool = %draft.name,
tc_id = %draft.id,
hit_count = new_count,
"[ai] 只读工具轮内去重命中(第 {} 次同参,本轮首个已执行),跳过重复真执行",
new_count
);
}
if !dupe_audit_records.is_empty() {
let dup_len = dupe_audit_records.len();
if let Err(e) = audit_repo.insert_batch(dupe_audit_records).await {
tracing::error!(
conv_id = %conv_id,
batch = dup_len,
"[ai] insert_batch 轮内去重审计批量写失败(已回滚,共 {} 条): {}",
dup_len,
e
);
}
}
}