diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 36cb6127a..8c17fd38b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -3555,7 +3555,7 @@ fn direct_codex_generated_source() -> GameCreationAppAssetSource { } } -fn direct_codex_output_fingerprint(root: &Path) -> String { +fn direct_codex_output_fingerprint_checked(root: &Path) -> Result { let mut hasher = Sha256::new(); let mut paths: Vec = direct_codex_game_outputs(root) .into_iter() @@ -3568,16 +3568,27 @@ fn direct_codex_output_fingerprint(root: &Path) -> String { hasher.update(local_path.as_bytes()); hasher.update([0]); let path = root.join(local_path); - match std::fs::read(path) { + match std::fs::read(&path) { Ok(bytes) => { hasher.update([1]); hasher.update((bytes.len() as u64).to_le_bytes()); hasher.update(bytes); } - Err(_) => hasher.update([0]), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => hasher.update([0]), + Err(error) => { + return Err(format!( + "读取直连 Codex 输出文件失败:{}:{error}", + path.display() + )); + } } } - format!("{:x}", hasher.finalize()) + Ok(format!("{:x}", hasher.finalize())) +} + +#[cfg(test)] +fn direct_codex_output_fingerprint(root: &Path) -> String { + direct_codex_output_fingerprint_checked(root).expect("读取直连 Codex 输出指纹") } fn direct_npm_source_paths(root: &Path) -> Vec { @@ -4116,7 +4127,7 @@ fn sync_direct_codex_project_file_projection_at( root: &Path, previous_output_fingerprint: Option<&str>, ) -> Result<(), String> { - let current_output_fingerprint = direct_codex_output_fingerprint(root); + let current_output_fingerprint = direct_codex_output_fingerprint_checked(root)?; let outputs_changed = previous_output_fingerprint .map(|previous| previous != current_output_fingerprint) .unwrap_or(true); @@ -4554,7 +4565,8 @@ async fn run_direct_game_creator_turn_inner( let stream_enabled = load_game_creator_app_config() .map(|config| config.llm.stream) .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; - let previous_output_fingerprint = direct_codex_output_fingerprint(root); + let previous_output_fingerprint = direct_codex_output_fingerprint_checked(root) + .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; let base_system_prompt = build_direct_codex_system_prompt_with_creation_type(root, creation_type) .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; @@ -4748,7 +4760,9 @@ async fn run_direct_game_creator_turn_inner( if let Some(emitter) = turn_emitter { emitter.emit("finalizing", Some("response-finalization")); } - if direct_codex_output_fingerprint(root) != previous_output_fingerprint { + let current_output_fingerprint = direct_codex_output_fingerprint_checked(root) + .map_err(|error| TurnError::turn_failed(FailureStage::VersionRegistration, error))?; + if current_output_fingerprint != previous_output_fingerprint { emit_direct_game_creator_progress( root, "project.sync", @@ -4894,7 +4908,8 @@ async fn run_direct_game_creator_turn_with_private_editor_credentials( .await .map_err(|error| TurnError::turn_failed(FailureStage::ArtPreparation, error))?; } - let previous_output_fingerprint = direct_codex_output_fingerprint(root); + let previous_output_fingerprint = direct_codex_output_fingerprint_checked(root) + .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; let mut system_prompt = build_direct_codex_system_prompt(root) .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; if prepare_art { @@ -4906,7 +4921,8 @@ async fn run_direct_game_creator_turn_with_private_editor_credentials( } let _initial_reply = direct_game_creator_codex_chat_at(root, system_prompt.clone(), prompt.to_string()).await?; - let initial_output_fingerprint = direct_codex_output_fingerprint(root); + let initial_output_fingerprint = direct_codex_output_fingerprint_checked(root) + .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; let mut completion_error = direct_game_output_completion_error(root); let mut evidence_attempts = 0_usize; let mut evidence: Option = None; @@ -4937,7 +4953,8 @@ async fn run_direct_game_creator_turn_with_private_editor_credentials( emit_direct_game_creator_progress(root, "codex.review", "正在根据试玩证据自主检查游戏"); let mut reply = direct_game_creator_codex_chat_at(root, system_prompt.clone(), repair_prompt).await?; - let repaired_fingerprint = direct_codex_output_fingerprint(root); + let repaired_fingerprint = direct_codex_output_fingerprint_checked(root) + .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; completion_error = direct_game_output_completion_error(root); if completion_error.is_none() && (browser_checked_output_fingerprint.as_deref() != Some(repaired_fingerprint.as_str()) @@ -4968,7 +4985,8 @@ async fn run_direct_game_creator_turn_with_private_editor_credentials( "试玩未通过,正在进行最后一次自主修复", ); reply = direct_game_creator_codex_chat_at(root, system_prompt, repair_prompt).await?; - let final_fingerprint = direct_codex_output_fingerprint(root); + let final_fingerprint = direct_codex_output_fingerprint_checked(root) + .map_err(|error| TurnError::turn_failed(FailureStage::CodeGeneration, error))?; completion_error = direct_game_output_completion_error(root); if completion_error.is_none() && browser_checked_output_fingerprint.as_deref() != Some(final_fingerprint.as_str()) diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 85c1ea487..6c56aaaeb 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -81,5 +81,6 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 策划 Agent 的 Tauri 事件订阅失败原先只放行回合等待、没有任何用户提示;现把订阅 IPC 正文写入项目聊天错误出口,避免只收到不完整的最终态。 - 专业 Agent 历史回读与项目清单同步的后台 Promise 失败原先只返回 `failed` 或静默结束;现把具体读取/同步正文写入项目聊天错误出口。 - Direct 交付计划/合同 JSON 解析、策划工作区目录枚举与文本搜索还有 `map_err(|_|)` / `Err(_)` 丢细节;现保留 serde、目录和文件读取正文,安全路径边界仍单独拒绝。 +- 直连 Codex 输出指纹此前把除 `NotFound` 之外的所有读取错误都当成“文件不存在”,可能误判产物未变化;现只对确实不存在的文件保留空位,其余磁盘/权限错误沿回合失败正文上送。 - 错误报告对话框读取最新错误事件或诊断日志失败时原先只显示“暂不可用/使用快照”;现把读取 IPC/文件正文附在状态提示中,快照回退行为保持不变。 - WorkspaceLauncher 的清单版本读取、失效事件重读和事件订阅失败原先只写日志或静默回退;现把安全正文送入启动器状态提示,仍保留已有回退刷新路径。