From 73729394e22161e8658b4644ca8a54957b4853a6 Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 17 Sep 2026 09:27:48 +0000 Subject: [PATCH] =?UTF-8?q?=E7=AD=96=E5=88=92panic=E6=97=B6=E6=8A=8A?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BD=8D=E7=BD=AE=E5=92=8C=E8=B4=9F=E8=BD=BD?= =?UTF-8?q?=E5=86=99=E5=85=A5design=5Fdebug=20=E8=BF=90=E8=A1=8C=E6=AE=B5?= =?UTF-8?q?=E7=BB=8Ftokio=20task-local=E6=90=BA=E5=B8=A6=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=A0=B9=EF=BC=8C=E5=A4=9A=E7=BA=BF=E7=A8=8Bruntime=E8=B7=A8wo?= =?UTF-8?q?rker=E8=BF=81=E7=A7=BB=E4=B9=9F=E8=83=BD=E5=BD=92=E5=9B=A0=20?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E8=BF=90=E8=A1=8C=E5=AE=89=E8=A3=85panic=20h?= =?UTF-8?q?ook=EF=BC=88take=5Fhook=E5=8C=85=E8=A3=85=E5=8E=9Fhook=EF=BC=8C?= =?UTF-8?q?update=5Fhook=E5=9C=A81.96=E4=BB=8Dunstable=EF=BC=89=20hook?= =?UTF-8?q?=E4=BB=85=E5=9C=A8=E7=AD=96=E5=88=92=E8=BF=90=E8=A1=8C=E6=AE=B5?= =?UTF-8?q?=E5=86=85=E7=94=9F=E6=95=88=EF=BC=8C=E5=85=B6=E5=AE=83=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=9B=B4=E6=8E=A5=E9=80=8F=E4=BC=A0=E5=8E=9Fhook=20de?= =?UTF-8?q?sign=5Fpanic=5Ferror=E5=8F=AA=E8=BF=94=E5=9B=9E=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E5=85=AC=E5=BC=80=E6=96=87=E6=A1=88=EF=BC=8Cdebug?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=94=B1hook=E7=BB=9F=E4=B8=80=E5=86=99?= =?UTF-8?q?=E5=85=A5=20=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=E6=96=AD?= =?UTF-8?q?=E8=A8=80panic=E8=AE=B0=E5=BD=95=E8=90=BD=E7=9B=98=E4=B8=94?= =?UTF-8?q?=E5=8C=85=E5=90=ABdesign=5Fruntime.rs=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8E=E6=B3=A8=E5=85=A5payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent/design_runtime.rs | 85 ++++++++++++++++--- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index c81f84ec1..fbb42a22a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -15,6 +15,43 @@ const DESIGN_ACTIVE_LOCK: &str = ".agent/design-agent/active.lock"; const DESIGN_PANIC_PUBLIC_ERROR: &str = "策划运行发生内部错误,本轮已中断,可直接重试;若反复出现请反馈。"; +// 运行段经 task-local 携带项目根,panic hook 据此把位置和负载写进私有 design_debug。 +// task-local 而非 thread-local:多线程 runtime 下 future 会跨 worker 迁移。 +tokio::task_local! { + static DESIGN_PANIC_ROOT: Option; +} + +fn ensure_design_panic_hook() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + if let Ok(Some(root)) = DESIGN_PANIC_ROOT.try_with(Clone::clone) { + let location = info + .location() + .map(|location| { + format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + }) + .unwrap_or_else(|| "未知位置".to_string()); + design_debug( + &root, + "panic", + json!({ + "location": location, + "error": info.payload_as_str().unwrap_or("未知 panic 负载"), + }), + ); + } + previous(info); + })); + }); +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde( tag = "type", @@ -982,13 +1019,15 @@ async fn finish_design_command( // panic 边界:运行期 panic 转成普通失败,交给既有错误分支恢复(重读检查点、 // 写 last_error、发最终 view)。否则 unwind 会杀死 command task,IPC 永不 // 返回(前端停在工作态),会话停在无错误的 pending,用户只能看到无声的重试。 - let outcome = - std::panic::AssertUnwindSafe(run_design_loop(root, resources, &mut session, &mut emit)) - .catch_unwind() - .await; + ensure_design_panic_hook(); + let run = DESIGN_PANIC_ROOT.scope( + Some(root.to_path_buf()), + run_design_loop(root, resources, &mut session, &mut emit), + ); + let outcome = std::panic::AssertUnwindSafe(run).catch_unwind().await; let result = match outcome { Ok(result) => result, - Err(payload) => Err(design_panic_error(root, payload)), + Err(payload) => Err(design_panic_error(payload)), }; if let Err(error) = result { // 从最后一个持久检查点恢复,防止写后未记结果被误认为已完成。 @@ -1010,14 +1049,9 @@ async fn finish_design_command( Ok(view) } -// panic 负载可能包含路径或内容片段,公开文案固定;细节只进私有 design_debug。 -fn design_panic_error(root: &Path, payload: Box) -> String { - let detail = payload - .downcast_ref::<&str>() - .map(|value| (*value).to_string()) - .or_else(|| payload.downcast_ref::().cloned()) - .unwrap_or_else(|| "未知 panic 负载".to_string()); - design_debug(root, "panic", json!({"error": detail})); +// panic 负载可能包含路径或内容片段,公开文案固定;位置和负载由 panic hook 写进私有 +// design_debug(task-local 提供项目根),不进入用户可见消息。 +fn design_panic_error(_payload: Box) -> String { DESIGN_PANIC_PUBLIC_ERROR.to_string() } @@ -1665,6 +1699,7 @@ mod tests { #[tokio::test(flavor = "current_thread")] async fn design_tool_panic_becomes_visible_retryable_error() { let (_temp, root, resources) = init_design_project(); + std::env::set_var("GENARRATIVE_AGC_DESIGN_DEBUG", "1"); let panic_call = platform_llm::LlmToolCall { id: "call-panic".into(), name: "design_test__panic".into(), @@ -1705,6 +1740,30 @@ mod tests { "panic 不得写半个工具输出" ); + // design_debug 经独立线程落盘,轮询等待 panic 记录出现。 + let debug_dir = root.join(".debug/design-agent"); + let mut panic_record = None; + for _ in 0..100 { + panic_record = fs::read_dir(&debug_dir).ok().and_then(|entries| { + entries.flatten().find_map(|entry| { + let path = entry.path(); + let name = path.file_name()?.to_string_lossy().into_owned(); + if name.ends_with("-panic.json") { + fs::read_to_string(path).ok() + } else { + None + } + }) + }); + if panic_record.is_some() { + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + let panic_record = panic_record.expect("panic hook 必须把位置和负载写入 design_debug"); + assert!(panic_record.contains("design_runtime.rs")); + assert!(panic_record.contains("注入的策划工具 panic")); + let view = continue_design_agent_at(&root, &resources, "turn-retry", DesignInput::Retry, |_| {}) .await diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index c505ceb32..0828b8fda 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -8,6 +8,8 @@ `finish_design_command` 的运行段包在 `catch_unwind` 边界内:任何运行期 panic 被转成一次普通失败,由既有错误分支从最后一个持久检查点恢复,向会话写入固定公开文案"策划运行发生内部错误,本轮已中断,可直接重试;若反复出现请反馈。",并以 `running=false、可重试、lastError 有值` 的视图正常返回。panic 负载只写入私有 design_debug,不进入用户可见消息;continue、recover_uncertain 与 decide 三个入口共享同一边界。边界不改变工具错误、Provider 瞬态重试和批次不确定恢复的既有语义。 +运行段通过 task-local 携带项目根;首次运行时安装的 panic hook 在 panic 瞬间把代码位置(file:line:column)和负载追加写入私有 design_debug,随后交给原 hook 维持既有 stderr 输出。hook 只在策划运行段内生效(其它任务无 task-local 上下文时直接透传),多线程 runtime 下 future 跨 worker 迁移也能正确归因。 + ## 资源画布交互与工作台状态同步 - 工作台向窗口标题栏发布正在运行的项目时,输入未变化不得形成重复发布与清理的渲染循环;打开项目动作始终使用当前工作台处理逻辑,退出工作台后清除其标题栏状态。