From 065a193e4e6d65b4d31ee27f4dbb9dae3b55951d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 6 Oct 2026 19:23:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=B7=A5=E4=BD=9C=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E4=BB=8E=E7=B4=A2=E5=BC=95=E3=80=81checkpoint=20?= =?UTF-8?q?=E4=B8=8E=20git=20=E6=A3=80=E6=9F=A5=E4=B8=AD=E6=8E=92=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - filesystem 新增 should_skip_project_derived_path(快照口径 + .export),索引改用它 - checkpoint 恢复与清单过滤改用新口径,.export 不再进 checkpoint - git_inspect 的提交路径校验与工作区路径过滤改用新口径,.export 不再算未跟踪/可提交 - 常量改名 PROJECT_DERIVED_EXCLUDED_COMPONENTS,project_snapshot 测试补双向断言 --- .../src-tauri/src/git_inspect.rs | 6 +-- .../src-tauri/src/project/checkpoint.rs | 4 +- .../src-tauri/src/project/filesystem.rs | 43 ++++++++++++++----- .../src-tauri/src/project_snapshot/tests.rs | 29 ++++++++++++- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/git_inspect.rs b/apps/ai-game-creator-shell/src-tauri/src/git_inspect.rs index 74a7d4b92..a58999cdb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/git_inspect.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/git_inspect.rs @@ -1,5 +1,5 @@ use crate::command_exec::resolve_project_command_spec_at; -use crate::project::{normalize_relative_path, should_skip_project_snapshot_path}; +use crate::project::{normalize_relative_path, should_skip_project_derived_path}; use sha2::{Digest as _, Sha256}; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; @@ -1046,7 +1046,7 @@ fn validate_commit_paths(paths: &[String]) -> Result, LocalGitCommit let normalized = normalize_relative_path(path) .map_err(|_| LocalGitCommitError::ordinary("Git 提交路径必须是规范化的项目相对路径"))?; if normalized != *path - || should_skip_project_snapshot_path(&normalized) + || should_skip_project_derived_path(&normalized) || !unique.insert(normalized.clone()) { return Err(LocalGitCommitError::ordinary( @@ -1971,7 +1971,7 @@ fn parse_status(root: &Path, output: &str) -> (Vec, Vec, Vec Option { let normalized = normalize_relative_path(path).ok()?; - if should_skip_project_snapshot_path(&normalized) { + if should_skip_project_derived_path(&normalized) { return None; } if !git_worktree_path_is_safe(root, &normalized) { diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs b/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs index 70b12ab48..e5b86cb40 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs @@ -673,7 +673,7 @@ pub(crate) fn restore_local_project_checkpoint_at( } pub(crate) fn should_skip_project_restore_path(relative_path: &str) -> bool { - should_skip_project_snapshot_path(relative_path) + should_skip_project_derived_path(relative_path) || relative_path == ".agent/agent.db" || relative_path == PROJECT_PERMISSION_POLICY_PATH || relative_path == PROJECT_WRITE_LOCK_PATH @@ -787,7 +787,7 @@ pub(crate) fn read_checkpoint_files( } file.path = normalized; } - files.retain(|file| !should_skip_project_snapshot_path(&file.path)); + files.retain(|file| !should_skip_project_derived_path(&file.path)); Ok(files) } 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 d7b6ddac4..cf759c0e9 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 @@ -397,15 +397,37 @@ pub(crate) fn should_skip_project_index_path(relative_path: &str) -> bool { || relative_path == PROJECT_INDEX_PATH || relative_path.starts_with(".agent/checkpoints/") || relative_path.starts_with(".agent/runtime/") - || should_skip_project_snapshot_path(relative_path) + || should_skip_project_derived_path(relative_path) } -/// 项目索引、checkpoint、Agent 上下文与 git 检查共用的排除口径:`.agent` 是这些结果的 -/// 本机控制面,不参与其中。 +/// Agent 视角的排除口径:`.agent` 是本机控制面,写入门禁(`agent/direct_patch.rs`)与 +/// Agent 上下文(prompt / tool bridge)拿它判断「Agent 能不能碰、看不看得到」。 +/// +/// 它**不**包含 `.export`:那个目录必须让 code agent 能写。把项目内容当数据来读的消费者 +/// (索引、checkpoint、git 检查)请改用 `should_skip_project_derived_path`。 pub(crate) fn should_skip_project_snapshot_path(relative_path: &str) -> bool { project_snapshot_path_is_excluded(relative_path, false) } +/// 索引、checkpoint 与 git 检查共用的排除口径:在本机控制面之外,再排除本机派生的导出工作目录 +/// (`.export`,见 `PROJECT_DERIVED_EXCLUDED_COMPONENTS`)。 +/// +/// 这些文件能从项目源码重跑出来,不该进索引、checkpoint 或版本库视角,也不该被当成项目变更; +/// 但 Agent 必须能写它们,所以不能并进 `should_skip_project_snapshot_path`——那份列表同时是 +/// `agc_apply_patch` 的写入门禁,并进去会让首次适配直接卡死。 +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| { + let component = component.to_ascii_lowercase(); + PROJECT_DERIVED_EXCLUDED_COMPONENTS + .iter() + .any(|candidate| *candidate == component) + }) +} + /** * 项目快照同步(上传)的排除口径。 * @@ -492,16 +514,17 @@ const PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES: &[&str] = &[ const PROJECT_AGENT_STATE_DATABASE_SUFFIXES: &[&str] = &[".db", ".db-wal", ".db-shm"]; /** - * 只对快照同步生效的排除组件。 + * 本机派生的导出工作目录(`.export`):Agent 可见,其它消费者一律排除。 * * 不能并进上面的通用列表:那份列表同时是 Agent 的写入门禁(`agent/direct_patch.rs` 直接拿它 * 拒绝路径),而 `.export` 恰恰必须让 code agent 能写——导出注册表、打包脚本拷贝都由它生成。 * 放进去会让 `agc_apply_patch` 回「不得修改受保护或排除路径」,首次适配直接卡死。 * - * `.export` 是本机派生的导出工作目录:注册表、脚本拷贝、icon 与 zip 产物都能从项目源码重跑 - * 出来,zip 动辄数 MB,因此不进快照同步;换机或恢复后重新适配即可。 + * 也不能只排快照同步:索引会把多 MB 的 zip 整份读进内存算 checksum,checkpoint 会把它复制进 + * `.agent/checkpoints`,git 检查会把它当未跟踪文件放出去,允许提交。所以「把项目内容当数据 + * 来读」的消费者(索引、checkpoint、git 检查)都走 `should_skip_project_derived_path`。 */ -const PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS: &[&str] = &[".export"]; +const PROJECT_DERIVED_EXCLUDED_COMPONENTS: &[&str] = &[".export"]; fn project_snapshot_path_is_excluded(relative_path: &str, include_agent_state: bool) -> bool { let components = relative_path @@ -513,9 +536,9 @@ fn project_snapshot_path_is_excluded(relative_path: &str, include_agent_state: b PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS.contains(&component.as_str()) && !(include_agent_state && component == ".agent") }) || (include_agent_state - && components.iter().any(|component| { - PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS.contains(&component.as_str()) - })) + && components + .iter() + .any(|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 ae318018f..7008d2eee 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 @@ -232,14 +232,39 @@ fn project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials() assert!(should_skip_project_index_path(".agent/manifest.json")); assert!(should_skip_project_index_path(".agent/agent.db")); - // `.export/` 只排除在快照同步之外:通用口径同时是 Agent 的写入门禁 - // (`agc_apply_patch` 直接拿它拒路径),一并拒掉会让 code agent 写不了适配脚本。 + // `.export/` 不进 Agent 的写入门禁:通用口径同时是 `agc_apply_patch` 的拒路径依据, + // 一并拒掉会让 code agent 写不了适配脚本。 assert!(!should_skip_project_snapshot_path( ".export/xhs-minitool.pack.mjs" )); assert!(!should_skip_project_snapshot_path( ".export/xhs-minitool.json" )); + + // 但「把项目内容当数据来读」的消费者(索引 / checkpoint / git 检查)一律排除它: + // zip 能从项目源码重跑,不该进索引、checkpoint 清单或版本库视角。 + for derived_path in [ + ".export/xhs-minitool.zip", + ".export/xhs-minitool.json", + ".export/xhs-minitool.pack.mjs", + ".EXPORT/xhs-minitool.zip", + ] { + assert!( + should_skip_project_derived_path(derived_path), + "数据视角共用口径应排除本机派生目录:{derived_path}" + ); + assert!( + should_skip_project_index_path(derived_path), + "索引应排除本机派生目录:{derived_path}" + ); + assert!( + should_skip_project_restore_path(derived_path), + "checkpoint 恢复应排除本机派生目录:{derived_path}" + ); + } + // 普通源码目录不受影响,只有 `.export` 这类派生目录被额外排除。 + assert!(!should_skip_project_derived_path("game/main.ts")); + assert!(!should_skip_project_index_path("game/main.ts")); } #[test]