From a6428228d1a13c8cdf1b98d43eef718340445f15 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:51:52 +0800 Subject: [PATCH] =?UTF-8?q?Direct=20=E5=9B=9E=E5=90=88=20panic=20=E8=BD=AC?= =?UTF-8?q?=E6=88=90=E5=8F=AF=E8=AF=8A=E6=96=AD=E7=9A=84=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatch 层用 AssertUnwindSafe/catch_unwind 接住回合体 panic,不再落到 HostDropped 兜底 panic 负载转成直接可读文本并写进脱敏诊断与应用日志,终态改为分类失败 补齐 futures::FutureExt 导入 --- .../src/agent/thread_manager/dispatch.rs | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs index 4bfcadd2e..9e8a43357 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs @@ -13,13 +13,16 @@ use std::path::{Path, PathBuf}; +use futures::FutureExt; + #[cfg(test)] use crate::agent::PendingTurn; use crate::agent::{ append_direct_project_user_message_at, claim_pending_turn, complete_turn_if_reserved, - direct_codex_user_item_to_prompt, direct_now_ms, redact_agent_runtime_error, - run_direct_game_creator_turn_at_with_creation_type_and_emitter, thread_id_for_project, - DirectGameCreatorTurnUpdateEmitter, DirectTurnError, DirectTurnTerminal, DispatchedTurn, + direct_codex_user_item_to_prompt, direct_now_ms, record_direct_codex_failure, + redact_agent_runtime_error, run_direct_game_creator_turn_at_with_creation_type_and_emitter, + thread_id_for_project, DirectCodexFailureStage, DirectGameCreatorTurnUpdateEmitter, + DirectTurnError, DirectTurnTerminal, DispatchedTurn, }; /// 一次放行的占用。持有它就代表这一轮还没收口。 @@ -184,6 +187,17 @@ pub(crate) fn kick_queue_dispatch(root: &Path) { })); } +/// 把 panic 负载转成可读文本:`panic!("…")` 的负载是 `&str`,`panic!("{x}")` 是 `String`。 +fn direct_turn_panic_detail(payload: &(dyn std::any::Any + Send)) -> String { + if let Some(text) = payload.downcast_ref::<&str>() { + (*text).to_string() + } else if let Some(text) = payload.downcast_ref::() { + text.clone() + } else { + "未知 panic 负载".to_string() + } +} + /// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。 /// /// 这一段的每个失败都是**回合失败**:占用与开始事件已经写好,只走占用对象的终态出口,不再回到任何 @@ -218,16 +232,38 @@ async fn run_dispatched_direct_turn( crate::agent::codex_app_server::emit_direct_thread_user_item(&root, &canonical_user_item); let capture = crate::analytics::gui::capture_writer_context(); let emitter = DirectGameCreatorTurnUpdateEmitter::new(&root, turn_id); - let outcome = run_direct_game_creator_turn_at_with_creation_type_and_emitter( - &root, - &prompt, - dispatched.pending.creation_type.as_deref(), - Some(&emitter), - Some(canonical_user_item.clone()), - capture, - release_identity_generation, + // 回合体 panic 不能落到 `TurnReservation` 的 Drop 兜底:那会把这一轮收成"说不出原因"的 + // `HostDropped`,症状正是用户看到的「本轮执行已中断,请重试」,而项目侧诊断与应用日志 + // **一行都不会有**(真实案例:2026-10-02 连续两轮)。这里把 panic 转成分类失败, + // 走与正常失败同一条收口:先写脱敏诊断,再写终态。 + let outcome = match std::panic::AssertUnwindSafe( + run_direct_game_creator_turn_at_with_creation_type_and_emitter( + &root, + &prompt, + dispatched.pending.creation_type.as_deref(), + Some(&emitter), + Some(canonical_user_item.clone()), + capture, + release_identity_generation, + ), ) - .await; + .catch_unwind() + .await + { + Ok(result) => result, + Err(payload) => { + let failure = DirectTurnError::turn_failed( + DirectCodexFailureStage::CodeGeneration, + format!( + "DirectProject 宿主任务 panic:{}", + direct_turn_panic_detail(payload.as_ref()) + ), + ); + let stage = failure.turn_failure_stage(); + let detail = record_direct_codex_failure(&root, &failure, Some(turn_id.as_str())); + Err(DirectTurnError::TurnFailed { stage, detail }) + } + }; match outcome { Ok(_) => { // 深层的终态出口已经在 `run_turn` 里写出 `turn.completed`;这里只补最后一条回合更新。