diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs b/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs index 8c140b94b..566f41128 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs @@ -415,12 +415,15 @@ pub(crate) fn should_skip_project_snapshot_path(relative_path: &str) -> bool { /// 这些文件能从项目源码重跑出来,不该进索引、checkpoint 或版本库视角,也不该被当成项目变更; /// 但 Agent 必须能写它们,所以不能并进 `should_skip_project_snapshot_path`——那份列表同时是 /// `agc_apply_patch` 的写入门禁,并进去会让首次适配直接卡死。 +/// +/// `.export` 只认项目根那一层(`EXPORT_WORK_RELATIVE_DIR` 就是 `root/.export`):源码里合法的 +/// 嵌套目录 `game/.export/...` 是用户自己的文件,不能被误伤。 pub(crate) fn should_skip_project_derived_path(relative_path: &str) -> bool { project_snapshot_path_is_excluded(relative_path, false) || relative_path .split('/') - .filter(|component| !component.is_empty()) - .any(|component| { + .find(|component| !component.is_empty()) + .is_some_and(|component| { PROJECT_DERIVED_EXCLUDED_COMPONENTS .iter() .any(|candidate| component.eq_ignore_ascii_case(candidate)) @@ -439,7 +442,8 @@ pub(crate) fn should_skip_project_derived_path(relative_path: &str) -> bool { * `.env*` 与凭据类文件名一律不参与同步;符号链接与重解析点在扫描阶段单独跳过。 * * 另外,同步还多认 `PROJECT_DERIVED_EXCLUDED_COMPONENTS`(`.export`):本机派生 - * 的导出工作目录不进同步,但 Agent 仍要能读写它。 + * 的导出工作目录不进同步,但 Agent 仍要能读写它。`.export` 同样只认项目根那一层, + * 源码里的嵌套 `game/.export/...` 不在此列。 */ pub(crate) fn should_skip_project_snapshot_sync_path(relative_path: &str) -> bool { project_snapshot_path_is_excluded(relative_path, true) @@ -536,8 +540,8 @@ fn project_snapshot_path_is_excluded(relative_path: &str, include_agent_state: b && !(include_agent_state && component == ".agent") }) || (include_agent_state && components - .iter() - .any(|component| PROJECT_DERIVED_EXCLUDED_COMPONENTS.contains(&component.as_str()))) + .first() + .is_some_and(|component| PROJECT_DERIVED_EXCLUDED_COMPONENTS.contains(&component.as_str()))) { return true; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/tests.rs index 7008d2eee..f344b9e82 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/tests.rs @@ -267,6 +267,41 @@ fn project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials() assert!(!should_skip_project_index_path("game/main.ts")); } +#[test] +fn derived_export_directory_is_matched_only_at_the_project_root() { + // 项目根的 `.export` 才是本机派生工作目录:数据视角与快照同步都排除。 + for root_export in [ + ".export/xhs-minitool.zip", + ".export/xhs-minitool.json", + ".EXPORT/xhs-minitool.zip", + ] { + assert!( + should_skip_project_derived_path(root_export), + "根目录的 .export 必须排除:{root_export}" + ); + assert!( + should_skip_project_snapshot_sync_path(root_export), + "根目录的 .export 不进快照同步:{root_export}" + ); + } + + // 嵌套在源码目录里的 `.export` 是用户自己的文件,不能按本机派生目录误伤。 + for nested in [ + "game/.export/local.ts", + "game/.export/xhs-minitool.zip", + "src/vendor/.export/keep.json", + ] { + assert!( + !should_skip_project_derived_path(nested), + "嵌套 .export 是用户源码,不该被数据视角排除:{nested}" + ); + assert!( + !should_skip_project_snapshot_sync_path(nested), + "嵌套 .export 是用户源码,不该被排除出快照同步:{nested}" + ); + } +} + #[test] fn project_snapshot_diff_reuses_metadata_and_reports_a_single_modification() { let root = fixture_root();