//! 调试落盘:保存 LLM 原始输出 / 失败输入,便于排查截断、空返回等问题。 //! //! 本模块用 `#[cfg(all(debug_assertions, not(test)))]` 声明——仅在开发(debug) //! 且非测试构建中编入二进制;生产 release 与 cargo test 下整个模块与其调用处一并被剔除, //! 不会往仓库写任何文件。 use crate::unix_millis; use std::fs; use std::path::PathBuf; // 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录), // 以便把草案落到仓库内而非 tmp 项目目录。 fn repo_root() -> Option { let mut roots = Vec::new(); if let Ok(cwd) = std::env::current_dir() { roots.push(cwd); } if let Ok(exe) = std::env::current_exe() { if let Some(parent) = exe.parent() { roots.push(parent.to_path_buf()); } } for root in roots { for directory in root.ancestors().take(8) { let marker = directory .join("apps") .join("ai-game-creator-shell") .join("src-tauri") .join("Cargo.toml"); if marker.is_file() { return Some(directory.to_path_buf()); } } } None } // 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 .llm-drafts/, // 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断主流程。 pub(crate) fn persist_snapshot(raw_content: &str) { let Some(repo_root) = repo_root() else { app_log!("llm.draft.snapshot.skip: 未能定位仓库根目录"); return; }; let dir = repo_root .join("apps") .join("ai-game-creator-shell") .join(".llm-drafts"); if let Err(error) = fs::create_dir_all(&dir) { app_log!("llm.draft.snapshot.dir.failed: {}: {error}", dir.display()); return; } let path = dir.join(format!("draft-{}.txt", unix_millis())); if let Err(error) = fs::write(&path, raw_content) { app_log!( "llm.draft.snapshot.write.failed: {}: {error}", path.display() ); return; } // 同步更新 latest.txt,方便直接打开最近一次草案。 let latest = dir.join("latest.txt"); let _ = fs::write(&latest, raw_content); app_log!("llm.draft.snapshot.saved: {}", path.display()); } // LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt) // 连同错误信息一起落盘到 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 pub(crate) fn persist_error_input(system_prompt: &str, user_prompt: &str, error: &str) { let Some(repo_root) = repo_root() else { app_log!("llm.draft.error-input.skip: 未能定位仓库根目录"); return; }; let dir = repo_root .join("apps") .join("ai-game-creator-shell") .join(".llm-drafts"); if let Err(io_error) = fs::create_dir_all(&dir) { app_log!( "llm.draft.error-input.dir.failed: {}: {io_error}", dir.display() ); return; } let body = format!( "# LLM 生成失败输入快照\n\n## 错误\n{error}\n\n## System Prompt\n{system_prompt}\n\n## User Prompt(含用户需求/短长期记忆/spec/agenda/组 brief/findings)\n{user_prompt}\n", ); let path = dir.join(format!("error-input-{}.txt", unix_millis())); if let Err(io_error) = fs::write(&path, &body) { app_log!( "llm.draft.error-input.write.failed: {}: {io_error}", path.display() ); return; } // 同步更新 latest-error-input.txt,方便直接打开最近一次失败输入。 let latest = dir.join("latest-error-input.txt"); let _ = fs::write(&latest, &body); app_log!("llm.draft.error-input.saved: {}", path.display()); }