diff --git a/apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs b/apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs new file mode 100644 index 000000000..6a78cd9a3 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs @@ -0,0 +1,98 @@ +//! 调试落盘:保存 LLM 原始输出 / 失败输入,便于排查截断、空返回等问题。 +//! +//! 本模块在 main.rs 用 `#[cfg(all(debug_assertions, not(test)))]` 声明——仅在开发(debug) +//! 且非测试构建中编入二进制;生产 release 与 cargo test 下整个模块与其调用处一并被剔除, +//! 不会往仓库写任何文件。 + +use super::*; + +// 找到仓库根(包含 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(super) fn persist_snapshot(raw_content: &str) { + let Some(repo_root) = repo_root() else { + eprintln!("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) { + eprintln!("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) { + eprintln!( + "llm.draft.snapshot.write.failed: {}: {error}", + path.display() + ); + return; + } + // 同步更新 latest.txt,方便直接打开最近一次草案。 + let latest = dir.join("latest.txt"); + let _ = fs::write(&latest, raw_content); + eprintln!("llm.draft.snapshot.saved: {}", path.display()); +} + +// LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt) +// 连同错误信息一起落盘到 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 +pub(super) fn persist_error_input(system_prompt: &str, user_prompt: &str, error: &str) { + let Some(repo_root) = repo_root() else { + eprintln!("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) { + eprintln!( + "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) { + eprintln!( + "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); + eprintln!("llm.draft.error-input.saved: {}", path.display()); +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 6b0efb188..360cbbad4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -3749,104 +3749,10 @@ fn parse_llm_game_draft_response(content: &str) -> Result .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) } -// 调试落盘:保存 LLM 原始输出 / 失败输入,便于排查截断、空返回等问题。 -// 整个模块用 #[cfg] 编译期门控——仅在开发(debug)且非测试构建中编入二进制; -// 生产 release 与 cargo test 下模块与调用处一并被剔除,不会往仓库写任何文件。 +// 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。 +// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。 #[cfg(all(debug_assertions, not(test)))] -mod debug_drafts { - use super::*; - - // 找到仓库根(包含 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(super) fn persist_snapshot(raw_content: &str) { - let Some(repo_root) = repo_root() else { - eprintln!("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) { - eprintln!("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) { - eprintln!( - "llm.draft.snapshot.write.failed: {}: {error}", - path.display() - ); - return; - } - // 同步更新 latest.txt,方便直接打开最近一次草案。 - let latest = dir.join("latest.txt"); - let _ = fs::write(&latest, raw_content); - eprintln!("llm.draft.snapshot.saved: {}", path.display()); - } - - // LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt) - // 连同错误信息一起落盘到 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 - pub(super) fn persist_error_input(system_prompt: &str, user_prompt: &str, error: &str) { - let Some(repo_root) = repo_root() else { - eprintln!("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) { - eprintln!( - "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) { - eprintln!( - "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); - eprintln!("llm.draft.error-input.saved: {}", path.display()); - } -} +mod debug_drafts; fn strip_llm_thinking_blocks(content: &str) -> String { let mut output = String::new();