修复: 工具执行无心跳致流中断误报
This commit is contained in:
@@ -248,6 +248,60 @@ async fn detect_retry_count(audit_repo: &AiToolExecutionRepo, tc_id: &str) -> u3
|
||||
|
||||
/// 处理流式接收的工具调用:Low 风险并行执行(join_all),Med/High 进审批门控
|
||||
/// 返回待审批的工具数量(0 = 全部自动执行完成)
|
||||
/// 工具执行 + 心跳保活:execute 期间每 30s emit AiHeartbeat(对齐 stream_recv.rs
|
||||
/// stream_llm select! 心跳语义),execute 完 abort 心跳 task。
|
||||
///
|
||||
/// 根治 BUG-260624-03:工具执行在 stream_llm 之外(本模块),原本无 AiHeartbeat。
|
||||
/// 单次 execute 超过前端 STREAM_TIMEOUT_MS(130s)——bash 跑 cargo/测试、read 大文件、
|
||||
/// 全盘 search 等开发长命令——前端 watchdog 误判断流,抛"工具已执行完成后续中断"误报
|
||||
/// (实测:用户报"一边流一边抛",前一轮 delta 文本在屏 + 当前轮工具执行静默 > 130s)。
|
||||
/// 补工具执行阶段的心跳缺口,前端 watchdog 在静默期也能收到 reset,不再误杀长工具。
|
||||
async fn execute_with_heartbeat(
|
||||
tools: &AiToolRegistry,
|
||||
name: &str,
|
||||
args: serde_json::Value,
|
||||
app: &AppHandle,
|
||||
conv_id: &str,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::time::Duration;
|
||||
|
||||
// RAII guard:drop 时 stop+abort 心跳 task,确保 execute panic(unwind)也不泄漏心跳 task。
|
||||
struct HeartbeatGuard {
|
||||
stop: Arc<AtomicBool>,
|
||||
handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
impl Drop for HeartbeatGuard {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::SeqCst);
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let app_h = app.clone();
|
||||
let conv = conv_id.to_string();
|
||||
let stop_c = stop.clone();
|
||||
// 心跳 task:弃首 tick(tokio interval 首 tick 立即返回,对齐 stream_recv.rs:174),
|
||||
// 后每 30s emit AiHeartbeat(< 130s watchdog 确保覆盖)。
|
||||
let heartbeat = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_c.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let ev = AiChatEvent::AiHeartbeat { conversation_id: Some(conv.clone()) };
|
||||
let _ = app_h.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_h.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
}
|
||||
});
|
||||
// RAII:execute 无论 Ok/Err/panic(unwind),_guard drop 自动 stop+abort,无心跳 task 泄漏。
|
||||
let _guard = HeartbeatGuard { stop, handle: heartbeat };
|
||||
tools.execute(name, args).await
|
||||
}
|
||||
|
||||
pub(crate) async fn process_tool_calls(
|
||||
session: &mut AiSession,
|
||||
tool_calls_acc: HashMap<u32, ToolCallDraft>,
|
||||
@@ -601,7 +655,7 @@ pub(crate) async fn process_tool_calls(
|
||||
let app_clone = app_handle.clone();
|
||||
let conv_clone = conv_id.to_string();
|
||||
async move {
|
||||
let exec_result = tools.execute(&draft.name, args).await;
|
||||
let exec_result = execute_with_heartbeat(&tools, &draft.name, args, &app_clone, &conv_clone).await;
|
||||
match exec_result {
|
||||
Ok(val) => {
|
||||
let content = val.to_string();
|
||||
@@ -658,7 +712,7 @@ pub(crate) async fn process_tool_calls(
|
||||
let app_clone = app_handle.clone();
|
||||
let conv_clone = conv_id.to_string();
|
||||
async move {
|
||||
let result = tools.execute(&draft.name, args).await;
|
||||
let result = execute_with_heartbeat(&tools, &draft.name, args, &app_clone, &conv_clone).await;
|
||||
match result {
|
||||
Ok(val) => {
|
||||
// L3 emit 双写:Low 风险工具执行成功 emit Completed 双路发布。
|
||||
|
||||
Reference in New Issue
Block a user