重构: audit第三批cache + anthropic_compat拆分(strategy并行)
- audit 第三批: 新建 audit/cache.rs(151行 find_cached_high_risk_result/canonical_args_key/sort_object_keys/PENDING_APPROVAL_PLACEHOLDER), audit/mod.rs 586→451(-135); pub(crate) re-export, F-09 per_conv保留 - anthropic_compat 拆分: 新建 df-ai/anthropic_helpers.rs(223行 apply_anthropic_event + 5 struct + 2常量), anthropic_compat.rs 1036→842; Provider impl保留(impl块约束), 字段pub(crate) 主代兜底: cargo check --workspace 0 + test df-ai 119 + test devflow 98 strategy: 核心库自底向上, 单批1-2文件原子, impl块约束(Provider方法保留只抽纯函数/类型) git add指定(audit/* + df-ai/*, 不含其他会话)
This commit is contained in:
@@ -18,13 +18,6 @@ use crate::commands::err_str;
|
||||
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 {
|
||||
@@ -177,139 +170,11 @@ 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_id(provider 每轮新 id)
|
||||
/// → `process_tool_calls` 重新 insert pending(audit.rs:418) → 用户被迫重新审批,循环。
|
||||
///
|
||||
/// **治本(F-05)**:即使 LLM 仍重试,同 (tool_name, args) 不重复审批。本函数扫描会话历史,
|
||||
/// 若发现**已落定**(executed/failed/rejected,非 pending)的同类同参高危调用,返回其
|
||||
/// tool_result 内容,调用方把缓存结果作为新 tool_call_id 的 tool_result 回传 LLM,跳过审批。
|
||||
///
|
||||
/// **安全边界**:
|
||||
/// - 仅 High risk(循环源头);Low/Med 不去重(Low 直行无审批,Med 重试场景少且去重易误伤)。
|
||||
/// - 仅匹配**已落定**结果(pending 的不命中——pending 已有独立流程,不会触发循环;
|
||||
/// 且避免两个 pending 互相吞掉审批)。LLM 只有在收到 tool_result 后才会重试,
|
||||
/// 故循环必然是「上一条已落定 → 重试」形态,pending 不命中不影响治本。
|
||||
/// - args 走 JSON 规范化比较(键序无关),避免 LLM 两次生成键序不同误判为不同命令。
|
||||
/// - 返回 None 表示无缓存命中(走原审批流程)。
|
||||
///
|
||||
/// `session` 只读扫描 messages(不写),调用方据返回值决定是否跳过 insert pending。
|
||||
///
|
||||
/// F-260616-09 B 批2:经 `conv_id` 索引 per_conv.messages(顶层 messages 批2 后是死字段)。
|
||||
/// conv_id 来源:process_tool_calls 入参 → 由 agentic.rs run_agentic_loop 入参透传。
|
||||
async fn find_cached_high_risk_result(
|
||||
session: &AiSession,
|
||||
conv_id: &str,
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Option<(String, String)> {
|
||||
use df_ai::provider::MessageRole;
|
||||
|
||||
// 规范化新调用的 args 为可比字符串(排序键,键序无关)
|
||||
let new_args_key = canonical_args_key(args);
|
||||
|
||||
// F-260616-09 B 批2:读 per_conv.messages。process_tool_calls 调用前 loop 入口已桥接建立 per_conv,
|
||||
// 故 conv_read 必命中;防御性 None 时返 None(无缓存命中,走原审批流程)。
|
||||
let conv = session.conv_read(conv_id)?;
|
||||
// ContextManager::iter 返回 impl Iterator(非 DoubleEnded),collect 成 Vec 再反向遍历。
|
||||
// 单对话消息量小(百级),collect 开销可忽略。
|
||||
let msgs: Vec<&ChatMessage> = conv.messages.iter().collect();
|
||||
|
||||
// 1) 反向扫描 assistant tool_calls,找最近一条同名同参的 High 工具调用 → 拿到旧 tool_call_id
|
||||
// 反向:循环是「最近一次超时→重试」,命中通常是末尾附近,反向先停省全扫。
|
||||
let mut prev_tool_call_id: Option<String> = None;
|
||||
for msg in msgs.iter().rev() {
|
||||
if !matches!(msg.role, MessageRole::Assistant) {
|
||||
continue;
|
||||
}
|
||||
let Some(tcs) = msg.tool_calls.as_ref() else { continue };
|
||||
for tc in tcs {
|
||||
if tc.function.name != tool_name {
|
||||
continue;
|
||||
}
|
||||
// 旧调用的 args 是流式拼接的 JSON 字符串,解析失败跳过(不误判为命中)
|
||||
let Ok(old_args) = serde_json::from_str::<serde_json::Value>(&tc.function.arguments) else {
|
||||
continue;
|
||||
};
|
||||
if canonical_args_key(&old_args) == new_args_key {
|
||||
prev_tool_call_id = Some(tc.id.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if prev_tool_call_id.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 用旧 tool_call_id 找对应 tool_result。注意:审批拒绝/超时失败也属「已落定」,
|
||||
// 其 tool_result 内容同样回传(LLM 看到原反馈自行决定,不再逼用户二次审批)。
|
||||
// pending 占位(「需要用户审批,等待确认」)不命中——仍在审批中,走原流程。
|
||||
let old_id = prev_tool_call_id?;
|
||||
for msg in msgs.iter().rev() {
|
||||
if !matches!(msg.role, MessageRole::Tool) {
|
||||
continue;
|
||||
}
|
||||
if msg.tool_call_id.as_deref() != Some(old_id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
// 命中旧 tool_result:排除 pending 占位(内容固定为 PENDING_APPROVAL_PLACEHOLDER)
|
||||
if msg.content == PENDING_APPROVAL_PLACEHOLDER {
|
||||
return None;
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
/// 把 JSON args 规范化为可比字符串:对象键按字典序排序后序列化,
|
||||
/// 键序不同的等价参数生成同一 key(防 LLM 两次生成键序不同误判为不同命令)。
|
||||
fn canonical_args_key(args: &serde_json::Value) -> String {
|
||||
let mut v = args.clone();
|
||||
sort_object_keys(&mut v);
|
||||
// 紧凑序列化(无空白),保证稳定可比
|
||||
serde_json::to_string(&v).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 递归对 JSON 对象的键做字典序排序(就地),数组成员也递归排序。
|
||||
fn sort_object_keys(v: &mut serde_json::Value) {
|
||||
match v {
|
||||
serde_json::Value::Object(map) => {
|
||||
// BTreeMap 按键排序,重建 Object
|
||||
let mut entries: Vec<(String, serde_json::Value)> = map
|
||||
.iter()
|
||||
.map(|(k, val)| (k.clone(), val.clone()))
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
map.clear();
|
||||
for (k, mut val) in entries {
|
||||
sort_object_keys(&mut val);
|
||||
map.insert(k, val);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
for item in arr {
|
||||
sort_object_keys(item);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// cache(audit/cache.rs):F-260616-05 高危工具去重缓存。
|
||||
// 第三批从本文件抽离,行为零变更。pub(super) use 供本文件 process_tool_calls 裸名调用
|
||||
// (find_cached_high_risk_result),PENDING_APPROVAL_PLACEHOLDER 供 process_tool_calls 占位回填共享。
|
||||
mod cache;
|
||||
pub(super) use cache::{find_cached_high_risk_result, PENDING_APPROVAL_PLACEHOLDER};
|
||||
|
||||
/// AE-2025-03(路径 B):write_file 审批预览 diff 生成。
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user