修复: CR-51 trust-hit持锁阻塞IPC(audit execute移锁外spawn+回填持锁)

This commit is contained in:
2026-06-17 02:04:01 +08:00
parent b6fe405431
commit 332a6bba1a

View File

@@ -555,6 +555,10 @@ pub(crate) async fn process_tool_calls(
// 把缓存结果作为新 tool_call_id 的 tool_result 回传 LLM跳过 insert pending + 跳过审批, // 把缓存结果作为新 tool_call_id 的 tool_result 回传 LLM跳过 insert pending + 跳过审批,
// 断「超时→重试→重新审批」循环。Med 不去重去重易误伤Low 无审批本就不进此分支。 // 断「超时→重试→重新审批」循环。Med 不去重去重易误伤Low 无审批本就不进此分支。
let mut low_risk: Vec<(ToolCallDraft, serde_json::Value)> = Vec::new(); let mut low_risk: Vec<(ToolCallDraft, serde_json::Value)> = Vec::new();
// trust_hits 收集AE-04 会话信任命中(同会话已批准同类操作),真实执行但移到锁外 spawn
// (对齐 Low risk L673-720 不持锁模式)。命中时此处只 emit toast + 收集,不 .await execute。
// 元组携带 risk_level: 回填审计需原等级(Med/High 才进此分支),避免闭包内再查 registry
let mut trust_hits: Vec<(ToolCallDraft, serde_json::Value, String, RiskLevel)> = Vec::new();
for (_, draft, args) in drafts { for (_, draft, args) in drafts {
let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High); let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High);
match risk_level { match risk_level {
@@ -584,33 +588,16 @@ pub(crate) async fn process_tool_calls(
"[AE-2025-04] 会话信任命中: 同会话已批准同类操作,自动放行(跳过审批+二次确认)" "[AE-2025-04] 会话信任命中: 同会话已批准同类操作,自动放行(跳过审批+二次确认)"
); );
// emit 轻量 toast 事件(前端 AiChat.vue 显示"🔓 自动放行: tool(dir)") // emit 轻量 toast 事件(前端 AiChat.vue 显示"🔓 自动放行: tool(dir)")
// toast 即时反馈,锁内 emit 不阻塞
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiToolAutoApproved { let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiToolAutoApproved {
id: draft.id.clone(), id: draft.id.clone(),
tool: draft.name.clone(), tool: draft.name.clone(),
dir: dir_label, dir: dir_label.clone(),
conversation_id: Some(conv_id.to_string()), conversation_id: Some(conv_id.to_string()),
}); });
// 真实执行工具(在 session 锁外,与 Low risk 并行执行闭包同款不持锁) // 收集后循环外 spawn 执行,对齐 Low risk 不持锁(原注释 L593 声称"锁外"但代码持锁,
let exec_args = args.clone(); // run_command 慢命令会阻塞同会话所有触 state.ai_session 的 IPC,CR-51 修此)
let exec_tool = draft.name.clone(); trust_hits.push((draft, args, dir_label, risk_level));
let exec_result = tools_arc.execute(&exec_tool, exec_args).await;
let (status, content) = match &exec_result {
Ok(val) => ("completed", val.to_string()),
Err(e) => ("failed", e.to_string()),
};
session.messages.push(ChatMessage::tool_result(&draft.id, content.clone()));
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted {
id: draft.id.clone(),
result: serde_json::Value::String(content.clone()),
conversation_id: Some(conv_id.to_string()),
});
// AR-11数据变更类工具执行成功后 emit df-data-changed 联动刷新
// (write_file/run_command 不在 data_change_for_tool 映射内,emit_data_changed 内部 None 即 noop)
if exec_result.is_ok() {
emit_data_changed(app_handle, &draft.name);
}
// 审计: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")).await;
continue; continue;
} }
// F-05仅 High 查去重缓存Med 保持原审批流程 // F-05仅 High 查去重缓存Med 保持原审批流程
@@ -667,6 +654,57 @@ pub(crate) async fn process_tool_calls(
} }
} }
// AE-04 trust-hit 并行执行execute + 即时 emit 在闭包内(不持 session 锁),
// push tool_result / audit 在 join_all 后串行回填(持锁)。对齐 Low risk 不持锁模式。
// CR-51 修:原 trust-hit 分支 .await execute 时 session 仍 in scope持锁跨整个函数
// run_command 慢命令阻塞同会话所有触 state.ai_session 的 IPC。
// join_all 保序——结果顺序 = trust_hits 输入顺序 = tc_list 原始 index 顺序,不额外 sort
if !trust_hits.is_empty() {
let results: Vec<(ToolCallDraft, RiskLevel, Result<String, String>)> =
futures::future::join_all(trust_hits.into_iter().map(|(draft, args, _dir_label, risk_level)| {
let tools = tools_arc.clone();
let app_clone = app_handle.clone();
let conv_clone = conv_id.to_string();
async move {
let exec_result = tools.execute(&draft.name, args).await;
match exec_result {
Ok(val) => {
let content = val.to_string();
let _ = app_clone.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted {
id: draft.id.clone(),
result: serde_json::Value::String(content.clone()),
conversation_id: Some(conv_clone),
});
// AR-11数据变更类工具执行成功后 emit df-data-changed 联动刷新
// (write_file/run_command 不在 data_change_for_tool 映射内,emit_data_changed 内部 None 即 noop)
emit_data_changed(&app_clone, &draft.name);
(draft, risk_level, Ok(content))
}
Err(e) => {
let content = e.to_string();
let _ = app_clone.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted {
id: draft.id.clone(),
result: serde_json::Value::String(content.clone()),
conversation_id: Some(conv_clone),
});
(draft, risk_level, Err(content))
}
}
}
})).await;
// 串行回填 tool_result + 审计(持 session 锁)
for (draft, risk_level, outcome) in results {
let (status, content) = match outcome {
Ok(c) => ("completed", c),
Err(c) => ("failed", c),
};
session.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")).await;
}
}
// Low 风险并行执行execute + 即时 emit 在闭包内(不持 session 锁), // Low 风险并行执行execute + 即时 emit 在闭包内(不持 session 锁),
// push tool_result / audit 在 join_all 后串行回填(持锁,与 Med/High 占位拼接)。 // push tool_result / audit 在 join_all 后串行回填(持锁,与 Med/High 占位拼接)。
// join_all 保序——结果顺序 = low_risk 输入顺序 = tc_list 原始 index 顺序,不额外 sort // join_all 保序——结果顺序 = low_risk 输入顺序 = tc_list 原始 index 顺序,不额外 sort