修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::provider::ChatMessage;
|
||||
@@ -91,20 +92,24 @@ pub(super) fn should_auto_for_persona(
|
||||
///
|
||||
/// 首批信任工具:write_file / run_command。同会话已批准过同工具+同目录 →
|
||||
/// `TrustKey` 命中,返回 `Some(TrustKey)`;否则返回 `None`(走原审批流程)。
|
||||
pub(super) fn check_trust_hits(
|
||||
///
|
||||
/// BUG-260624-03/P0 重构:签名改 `session_arc: &Arc<Mutex<AiSession>>`,内部短 lock 读
|
||||
/// `session_trust` 后立即 drop,信任查询不持锁,与 process_tool_calls 持锁 await 反模式解耦。
|
||||
pub(super) async fn check_trust_hits(
|
||||
draft: &ToolCallDraft,
|
||||
args: &serde_json::Value,
|
||||
session: &AiSession,
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
) -> Option<TrustKey> {
|
||||
trust_key_for(&draft.name, args)
|
||||
.and_then(|key| {
|
||||
if session.conv_read(conv_id).map(|c| c.session_trust.contains(&key)).unwrap_or(false) {
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
let key = trust_key_for(&draft.name, args)?;
|
||||
// 短 lock 读 session_trust(仅 contains 判定,无 await,纳秒级),命中即返 key
|
||||
let hit = {
|
||||
let session = session_arc.lock().await;
|
||||
session.conv_read(conv_id)
|
||||
.map(|c| c.session_trust.contains(&key))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
if hit { Some(key) } else { None }
|
||||
}
|
||||
|
||||
/// 插入 pending 审批:生成 diff → 重试检测 → insert PendingApproval + 占位 tool_result
|
||||
@@ -112,11 +117,15 @@ pub(super) fn check_trust_hits(
|
||||
///
|
||||
/// **注意**:调用方应在调用前先 +=1 `pending_count`(保持与原 `handle_approval_tool`
|
||||
/// 行为一致——重试 skip 分支也在 `pending_count += 1` 之后返回)。
|
||||
///
|
||||
/// BUG-260624-03/P0 重构:签名改 `session_arc: &Arc<Mutex<AiSession>>`,所有慢操作
|
||||
/// (build_write_file_diff/detect_retry_count/build_approval_reason/audit_tool_call)
|
||||
/// 在锁外 await,仅 `pending_approvals.insert` + `messages.push` 两处纯写改短 lock 段。
|
||||
pub(super) async fn insert_pending_approval(
|
||||
draft: ToolCallDraft,
|
||||
args: serde_json::Value,
|
||||
risk_level: RiskLevel,
|
||||
session: &mut AiSession,
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
app_handle: &AppHandle,
|
||||
@@ -144,7 +153,11 @@ pub(super) async fn insert_pending_approval(
|
||||
"已跳过重试(同 tool_call_id={} 此前已审批执行过,防 LLM 死循环重试同卡死工具)",
|
||||
draft.id
|
||||
);
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &skip_msg));
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &skip_msg));
|
||||
}
|
||||
// L3 emit 双写:重试 guard 跳过 emit Completed 双路发布。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
@@ -157,19 +170,24 @@ pub(super) async fn insert_pending_approval(
|
||||
return;
|
||||
}
|
||||
|
||||
session.pending_approvals.insert(draft.id.clone(), PendingApproval {
|
||||
tool_call_id: draft.id.clone(),
|
||||
tool_name: draft.name.clone(),
|
||||
arguments: args.clone(),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
recovered: false,
|
||||
// 阶段3a:普通 RiskLevel 审批标 kind=Risk{diff}(下沉原 diff 字段)。
|
||||
kind: ApprovalKind::Risk { diff: approval_diff.clone() },
|
||||
retry_count,
|
||||
created_at: Some(std::time::SystemTime::now()),
|
||||
});
|
||||
// 阶段2:占位带 __PENDING__:tc_id 标记,供 sanitize 豁免保留 + 出口断言自愈(防 400 orphan)
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
||||
// 短 lock 段:纯写 pending_approvals.insert + 占位 tool_result push
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.pending_approvals.insert(draft.id.clone(), PendingApproval {
|
||||
tool_call_id: draft.id.clone(),
|
||||
tool_name: draft.name.clone(),
|
||||
arguments: args.clone(),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
recovered: false,
|
||||
// 阶段3a:普通 RiskLevel 审批标 kind=Risk{diff}(下沉原 diff 字段)。
|
||||
kind: ApprovalKind::Risk { diff: approval_diff.clone() },
|
||||
retry_count,
|
||||
created_at: Some(std::time::SystemTime::now()),
|
||||
});
|
||||
// 阶段2:占位带 __PENDING__:tc_id 标记,供 sanitize 豁免保留 + 出口断言自愈(防 400 orphan)
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
||||
}
|
||||
// 慢操作锁外:拼 reason(DB 读)+ emit + 审计落 pending 纪录(DB 写)
|
||||
let reason = build_approval_reason(&draft.name, &args, risk_level, db).await;
|
||||
// L3 emit 双写:Med/High 风险审批挂起 AiApprovalRequired 双路发布(tunnel 透传 miniapp 弹审批窗)。
|
||||
let ev = AiChatEvent::AiApprovalRequired {
|
||||
@@ -225,13 +243,16 @@ pub(super) async fn detect_retry_count(audit_repo: &AiToolExecutionRepo, tc_id:
|
||||
/// 2. 否则走审批分支:会话信任(`check_trust_hits`)→ F-05 高危去重缓存 → 阶段4 重试 guard →
|
||||
/// `insert_pending_approval`(write_file diff + 挂起 + emit + 审计落 pending 记录)。
|
||||
///
|
||||
/// **不持 session 锁**:调用方(process_tool_calls)在持锁循环内调用本函数。
|
||||
/// BUG-260624-03/P0 重构:签名改 `session_arc: &Arc<Mutex<AiSession>>`,所有 session 访问
|
||||
/// 都改短 lock 段(check_trust_hits/find_cached_high_risk_result 内部短 lock,命中后 push
|
||||
/// tool_result 短 lock 段)。慢操作(audit_tool_call/find_cached_high_risk_result 的 DB 查)
|
||||
/// 全在锁外 await,根治 process_tool_calls 持 session lock 期间 await 慢操作死锁反模式。
|
||||
pub(super) async fn handle_approval_tool(
|
||||
draft: ToolCallDraft,
|
||||
args: serde_json::Value,
|
||||
risk_level: RiskLevel,
|
||||
auto_exec_mode: &str,
|
||||
session: &mut AiSession,
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
_tools_arc: &Arc<AiToolRegistry>,
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
@@ -254,7 +275,7 @@ pub(super) async fn handle_approval_tool(
|
||||
// 命中后走与 F-05 去重命中相似的「直接执行 + Completed + 审计 decided_by=auto_trust」路径,
|
||||
// 但与 F-05 不同:F-05 复用缓存 tool_result 跳过执行;trust 放行**真实执行工具**
|
||||
// (用户信任同目录同类操作,但仍要看每次的真实结果)。
|
||||
if let Some(key) = check_trust_hits(&draft, &args, session, conv_id) {
|
||||
if let Some(key) = check_trust_hits(&draft, &args, session_arc, conv_id).await {
|
||||
let dir_label = match &key {
|
||||
TrustKey::Write { dir } | TrustKey::Execute { dir } => dir.clone(),
|
||||
};
|
||||
@@ -282,14 +303,20 @@ pub(super) async fn handle_approval_tool(
|
||||
|
||||
// ── Step 3: F-05 高危去重缓存(仅 High) ──
|
||||
if matches!(risk_level, RiskLevel::High) {
|
||||
if let Some((cached, status)) = find_cached_high_risk_result(session, conv_id, audit_repo, &draft.name, &args).await {
|
||||
// find_cached_high_risk_result 内部短 lock 读 messages + 锁外 await DB 查 status,
|
||||
// 返回 (cached_content, status) 时不持锁
|
||||
if let Some((cached, status)) = find_cached_high_risk_result(session_arc, conv_id, audit_repo, &draft.name, &args).await {
|
||||
// 命中:把缓存结果作为新 tool_call_id 的 tool_result 回传,跳过审批
|
||||
tracing::info!(
|
||||
tool = %draft.name,
|
||||
new_tool_call_id = %draft.id,
|
||||
"[F-05] 高危工具去重命中:LLM 重试同命令,复用缓存结果跳过审批(断循环)"
|
||||
);
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &cached));
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &cached));
|
||||
}
|
||||
// L3 emit 双写:高危去重命中复用缓存 emit Completed 双路发布。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
@@ -307,7 +334,7 @@ pub(super) async fn handle_approval_tool(
|
||||
// ── Step 4: 插入 pending 审批 ──
|
||||
*pending_count += 1;
|
||||
insert_pending_approval(
|
||||
draft, args, risk_level, session, conv_id, audit_repo, app_handle, db,
|
||||
draft, args, risk_level, session_arc, conv_id, audit_repo, app_handle, db,
|
||||
current_message_id,
|
||||
).await;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
//! + `sort_object_keys` + `PENDING_APPROVAL_PLACEHOLDER`(去重与 process_tool_calls
|
||||
//! 共用占位常量)。三函数内部闭环(find_cached 调 canonical→sort),无外部 helper 依赖。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::provider::ChatMessage;
|
||||
use df_storage::crud::AiToolExecutionRepo;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::super::AiSession;
|
||||
|
||||
@@ -70,7 +73,7 @@ pub(crate) fn is_pending_placeholder(content: &str) -> bool {
|
||||
/// F-260616-09 B 批2:经`conv_id` 索引 per_conv.messages(顶层 messages 批2 后是死字段)。
|
||||
/// conv_id 来源:process_tool_calls 入参 → 由 agentic/mod.rs run_agentic_loop 入参透传。
|
||||
pub(crate) async fn find_cached_high_risk_result(
|
||||
session: &AiSession,
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
tool_name: &str,
|
||||
@@ -81,70 +84,78 @@ pub(crate) async fn find_cached_high_risk_result(
|
||||
// 规范化新调用的 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();
|
||||
// BUG-260624-03/P0 重构:短 lock 段读 messages + 找旧 tool_call_id + 旧 tool_result content,
|
||||
// drop 锁后再锁外 await DB 查 status(原代码持锁 await audit_repo,违反持锁 await 慢操作禁令)。
|
||||
// 第一步:锁内(async block 包裹,出 block 自动 drop guard)反向扫描,定位旧 tool_call_id 与对应 tool_result content
|
||||
let cached: Option<(String, String)> = (async {
|
||||
let session = session_arc.lock().await;
|
||||
// F-260616-09 B 批2:读 per_conv.messages。process_tool_calls 调用前 loop 入口已桥接建立 per_conv,
|
||||
// 故 conv_read 必命中;防御性 None 时返 None(无缓存命中,走原审批流程)。
|
||||
let Some(conv) = session.conv_read(conv_id) else { return None };
|
||||
// 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 {
|
||||
// 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;
|
||||
}
|
||||
// 旧调用的 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());
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
// 2) 用旧 tool_call_id 找对应 tool_result。注意:审批拒绝/超时失败也属「已落定」,
|
||||
// 其 tool_result 内容同样回传(LLM 看到原反馈自行决定,不再逼用户二次审批)。
|
||||
// pending 占位(「需要用户审批,等待确认」)不命中——仍在审批中,走原流程。
|
||||
let Some(old_id) = prev_tool_call_id else { return None };
|
||||
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__:tc_id 标记)
|
||||
// 阶段2:占位带标记后不再精确等于基础文本,改用 is_pending_placeholder 匹配。
|
||||
// 加固(子串误伤):`__PENDING__` 标记是 audit/cache.rs 占位模板独占信号(权威判定);
|
||||
// 老占位分支已收紧为精确全文等值(非 starts_with 前缀),杜绝用户真实 tool_result 内容
|
||||
// 恰以"需要用户审批..."开头被误判占位致去重误吞(详见 context_helpers::is_pending_placeholder)。
|
||||
if is_pending_placeholder(&msg.content) {
|
||||
return None;
|
||||
}
|
||||
return Some((old_id, msg.content.clone()));
|
||||
}
|
||||
if msg.tool_call_id.as_deref() != Some(old_id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
// 命中旧 tool_result:排除 pending 占位(基础文本或带 __PENDING__:tc_id 标记)
|
||||
// 阶段2:占位带标记后不再精确等于基础文本,改用 is_pending_placeholder 匹配。
|
||||
// 加固(子串误伤):`__PENDING__` 标记是 audit/cache.rs 占位模板独占信号(权威判定);
|
||||
// 老占位分支已收紧为精确全文等值(非 starts_with 前缀),杜绝用户真实 tool_result 内容
|
||||
// 恰以"需要用户审批..."开头被误判占位致去重误吞(详见 context_helpers::is_pending_placeholder)。
|
||||
if is_pending_placeholder(&msg.content) {
|
||||
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
|
||||
None
|
||||
}).await;
|
||||
// 锁外:查审计表拿缓存来源真实 status(completed/rejected/failed),透传给
|
||||
// audit_tool_call 而非固定 completed(审计语义与结果内容一致,防"rejected/failed 结果
|
||||
// 记 completed"误导安全追溯)。审计记录缺失/查询失败 fallback completed(不阻塞去重,降级原行为)。
|
||||
let (old_id, content) = cached?;
|
||||
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());
|
||||
Some((content, status))
|
||||
}
|
||||
|
||||
/// 把 JSON args 规范化为可比字符串:对象键按字典序排序后序列化,
|
||||
@@ -156,6 +167,117 @@ fn canonical_args_key(args: &serde_json::Value) -> String {
|
||||
serde_json::to_string(&v).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// BUG-2026-07-07/P0-2:只读幂等工具结果缓存(治 LLM 死循环重调)。
|
||||
///
|
||||
/// **根因链(实测 9357c27c)**:LLM 无"已调用过"记忆,对同参只读工具反复触发:
|
||||
/// - `read_file(application.xml)` 读了 17 次(两次返回完全相同的 8457c 全文)
|
||||
/// - `search_files(pattern='.java')` 连调 13 次(每次返回 items=0/has_more=True 几乎一致)
|
||||
/// 每次都真执行 + 全文回灌 tool_result,是 prompt 64 万的直接元凶。
|
||||
///
|
||||
/// **治本**:对幂等只读工具,扫描会话历史,若发现**已成功完成**的同类同参调用,
|
||||
/// 返回其 tool_result 内容,调用方把缓存结果作为新 tool_call_id 的 tool_result 回传 LLM,
|
||||
/// 跳过真实执行(也跳过 emit Started/Completed——直接走缓存回填路径)。
|
||||
///
|
||||
/// **安全边界**(对照 find_cached_high_risk_result 严格收紧):
|
||||
/// - 仅匹配白名单只读幂等工具(READONLY_CACHE_TOOLS):read_file/list_directory/
|
||||
/// search_files/grep/read_symbol。写工具(write_file/patch_file/run_command 等)
|
||||
/// 永不缓存,避免误伤。
|
||||
/// - 仅匹配 **status=completed** 的成功结果(失败/拒绝不缓存,可能重试成功)。
|
||||
/// - 仅匹配 **非占位** 结果(排除 pending_placeholder 占位)。
|
||||
/// - args 走 JSON 规范化比较(键序无关),与 find_cached_high_risk_result 一致。
|
||||
/// - 返回 None 表示无缓存命中(走原执行流程)。
|
||||
///
|
||||
/// 与 find_cached_high_risk_result 区别:后者治"超时→重试→重新审批"循环( High risk),
|
||||
/// 本函数治"LLM 失忆→同参重调"死循环( Low risk 只读),问题形态相似但触发原因不同。
|
||||
pub(crate) async fn find_cached_readonly_result(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Option<String> {
|
||||
use df_ai::provider::MessageRole;
|
||||
|
||||
// 白名单:只读幂等工具才缓存。写工具/有副作用工具永不缓存。
|
||||
if !READONLY_CACHE_TOOLS.contains(&tool_name) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_args_key = canonical_args_key(args);
|
||||
|
||||
// BUG-260624-03/P0 重构:短 lock 段读 messages + 锁外 await DB 查 status(原代码持锁 await)
|
||||
let cached: Option<(String, String)> = (async {
|
||||
let session = session_arc.lock().await;
|
||||
let Some(conv) = session.conv_read(conv_id) else { return None };
|
||||
let msgs: Vec<&ChatMessage> = conv.messages.iter().collect();
|
||||
|
||||
// 1) 反向扫描 assistant tool_calls,找最近一条同名同参调用 → 拿到旧 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;
|
||||
}
|
||||
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
|
||||
let Some(old_id) = prev_tool_call_id else { return None };
|
||||
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;
|
||||
}
|
||||
// 排除 pending 占位(仍在审批中,未真执行)
|
||||
if is_pending_placeholder(&msg.content) {
|
||||
return None;
|
||||
}
|
||||
return Some((old_id, msg.content.clone()));
|
||||
}
|
||||
None
|
||||
}).await;
|
||||
// 锁外:查审计表确认是 completed 成功结果(失败/拒绝不缓存,给 LLM 重试机会)
|
||||
let (old_id, content) = cached?;
|
||||
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());
|
||||
if status != "completed" {
|
||||
return None;
|
||||
}
|
||||
Some(content)
|
||||
}
|
||||
|
||||
/// 可缓存的只读幂等工具白名单。
|
||||
/// 仅含纯读取/查询类工具:同参多次调用结果一致,缓存命中安全。
|
||||
/// 写工具/有副作用工具(write_file/patch_file/run_command/create_*/update_*/delete_* 等)
|
||||
/// 永不进此白名单——它们每次执行都可能改变状态,缓存会掩盖变化。
|
||||
const READONLY_CACHE_TOOLS: &[&str] = &[
|
||||
"read_file", // 读文件内容(同 path+offset+limit+search 参数 → 同结果)
|
||||
"read_symbol", // AST 符号读取(同 path+symbol+full → 同结果)
|
||||
"list_directory", // 列目录(同 path+recursive+max_depth → 同结果)
|
||||
"search_files", // 搜文件名(同 path+pattern+offset+limit → 同结果)
|
||||
"grep", // 搜文件内容(同 path+pattern+mode → 同结果)
|
||||
];
|
||||
|
||||
/// 递归对 JSON 对象的键做字典序排序(就地),数组成员也递归排序。
|
||||
fn sort_object_keys(v: &mut serde_json::Value) {
|
||||
match v {
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::provider::ChatMessage;
|
||||
@@ -62,10 +63,10 @@ pub use restore::restore_pending_approvals;
|
||||
mod finalize;
|
||||
pub(crate) use finalize::audit_finalize;
|
||||
|
||||
// cache(audit/cache.rs):F-260616-05 高危工具去重缓存。
|
||||
// 第三批从本文件抽离,行为零变更。
|
||||
// cache(audit/cache.rs):F-260616-05 高危工具去重缓存 + BUG-2026-07-07/P0-2 只读工具缓存。
|
||||
// 第三批从本文件抽离,行为零变更(P0-2 为新增能力)。
|
||||
mod cache;
|
||||
pub(super) use cache::pending_placeholder_for;
|
||||
pub(super) use cache::{find_cached_readonly_result, pending_placeholder_for};
|
||||
|
||||
// data_change(audit/data_change.rs):AR-11 数据变更联动刷新。
|
||||
// 第四批从本文件抽离,行为零变更。pub(crate) use 保持 emit_data_changed 对 crate 内可见
|
||||
@@ -136,11 +137,25 @@ async fn execute_with_heartbeat(
|
||||
});
|
||||
// RAII:execute 无论 Ok/Err/panic(unwind),_guard drop 自动 stop+abort,无心跳 task 泄漏。
|
||||
let _guard = HeartbeatGuard { stop, handle: heartbeat };
|
||||
tools.execute(name, args).await
|
||||
// BUG-2026-07-19: tools.execute 无 timeout 时,卡死工具(run_command 长命令/read_file 大文件/同步
|
||||
// 阻塞工具)永久挂起 → process_tool_calls 持 session lock 永久 → guard.reset 等 lock → AiCompleted
|
||||
// 永不发 → 前端"回答完卡住/超时清空"。60s timeout 兜底:超时返错误 tool_result,锁释放,loop 续跑。
|
||||
// 心跳 30s 续命前端 watchdog,60s timeout 覆盖绝大多数工具(run_command 已自带 10s 子超时)。
|
||||
match tokio::time::timeout(Duration::from_secs(60), tools.execute(name, args)).await {
|
||||
Ok(result) => result,
|
||||
Err(_elapsed) => {
|
||||
tracing::error!(
|
||||
conv_id = %conv_id,
|
||||
tool = %name,
|
||||
"[ai] 工具执行超时(60s),返回错误 tool_result(防 process_tool_calls 持 session lock 永久卡死)"
|
||||
);
|
||||
Err(anyhow::anyhow!("工具执行超时(60s),已中止(防死锁)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn process_tool_calls(
|
||||
session: &mut AiSession,
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
tool_calls_acc: HashMap<u32, ToolCallDraft>,
|
||||
tools_arc: &Arc<AiToolRegistry>,
|
||||
db: &Arc<Database>,
|
||||
@@ -164,9 +179,33 @@ pub(crate) async fn process_tool_calls(
|
||||
// push 到 per_conv.messages(audit/mod.rs:882),此处取末条 assistant id 作为本轮工具
|
||||
// 调用所属的溯源 message_id,贯穿所有 audit_tool_call 写入。None 表示无 assistant 消息
|
||||
// (异常路径/老数据无 id),audit 落 message_id=None,展示侧兼容。
|
||||
let current_message_id: Option<String> = session
|
||||
.conv_read(conv_id)
|
||||
.and_then(|c| c.messages.last_assistant_message_id());
|
||||
//
|
||||
// BUG-260624-03/P0 重构:短 lock 段读 current_message_id(纯读,无 await),clone 出来后续传参。
|
||||
// 整个 process_tool_calls 不再要求调用方持锁,内部所有 session.xxx 访问均短 lock 段,
|
||||
// 慢操作(execute_with_heartbeat/audit_tool_call/detect_retry_count/DB 查)全在锁外 await。
|
||||
// BUG-2026-07-19: current_message_id 读加 200ms timeout(防 session lock 竞争卡死致
|
||||
// process_tool_calls 进不去 emit AiToolCallStarted → 前端工具卡片不呈现 + 45s 看门狗超时)。
|
||||
// 超时用 None(溯源降级,非致命)。治本:消除 session lock 长持有(save clone 段已优化)。
|
||||
let current_message_id: Option<String> = match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(200),
|
||||
async {
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let session = session_arc.lock().await;
|
||||
let __id = session.conv_read(conv_id)
|
||||
.and_then(|c| c.messages.last_assistant_message_id());
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:192 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
__id
|
||||
},
|
||||
).await {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
tracing::warn!(conv_id = %conv_id, "[ai] process_tool_calls current_message_id lock 200ms 超时,溯源降级 None(session lock 竞争)");
|
||||
None
|
||||
}
|
||||
};
|
||||
let current_message_id = current_message_id.as_deref();
|
||||
|
||||
// 解析 args + 批量发 Started(前端骨架按原始 index 顺序展示)
|
||||
@@ -218,8 +257,22 @@ pub(crate) async fn process_tool_calls(
|
||||
// Phase C: Denied 路径 → 硬拒(push 错误 tool_result + emit Completed),不挂起 loop。
|
||||
// 工具返 Err 让 LLM 知路径被禁,自行调整;loop 继续下一轮(不暂停)。
|
||||
for (draft, reason) in path_denied {
|
||||
let err_msg = format!("路径授权拒绝: {}", reason);
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &err_msg));
|
||||
// BUG-2026-07-07/P1-3:拒绝消息改结构化 JSON,防裸文本破坏 tool role 消息协议
|
||||
let err_msg = serde_json::json!({
|
||||
"status": "rejected",
|
||||
"reason": "path_blacklist",
|
||||
"message": format!("路径授权拒绝: {}", reason)
|
||||
}).to_string();
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &err_msg));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:262 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
}
|
||||
// L3 emit 双写:路径黑名单拒绝 emit Completed 双路发布。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
@@ -240,6 +293,9 @@ pub(crate) async fn process_tool_calls(
|
||||
// Phase B: NeedsAuth 路径 → 挂起 loop(insert pending + emit AiDirAuthRequired + push 占位 tool_result)。
|
||||
// 复用审批挂起架构:pending_approvals 以 tool_call_id 为键,ai_authorize_dir IPC remove 后恢复。
|
||||
// pending_count 计入(让 agentic loop 检测到挂起并暂停,等 ai_authorize_dir → try_continue 恢复)。
|
||||
//
|
||||
// BUG-260624-03/P0 重构:detect_retry_count(DB 读)/emit/audit_tool_call(DB 写)均在锁外,
|
||||
// 仅 push tool_result + pending_approvals.insert 两处纯写改短 lock 段。
|
||||
for (draft, args, req) in path_auth_pending {
|
||||
pending_count += 1;
|
||||
// L1 补丁:req.dirs 含所有未授权父目录;emit 用首个作主展示目录(前端弹窗主显)。
|
||||
@@ -248,14 +304,23 @@ pub(crate) async fn process_tool_calls(
|
||||
.map(|d| d.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
let path_str = req.raw_paths.first().cloned().unwrap_or_default();
|
||||
// 阶段4(容错/恢复,开关 df-ai-approval-retry):同 tc_id 重试检测。
|
||||
// 锁外:阶段4(容错/恢复,开关 df-ai-approval-retry):同 tc_id 重试检测。
|
||||
let retry_count = detect_retry_count(&audit_repo, &draft.id).await;
|
||||
if retry_count >= 1 {
|
||||
let skip_msg = format!(
|
||||
"已跳过重试(同 tool_call_id={} 此前已审批执行过,防 LLM 死循环重试同卡死工具)",
|
||||
draft.id
|
||||
);
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &skip_msg));
|
||||
// 短 lock 段:push tool_result(纯写)
|
||||
{
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &skip_msg));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:305 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
}
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
result: serde_json::Value::String(skip_msg.clone()),
|
||||
@@ -271,22 +336,31 @@ pub(crate) async fn process_tool_calls(
|
||||
).await;
|
||||
continue;
|
||||
}
|
||||
session.pending_approvals.insert(
|
||||
draft.id.clone(),
|
||||
PendingApproval {
|
||||
tool_call_id: draft.id.clone(),
|
||||
tool_name: draft.name.clone(),
|
||||
arguments: args.clone(),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
recovered: false,
|
||||
// 阶段3a:路径授权挂起标 kind=Path(req)(下沉原 path_auth 字段)。
|
||||
kind: ApprovalKind::Path(req),
|
||||
retry_count,
|
||||
created_at: Some(std::time::SystemTime::now()),
|
||||
},
|
||||
);
|
||||
// 占位 tool_result(与 RiskLevel 审批一致),ai_authorize_dir 批准后替换为真实结果。
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
||||
// 短 lock 段:pending_approvals.insert + 占位 tool_result push(两处纯写,无 await)
|
||||
{
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let mut session = session_arc.lock().await;
|
||||
session.pending_approvals.insert(
|
||||
draft.id.clone(),
|
||||
PendingApproval {
|
||||
tool_call_id: draft.id.clone(),
|
||||
tool_name: draft.name.clone(),
|
||||
arguments: args.clone(),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
recovered: false,
|
||||
// 阶段3a:路径授权挂起标 kind=Path(req)(下沉原 path_auth 字段)。
|
||||
kind: ApprovalKind::Path(req),
|
||||
retry_count,
|
||||
created_at: Some(std::time::SystemTime::now()),
|
||||
},
|
||||
);
|
||||
// 占位 tool_result(与 RiskLevel 审批一致),ai_authorize_dir 批准后替换为真实结果。
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:325 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
}
|
||||
tracing::debug!(target: "ai_dirauth", conv = %conv_id, tool = %draft.name, tc_id = %draft.id, "emit AiDirAuthRequired(路径授权挂起,等 ai_authorize_dir 恢复)");
|
||||
let ev = AiChatEvent::AiDirAuthRequired {
|
||||
id: draft.id.clone(),
|
||||
@@ -351,17 +425,17 @@ pub(crate) async fn process_tool_calls(
|
||||
let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High);
|
||||
handle_approval_tool(
|
||||
draft, args, risk_level, &auto_exec_mode,
|
||||
session, conv_id, tools_arc, &audit_repo,
|
||||
session_arc, conv_id, tools_arc, &audit_repo,
|
||||
app_handle, db, &mut low_risk, &mut trust_hits,
|
||||
&mut pending_count, current_message_id,
|
||||
).await;
|
||||
}
|
||||
|
||||
// AE-04 trust-hit 并行执行:execute + 即时 emit 在闭包内(闭包不访问 session,非"锁已释放"——
|
||||
// session 锁仍由调用方 agentic/mod.rs 持有至 process_tool_calls 返回,CR-53 审查纠正原"移锁外"误述),
|
||||
// push tool_result / audit 在 join_all 后串行回填(持锁)。对齐 Low risk 并行模式。
|
||||
// AE-04 trust-hit 并行执行:execute + 即时 emit 在闭包内(闭包不访问 session,
|
||||
// BUG-260624-03/P0 重构后调用方不再持锁,本段所有 await 完全在锁外),
|
||||
// push tool_result / audit 在 join_all 后串行回填(短 lock push + 锁外 audit)。对齐 Low risk 并行模式。
|
||||
// CR-51 修:原 inline .await execute 串行执行每个工具(阻塞期间锁被持有,run_command 慢命令
|
||||
// 阻塞同会话 IPC);改 join_all 并行多工具减少总阻塞时间(锁持有时长不变,并行化降阻塞)。
|
||||
// 阻塞同会话 IPC);改 join_all 并行多工具减少总阻塞时间。P0 重构后锁外并行,根本消除阻塞。
|
||||
// join_all 保序——结果顺序 = trust_hits 输入顺序 = tc_list 原始 index 顺序,不额外 sort
|
||||
if !trust_hits.is_empty() {
|
||||
let results: Vec<(ToolCallDraft, RiskLevel, Result<String, String>)> =
|
||||
@@ -403,23 +477,68 @@ pub(crate) async fn process_tool_calls(
|
||||
}
|
||||
})).await;
|
||||
|
||||
// 串行回填 tool_result + 审计(持 session 锁)
|
||||
// 串行回填 tool_result + 审计(短 lock push + 锁外 audit)
|
||||
for (draft, risk_level, outcome) in results {
|
||||
let (status, content) = match outcome {
|
||||
Ok(c) => ("completed", c),
|
||||
Err(c) => ("failed", c),
|
||||
};
|
||||
// F-260616-09 B 批2:写 per_conv.messages。
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, content.clone()));
|
||||
// 审计:trust 放行仍记一条(decided_by=auto_trust),留痕可追溯
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, content.clone()));
|
||||
}
|
||||
// 锁外:审计(trust 放行仍记一条,decided_by=auto_trust),留痕可追溯
|
||||
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, status, risk_level, Some(content), Some("auto_trust"), current_message_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Low 风险并行执行:execute + 即时 emit 在闭包内(不持 session 锁),
|
||||
// push tool_result / audit 在 join_all 后串行回填(持锁,与 Med/High 占位拼接)。
|
||||
// push tool_result / audit 在 join_all 后串行回填(短 lock push + 锁外 audit,与 Med/High 占位拼接)。
|
||||
// join_all 保序——结果顺序 = low_risk 输入顺序 = tc_list 原始 index 顺序,不额外 sort
|
||||
if !low_risk.is_empty() {
|
||||
// BUG-2026-07-07/P0-2:只读幂等工具去重缓存(治 LLM 死循环重调)。
|
||||
// 实测 9357c27c 会话:LLM 对 read_file(application.xml) 连调 17 次、search_files 连调 13 次,
|
||||
// 每次返回几乎相同结果却反复重调,是 prompt 64 万的直接元凶。此处先查会话内是否已对
|
||||
// 同参只读工具成功执行过,命中则直接回填缓存结果跳过真执行,断 LLM 失忆死循环。
|
||||
// 安全边界见 find_cached_readonly_result 文档(仅白名单只读工具 + 仅 completed 成功结果)。
|
||||
//
|
||||
// BUG-260624-03/P0 重构:find_cached_readonly_result 内部短 lock + 锁外 DB 查,本段不持锁。
|
||||
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 cached = find_cached_readonly_result(session_arc, conv_id, &audit_repo, &draft.name, &args).await;
|
||||
if let Some(cached_content) = cached {
|
||||
// 缓存命中:直接 push tool_result + 审计(decided_by=cache_hit 标记缓存来源),
|
||||
// 不走真执行 + 不重emit Started/Completed(避免误导前端工具又执行了一次)。
|
||||
// emit Completed 携带缓存结果供前端折叠卡片展示(与 find_cached_high_risk_result 一致)。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
result: serde_json::Value::String(cached_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, &cached_content));
|
||||
}
|
||||
audit_tool_call(
|
||||
&audit_repo, conv_id, &draft.id, &draft.name, &draft.args,
|
||||
"completed", risk_level, Some(cached_content), Some("cache_hit"),
|
||||
current_message_id,
|
||||
).await;
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
tool = %draft.name,
|
||||
tc_id = %draft.id,
|
||||
"[ai] 只读工具缓存命中,跳过真执行(治 LLM 死循环重调)"
|
||||
);
|
||||
} else {
|
||||
low_risk_uncached.push((draft, args, risk_level));
|
||||
}
|
||||
}
|
||||
let low_risk = low_risk_uncached;
|
||||
// 携带 args + 原始 JSON result(create_idea source 补全需解析 result.id + args.source)。
|
||||
let results: Vec<(ToolCallDraft, serde_json::Value, RiskLevel, Result<String, String>)> =
|
||||
futures::future::join_all(low_risk.into_iter().map(|(draft, args, risk_level)| {
|
||||
@@ -464,7 +583,7 @@ pub(crate) async fn process_tool_calls(
|
||||
}
|
||||
})).await;
|
||||
|
||||
// 串行回填 tool_result + 审计(持 session 锁)
|
||||
// 串行回填 tool_result + 审计(短 lock push + 锁外 audit)
|
||||
for (draft, raw_result, risk_level, outcome) in results {
|
||||
let (status, content) = match outcome {
|
||||
Ok(c) => ("completed", c),
|
||||
@@ -477,8 +596,11 @@ pub(crate) async fn process_tool_calls(
|
||||
let args_val = serde_json::from_str(&draft.args).unwrap_or(serde_json::Value::Null);
|
||||
maybe_fill_idea_source(db, &draft.name, &args_val, &raw_result, current_message_id).await;
|
||||
}
|
||||
// F-260616-09 B 批2:写 per_conv.messages。
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, content.clone()));
|
||||
// 短 lock 段:写 per_conv.messages(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, content.clone()));
|
||||
}
|
||||
// F-#97 审计留痕:low_risk 向量中 risk_level != Low 即 mode 放行(Med 仅 medium/all、High 仅 all 才进)。
|
||||
// decided_by 区分接管来源,审计表可追溯 all 模式执行了多少高危命令(治 securityReview blocker:
|
||||
// 原硬编码 RiskLevel::Low 致 Med/High 接管操作全标 Low/auto 失真,等于没记)。
|
||||
@@ -492,12 +614,18 @@ pub(crate) async fn process_tool_calls(
|
||||
}
|
||||
|
||||
// 阶段3a:单表 pending_approvals 统计 pending 总数(path/risk 合一,kind 区分)。
|
||||
let (path_count, risk_count) = session.pending_approvals.values()
|
||||
.fold((0usize, 0usize), |(p, r), a| match a.kind {
|
||||
ApprovalKind::Path(_) => (p + 1, r),
|
||||
ApprovalKind::Risk { .. } => (p, r + 1),
|
||||
});
|
||||
tracing::debug!(target: "ai_dirauth", pending_count, pending_approvals_len = session.pending_approvals.len(), path_count, risk_count, "process_tool_calls 返回(路径/风险挂起分布)");
|
||||
// 短 lock 段:读 pending_approvals 统计(纯读,无 await)
|
||||
let (path_count, risk_count, pending_approvals_len) = {
|
||||
let session = session_arc.lock().await;
|
||||
let len = session.pending_approvals.len();
|
||||
let (p, r) = session.pending_approvals.values()
|
||||
.fold((0usize, 0usize), |(p, r), a| match a.kind {
|
||||
ApprovalKind::Path(_) => (p + 1, r),
|
||||
ApprovalKind::Risk { .. } => (p, r + 1),
|
||||
});
|
||||
(p, r, len)
|
||||
};
|
||||
tracing::debug!(target: "ai_dirauth", pending_count, pending_approvals_len, path_count, risk_count, "process_tool_calls 返回(路径/风险挂起分布)");
|
||||
pending_count
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user