diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_patch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_patch.rs index 2562f9b5e..fd7de081d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_patch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_patch.rs @@ -191,42 +191,60 @@ fn validate_argv(executable: &Path, patch: &str) -> Result<(), String> { Ok(()) } -fn target_fingerprints(targets: &[PatchTarget]) -> BTreeMap> { - targets - .iter() - .map(|target| { - let digest = (|| { - match std::fs::symlink_metadata(&target.absolute) { - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - return Some("missing".into()) - } - Ok(_) => {} - Err(_) => return None, - } +fn target_fingerprints( + targets: &[PatchTarget], +) -> Result>, String> { + let mut fingerprints = BTreeMap::new(); + for target in targets { + let digest = match std::fs::symlink_metadata(&target.absolute) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Some("missing".into()), + Ok(_) => { let (mut file, metadata) = - open_project_snapshot_regular_file(&target.absolute, "补丁指纹").ok()?; + open_project_snapshot_regular_file(&target.absolute, "补丁指纹").map_err( + |error| format!("补丁指纹读取失败:{}:{error}", target.relative), + )?; if metadata.len() > TARGET_HASH_MAX_BYTES { - return None; + return Err(format!( + "补丁指纹读取失败:目标文件 {} 大小 {} bytes,超过 {} bytes 上限", + target.relative, + metadata.len(), + TARGET_HASH_MAX_BYTES + )); } let mut digest = Sha256::new(); let mut buffer = [0u8; 16 * 1024]; let mut total = 0u64; loop { - let count = file.read(&mut buffer).ok()?; + let count = file.read(&mut buffer).map_err(|error| { + format!( + "补丁指纹读取失败:{}:读取文件正文失败:{error}", + target.relative + ) + })?; if count == 0 { break; } total += count as u64; if total > TARGET_HASH_MAX_BYTES { - return None; + return Err(format!( + "补丁指纹读取失败:目标文件 {} 在读取期间超过 {} bytes 上限", + target.relative, TARGET_HASH_MAX_BYTES + )); } digest.update(&buffer[..count]); } Some(format!("file:{:x}", digest.finalize())) - })(); - (target.relative.clone(), digest) - }) - .collect() + } + Err(error) => { + return Err(format!( + "补丁指纹检查失败:{}:读取文件元数据失败:{error}", + target.relative + )); + } + }; + fingerprints.insert(target.relative.clone(), digest); + } + Ok(fingerprints) } fn analytics_patch_changes( @@ -278,7 +296,7 @@ fn run_transaction( let targets = collect_targets(root, &parsed.hunks)?; let executable = session.codex_executor()?; validate_argv(&executable, &parsed.patch)?; - let before = target_fingerprints(&targets); + let before = target_fingerprints(&targets)?; let lease = session.admit(EffectKind::Write, None)?; let process = runtime.block_on(crate::command_exec::run_owned_codex_patch_at( root, @@ -317,7 +335,7 @@ fn run_transaction( error.to_string(), ), }; - let after = target_fingerprints(&targets); + let after = target_fingerprints(&targets)?; let changed_paths = targets .iter() .filter(|target| { @@ -439,6 +457,44 @@ mod tests { (temp, root.canonicalize().unwrap()) } + #[test] + fn target_fingerprints_keep_missing_distinct_and_report_read_failures() { + let (_temp, root) = project(); + let missing = PatchTarget { + relative: "game/missing.txt".into(), + absolute: root.join("game/missing.txt"), + }; + let fingerprints = target_fingerprints(std::slice::from_ref(&missing)).unwrap(); + assert_eq!( + fingerprints.get("game/missing.txt"), + Some(&Some("missing".into())) + ); + + let directory = root.join("game/fingerprint-directory"); + std::fs::create_dir_all(&directory).unwrap(); + let directory_target = PatchTarget { + relative: "game/fingerprint-directory".into(), + absolute: directory, + }; + let error = target_fingerprints(std::slice::from_ref(&directory_target)).unwrap_err(); + assert!(error.contains("补丁指纹读取失败"), "{error}"); + assert!( + error.contains("打开补丁指纹失败") || error.contains("必须是普通文件"), + "{error}" + ); + + let oversized = root.join("game/fingerprint-oversized.bin"); + let file = std::fs::File::create(&oversized).unwrap(); + file.set_len(TARGET_HASH_MAX_BYTES + 1).unwrap(); + let oversized_target = PatchTarget { + relative: "game/fingerprint-oversized.bin".into(), + absolute: oversized, + }; + let error = target_fingerprints(std::slice::from_ref(&oversized_target)).unwrap_err(); + assert!(error.contains("超过"), "{error}"); + assert!(error.contains("补丁指纹读取失败"), "{error}"); + } + #[test] fn parser_paths_include_every_move_source_and_destination_before_any_write() { let (_temp, root) = project(); diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index fed146221..f373aef22 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -85,3 +85,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - DirectProject 上下文的外部路径元数据读取失败原先统一成 `file-not-found`;现区分确实不存在与权限/磁盘读取错误,后者保留底层正文。 - 错误报告对话框读取最新错误事件或诊断日志失败时原先只显示“暂不可用/使用快照”;现把读取 IPC/文件正文附在状态提示中,快照回退行为保持不变。 - WorkspaceLauncher 的清单版本读取、失效事件重读和事件订阅失败原先只写日志或静默回退;现把安全正文送入启动器状态提示,仍保留已有回退刷新路径。 +- Direct 补丁事务的目标指纹读取原先把元数据、打开、正文读取和文件超限都折叠为 `None`,可能误判文件缺失或继续比较;现只对确实 `NotFound` 保留 `missing`,其余失败返回目标相对路径、阶段和底层正文,避免补丁结果被错误归因。