From 5378b67dd18e457512ebd535254c2801549c60ae Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 19:51:36 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E7=AA=97=E5=8F=A3token=E6=95=B0=E9=99=90=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E6=88=AA=E6=96=AD=E5=AF=BC=E8=87=B4=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=87=BA=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/main.rs | 140 ++++++++++++++++-- 1 file changed, 126 insertions(+), 14 deletions(-) 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 87c49778f..52901e8f4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -342,7 +342,7 @@ const DEFAULT_GAME_INDEX_HTML: &str = r#" "#; const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000"; -const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 5000; +const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000; const GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS: u32 = 900; const GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 180_000; const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3; @@ -1657,22 +1657,35 @@ async fn request_generator_game_draft_with_client( group_briefs_markdown: &str, agenda_markdown: &str, ) -> Result { + let system_prompt = game_creator_system_prompt(); + let user_prompt = game_creator_generator_user_prompt( + prompt, + short_memory, + long_memory, + spec_markdown, + findings_markdown, + group_briefs_markdown, + agenda_markdown, + ); let request = LlmTextRequest::new(vec![ - LlmMessage::system(game_creator_system_prompt()), - LlmMessage::user(game_creator_generator_user_prompt( - prompt, - short_memory, - long_memory, - spec_markdown, - findings_markdown, - group_briefs_markdown, - agenda_markdown, - )), + LlmMessage::system(system_prompt), + LlmMessage::user(user_prompt.clone()), ]) .with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS); - let response = request_game_creator_llm_text(client, request) - .await - .map_err(|error| format!("LLM 生成失败:{error}"))?; + let response = match request_game_creator_llm_text(client, request).await { + Ok(response) => response, + Err(error) => { + // LLM 调用失败(如 “LLM 返回内容为空”)时,把本次输入连同错误一并落盘,便于复现定位。 + persist_llm_game_draft_error_input( + system_prompt, + user_prompt.as_str(), + &error.to_string(), + ); + return Err(format!("LLM 生成失败:{error}")); + } + }; + // 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文。 + persist_llm_game_draft_snapshot(response.content.as_str()); let content = strip_llm_thinking_blocks(response.content.as_str()); parse_llm_game_draft_response(content.as_str()) } @@ -3573,6 +3586,105 @@ fn parse_llm_game_draft_response(content: &str) -> Result .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) } +// 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录), +// 复用本地 env 那套向上查找逻辑,以便把原始 LLM 草案落到仓库内而非 tmp 项目目录。 +fn game_creator_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 +} + +// 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 apps/ai-game-creator-shell/.llm-drafts/, +// 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断生成主流程。 +fn persist_llm_game_draft_snapshot(raw_content: &str) { + // 单元测试同样会走生成路径,避免 cargo test 往仓库写快照文件。 + if cfg!(test) { + return; + } + let Some(repo_root) = game_creator_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/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 +fn persist_llm_game_draft_error_input(system_prompt: &str, user_prompt: &str, error: &str) { + // 单元测试同样会走生成路径,避免 cargo test 往仓库写文件。 + if cfg!(test) { + return; + } + let Some(repo_root) = game_creator_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()); +} + fn strip_llm_thinking_blocks(content: &str) -> String { let mut output = String::new(); let mut rest = content; -- 2.52.0 From 525a4c874bff46ae05a9fd73cff6da645803f95c Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 19:52:01 +0800 Subject: [PATCH 2/8] add ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b28197188..c0413a2f4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ temp*build*/ /apps/ai-game-creator-shell/src-tauri/gen/ /apps/ai-game-creator-shell/src-tauri/logs/ /apps/ai-game-creator-shell/logs/ +/apps/ai-game-creator-shell/.llm-drafts/ /apps/mobile-shell/.expo/ /apps/mobile-shell/.expo-export-smoke/ /server-rs/.spacetimedb/ -- 2.52.0 From f994c82d8c0f73aa6236a260202b446563aabc95 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 20:37:28 +0800 Subject: [PATCH 3/8] =?UTF-8?q?chore:=20=E5=8F=96=E6=B6=88=E8=B7=9F?= =?UTF-8?q?=E8=B8=AA=20.claude/settings.local.json=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 ++++- .gitignore | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 31b5b2e81..8136d39f6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,10 @@ "Bash(findstr LISTENING)", "Bash(npx tsc:*)", "Bash(lsof -ti:8081)", - "Bash(curl -s http://localhost:8081/health)" + "Bash(curl -s http://localhost:8081/health)", + "Bash(npm -v)", + "Bash(cargo --version)", + "Bash(rustc --version)" ] } } diff --git a/.gitignore b/.gitignore index c0413a2f4..633a781d7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ temp*build*/ .worktrees/ .rag/ .env.secrets.local +.claude/settings.local.json spacetime.local.json deploy/container/api-server.env deploy/container/worker-smoke/ -- 2.52.0 From 2258013d92183236d59587f7d83d115cfc18c679 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 20:38:13 +0800 Subject: [PATCH 4/8] add retry when llm output is null --- .../src-tauri/src/main.rs | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) 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 536c749f6..19dcf1467 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -1439,6 +1439,7 @@ async fn request_llm_game_draft_with_client( "", "", "", + None, ) .await } @@ -1606,6 +1607,7 @@ async fn run_game_creator_agent_loop_at( &findings_markdown, &group_briefs_markdown, &read_optional_text(&root.join(&agenda.relative_path))?, + progress, ) .await { @@ -1788,6 +1790,7 @@ async fn request_generator_game_draft_with_client( findings_markdown: &str, group_briefs_markdown: &str, agenda_markdown: &str, + progress: Option<&AgentProgressEmitter<'_>>, ) -> Result { let system_prompt = game_creator_system_prompt(); let user_prompt = game_creator_generator_user_prompt( @@ -1799,21 +1802,43 @@ async fn request_generator_game_draft_with_client( group_briefs_markdown, agenda_markdown, ); - let request = LlmTextRequest::new(vec![ - LlmMessage::system(system_prompt), - LlmMessage::user(user_prompt.clone()), - ]) - .with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS); - let response = match request_game_creator_llm_text(client, request).await { - Ok(response) => response, - Err(error) => { - // LLM 调用失败(如 “LLM 返回内容为空”)时,把本次输入连同错误一并落盘,便于复现定位。 - persist_llm_game_draft_error_input( - system_prompt, - user_prompt.as_str(), - &error.to_string(), - ); - return Err(format!("LLM 生成失败:{error}")); + // deepseek-v4-pro 等推理模型偶发返回空 content(HTTP 200、completion_tokens=0, + // 参见 DeepSeek-V3 issue #1453)。这类空返回是上游瞬时故障,原样重发通常即可恢复, + // 因此仅对 EmptyResponse 最多重试 3 次(含首次共 4 次请求),其它错误不重试。 + const MAX_EMPTY_RETRIES: u32 = 3; + let mut empty_retries = 0u32; + let response = loop { + let request = LlmTextRequest::new(vec![ + LlmMessage::system(system_prompt), + LlmMessage::user(user_prompt.clone()), + ]) + .with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS); + match request_game_creator_llm_text(client, request).await { + Ok(response) => break response, + Err(platform_llm::LlmError::EmptyResponse) if empty_retries < MAX_EMPTY_RETRIES => { + empty_retries += 1; + eprintln!( + "llm.chat.generator.empty-response 重试 {empty_retries}/{MAX_EMPTY_RETRIES}(上游返回空 content,原样重发)" + ); + // 同步推送到 App 进度面板,便于在界面上看到重试(无需盯命令行)。 + emit_agent_progress( + progress, + "llm.generator.empty_retry", + &format!( + "Generator 收到空返回(DeepSeek 偶发),正在自动重试 {empty_retries}/{MAX_EMPTY_RETRIES}" + ), + ); + continue; + } + Err(error) => { + // 调用失败(含空返回重试耗尽)时,把本次输入连同错误一并落盘,便于复现定位。 + persist_llm_game_draft_error_input( + system_prompt, + user_prompt.as_str(), + &error.to_string(), + ); + return Err(format!("LLM 生成失败:{error}")); + } } }; // 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文。 -- 2.52.0 From 5f9f5e19eeeef9773bb2891f313a2736ba0ecb4e Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 21:03:21 +0800 Subject: [PATCH 5/8] =?UTF-8?q?chore:=20=E8=B0=83=E8=AF=95=E8=90=BD?= =?UTF-8?q?=E7=9B=98=E4=BB=85=E9=99=90=20debug=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=EF=BC=8C=E7=94=9F=E4=BA=A7=E4=B8=8D=E5=86=99=20.llm-drafts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ai-game-creator-shell/src-tauri/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 19dcf1467..23164d942 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -3777,8 +3777,8 @@ fn game_creator_repo_root() -> Option { // 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 apps/ai-game-creator-shell/.llm-drafts/, // 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断生成主流程。 fn persist_llm_game_draft_snapshot(raw_content: &str) { - // 单元测试同样会走生成路径,避免 cargo test 往仓库写快照文件。 - if cfg!(test) { + // 仅在开发/调试构建落盘排查;生产(release)构建与单元测试一律跳过,不往仓库写快照文件。 + if cfg!(test) || !cfg!(debug_assertions) { return; } let Some(repo_root) = game_creator_repo_root() else { @@ -3810,8 +3810,8 @@ fn persist_llm_game_draft_snapshot(raw_content: &str) { // LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt) // 连同错误信息一起落盘到仓库 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 fn persist_llm_game_draft_error_input(system_prompt: &str, user_prompt: &str, error: &str) { - // 单元测试同样会走生成路径,避免 cargo test 往仓库写文件。 - if cfg!(test) { + // 仅在开发/调试构建落盘排查;生产(release)构建与单元测试一律跳过,不往仓库写文件。 + if cfg!(test) || !cfg!(debug_assertions) { return; } let Some(repo_root) = game_creator_repo_root() else { -- 2.52.0 From 48424382cea81a336c10db54b166cb228e20fdb5 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 21:27:44 +0800 Subject: [PATCH 6/8] fix save function entrance --- .../src-tauri/src/main.rs | 186 +++++++++--------- 1 file changed, 94 insertions(+), 92 deletions(-) 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 23164d942..6b0efb188 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -1831,8 +1831,9 @@ async fn request_generator_game_draft_with_client( continue; } Err(error) => { - // 调用失败(含空返回重试耗尽)时,把本次输入连同错误一并落盘,便于复现定位。 - persist_llm_game_draft_error_input( + // 调用失败(含空返回重试耗尽)时,把本次输入连同错误一并落盘,便于复现定位(仅 debug 构建)。 + #[cfg(all(debug_assertions, not(test)))] + debug_drafts::persist_error_input( system_prompt, user_prompt.as_str(), &error.to_string(), @@ -1841,8 +1842,9 @@ async fn request_generator_game_draft_with_client( } } }; - // 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文。 - persist_llm_game_draft_snapshot(response.content.as_str()); + // 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文(仅 debug 构建)。 + #[cfg(all(debug_assertions, not(test)))] + debug_drafts::persist_snapshot(response.content.as_str()); let content = strip_llm_thinking_blocks(response.content.as_str()); parse_llm_game_draft_response(content.as_str()) } @@ -3747,103 +3749,103 @@ fn parse_llm_game_draft_response(content: &str) -> Result .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) } -// 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录), -// 复用本地 env 那套向上查找逻辑,以便把原始 LLM 草案落到仓库内而非 tmp 项目目录。 -fn game_creator_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()); +// 调试落盘:保存 LLM 原始输出 / 失败输入,便于排查截断、空返回等问题。 +// 整个模块用 #[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); } - } - 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()); + 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 } - None -} -// 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 apps/ai-game-creator-shell/.llm-drafts/, -// 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断生成主流程。 -fn persist_llm_game_draft_snapshot(raw_content: &str) { - // 仅在开发/调试构建落盘排查;生产(release)构建与单元测试一律跳过,不往仓库写快照文件。 - if cfg!(test) || !cfg!(debug_assertions) { - return; + // 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 .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()); } - let Some(repo_root) = game_creator_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/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 -fn persist_llm_game_draft_error_input(system_prompt: &str, user_prompt: &str, error: &str) { - // 仅在开发/调试构建落盘排查;生产(release)构建与单元测试一律跳过,不往仓库写文件。 - if cfg!(test) || !cfg!(debug_assertions) { - return; - } - let Some(repo_root) = game_creator_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() + // 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", ); - return; + 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()); } - 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()); } fn strip_llm_thinking_blocks(content: &str) -> String { -- 2.52.0 From 43cf689f78921bb1e65c0e91f832b920dc425f3d Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 26 Jun 2026 21:42:40 +0800 Subject: [PATCH 7/8] move debug function to new file --- .../src-tauri/src/debug_drafts.rs | 98 +++++++++++++++++ .../src-tauri/src/main.rs | 100 +----------------- 2 files changed, 101 insertions(+), 97 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs 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(); -- 2.52.0 From c7c29dadb589e38db87acca7fc66f3cc1c0a197f Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 26 Jun 2026 22:00:48 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E8=90=BD=E7=9B=98=E6=A8=A1=E5=9D=97=EF=BC=9A=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=20debug=20=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ai-game-creator-shell/src-tauri/src/debug.rs | 3 +++ .../src-tauri/src/{ => debug}/debug_drafts.rs | 10 ++++++---- apps/ai-game-creator-shell/src-tauri/src/main.rs | 14 +++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/src/debug.rs rename apps/ai-game-creator-shell/src-tauri/src/{ => debug}/debug_drafts.rs (93%) diff --git a/apps/ai-game-creator-shell/src-tauri/src/debug.rs b/apps/ai-game-creator-shell/src-tauri/src/debug.rs new file mode 100644 index 000000000..8172834ff --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/debug.rs @@ -0,0 +1,3 @@ +pub(crate) mod debug_drafts; + +pub(crate) use debug_drafts::*; \ No newline at end of file diff --git a/apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs b/apps/ai-game-creator-shell/src-tauri/src/debug/debug_drafts.rs similarity index 93% rename from apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs rename to apps/ai-game-creator-shell/src-tauri/src/debug/debug_drafts.rs index 6a78cd9a3..8e0d606a3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/debug_drafts.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/debug/debug_drafts.rs @@ -1,10 +1,12 @@ //! 调试落盘:保存 LLM 原始输出 / 失败输入,便于排查截断、空返回等问题。 //! -//! 本模块在 main.rs 用 `#[cfg(all(debug_assertions, not(test)))]` 声明——仅在开发(debug) +//! 本模块用 `#[cfg(all(debug_assertions, not(test)))]` 声明——仅在开发(debug) //! 且非测试构建中编入二进制;生产 release 与 cargo test 下整个模块与其调用处一并被剔除, //! 不会往仓库写任何文件。 -use super::*; +use std::fs; +use std::path::PathBuf; +use crate::unix_millis; // 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录), // 以便把草案落到仓库内而非 tmp 项目目录。 @@ -35,7 +37,7 @@ fn repo_root() -> Option { // 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 .llm-drafts/, // 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断主流程。 -pub(super) fn persist_snapshot(raw_content: &str) { +pub(crate) fn persist_snapshot(raw_content: &str) { let Some(repo_root) = repo_root() else { eprintln!("llm.draft.snapshot.skip: 未能定位仓库根目录"); return; @@ -64,7 +66,7 @@ pub(super) fn persist_snapshot(raw_content: &str) { // LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt) // 连同错误信息一起落盘到 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。 -pub(super) fn persist_error_input(system_prompt: &str, user_prompt: &str, error: &str) { +pub(crate) 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; 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 360cbbad4..471d7c0df 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -34,6 +34,11 @@ use shared_contracts::game_creation_app::{ use tauri::Emitter; use tauri_plugin_opener::OpenerExt; +// 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。 +// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。 +#[cfg(all(debug_assertions, not(test)))] +mod debug; + #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct InitLocalProjectResult { @@ -1833,7 +1838,7 @@ async fn request_generator_game_draft_with_client( Err(error) => { // 调用失败(含空返回重试耗尽)时,把本次输入连同错误一并落盘,便于复现定位(仅 debug 构建)。 #[cfg(all(debug_assertions, not(test)))] - debug_drafts::persist_error_input( + debug::persist_error_input( system_prompt, user_prompt.as_str(), &error.to_string(), @@ -1844,7 +1849,7 @@ async fn request_generator_game_draft_with_client( }; // 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文(仅 debug 构建)。 #[cfg(all(debug_assertions, not(test)))] - debug_drafts::persist_snapshot(response.content.as_str()); + debug::persist_snapshot(response.content.as_str()); let content = strip_llm_thinking_blocks(response.content.as_str()); parse_llm_game_draft_response(content.as_str()) } @@ -3749,11 +3754,6 @@ fn parse_llm_game_draft_response(content: &str) -> Result .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) } -// 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。 -// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。 -#[cfg(all(debug_assertions, not(test)))] -mod debug_drafts; - fn strip_llm_thinking_blocks(content: &str) -> String { let mut output = String::new(); let mut rest = content; -- 2.52.0