合并 master:AGC 项目快照整目录上传
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled

- 合并 origin/master(bba4f0eb2:项目快照整目录上传 .agent,保留项目身份与 Agent 历史)。
- 自动合并无冲突;共享记忆与实施计划文档经复核保留双方条目,游戏分发功能面未受影响。
- 同步目的:Gitea PR 门禁要求 PR head 包含目标分支最新提交,合并后重新推送触发 Project CI。
This commit is contained in:
2026-09-22 16:58:44 +08:00
8 changed files with 232 additions and 75 deletions
@@ -348,79 +348,113 @@ pub(crate) fn should_skip_project_index_path(relative_path: &str) -> bool {
|| should_skip_project_snapshot_path(relative_path)
}
/// 项目索引、checkpoint、Agent 上下文与 git 检查共用的排除口径:`.agent` 是这些结果的
/// 本机控制面,不参与其中。
pub(crate) fn should_skip_project_snapshot_path(relative_path: &str) -> bool {
project_snapshot_path_is_excluded(relative_path, false)
}
/**
* 项目快照同步(上传)的排除口径。
*
* 与 `should_skip_project_snapshot_path` 是同一份组件与后缀规则,唯一区别是 `.agent`:
* 它承载项目身份与 Agent 状态(`manifest.json`、`agent.db`、会话、运行日志、checkpoint、
* workbench、`project.lock`),必须整目录随快照同步,因此不再把 `.agent` 组件本身当作
* 排除项,并放行其中的 Agent 状态数据库(`.db` / `.db-wal` / `.db-shm`)。
*
* 其余排除项在 `.agent` 内同样生效:版本库、依赖与构建目录、凭据目录、敏感后缀、
* `.env*` 与凭据类文件名一律不参与同步;符号链接与重解析点在扫描阶段单独跳过。
*/
pub(crate) fn should_skip_project_snapshot_sync_path(relative_path: &str) -> bool {
project_snapshot_path_is_excluded(relative_path, true)
}
/// 任意层级出现即排除的目录组件。`.agent` 只有项目快照同步会放行。
const PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS: &[&str] = &[
".agent",
".git",
".hg",
".svn",
".ssh",
".aws",
".azure",
".gnupg",
".kube",
".docker",
".gcloud",
".terraform",
".password-store",
".secrets",
"secrets",
"credentials",
"node_modules",
"target",
"dist",
"build",
".next",
"coverage",
".cache",
];
/// 凭据、密钥与数据库转储类文件名后缀。
const PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES: &[&str] = &[
".pem",
".key",
".p12",
".pfx",
".ppk",
".jks",
".keystore",
".kdbx",
".db",
".db-wal",
".db-shm",
".sqlite",
".sqlite-wal",
".sqlite-shm",
".sqlite3",
".sqlite3-wal",
".sqlite3-shm",
".sql",
".sql.gz",
".sql.bz2",
".sql.xz",
".dump",
".dump.gz",
".dmp",
".bak",
".mdb",
".accdb",
".rdb",
".bson",
".pgdump",
".tfstate",
".tfstate.backup",
];
/// `.agent` 内的 Agent 状态数据库(`agent.db` 及其 WAL / SHM 旁文件)属于项目状态,
/// 随快照同步;其它数据库与转储后缀仍然排除。
const PROJECT_AGENT_STATE_DATABASE_SUFFIXES: &[&str] = &[".db", ".db-wal", ".db-shm"];
fn project_snapshot_path_is_excluded(relative_path: &str, include_agent_state: bool) -> bool {
let components = relative_path
.split('/')
.filter(|component| !component.is_empty())
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>();
if components.iter().any(|component| {
matches!(
component.as_str(),
".agent"
| ".git"
| ".hg"
| ".svn"
| ".ssh"
| ".aws"
| ".azure"
| ".gnupg"
| ".kube"
| ".docker"
| ".gcloud"
| ".terraform"
| ".password-store"
| ".secrets"
| "secrets"
| "credentials"
| "node_modules"
| "target"
| "dist"
| "build"
| ".next"
| "coverage"
| ".cache"
)
PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS.contains(&component.as_str())
&& !(include_agent_state && component == ".agent")
}) {
return true;
}
let Some(file_name) = components.last() else {
return true;
};
let sensitive_suffixes = [
".pem",
".key",
".p12",
".pfx",
".ppk",
".jks",
".keystore",
".kdbx",
".db",
".db-wal",
".db-shm",
".sqlite",
".sqlite-wal",
".sqlite-shm",
".sqlite3",
".sqlite3-wal",
".sqlite3-shm",
".sql",
".sql.gz",
".sql.bz2",
".sql.xz",
".dump",
".dump.gz",
".dmp",
".bak",
".mdb",
".accdb",
".rdb",
".bson",
".pgdump",
".tfstate",
".tfstate.backup",
];
let agent_state_database = include_agent_state
&& components
.first()
.is_some_and(|first| first.as_str() == ".agent");
let structured_secret_suffixes = [".json", ".txt", ".toml", ".yaml", ".yml"];
file_name == ".env"
|| file_name.starts_with(".env.")
@@ -471,9 +505,10 @@ pub(crate) fn should_skip_project_snapshot_path(relative_path: &str) -> bool {
|| file_name.starts_with("id_ecdsa")
|| file_name.starts_with("id_ed25519")
|| file_name.starts_with("id_xmss")
|| sensitive_suffixes
.iter()
.any(|suffix| file_name.ends_with(suffix))
|| PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES.iter().any(|suffix| {
file_name.ends_with(suffix)
&& !(agent_state_database && PROJECT_AGENT_STATE_DATABASE_SUFFIXES.contains(suffix))
})
|| ((file_name.contains("cookie") || file_name.contains("credential"))
&& structured_secret_suffixes
.iter()
@@ -23,9 +23,10 @@ pub(crate) struct ProjectSnapshotScanResult {
pub(crate) skipped: Vec<ProjectSnapshotSkippedPath>,
}
/// 扫描项目目录,复用 checkpoint / 项目索引同一份排除口径
/// `.agent`、版本控制目录、依赖与构建产物目录、凭据目录、符号链接与重解析点
/// 都不参与同步,超出单文件上限的文件进入跳过清单而不是静默丢弃。
/// 扫描项目目录,排除口径见 `should_skip_project_snapshot_sync_path`
/// `.agent` 是项目身份与 Agent 状态的权威位置,整目录参与同步;版本控制目录、
/// 依赖与构建产物目录、凭据目录、符号链接与重解析点都不参与同步,超出单文件
/// 上限的文件进入跳过清单而不是静默丢弃。
pub(crate) fn scan_project_snapshot_files(
root: &Path,
max_file_bytes: u64,
@@ -52,7 +53,7 @@ pub(crate) fn scan_project_snapshot_files(
let Ok(relative_path) = relative_project_path(root, &path) else {
continue;
};
if should_skip_project_snapshot_path(&relative_path) {
if should_skip_project_snapshot_sync_path(&relative_path) {
continue;
}
let metadata = match fs::symlink_metadata(&path) {
@@ -95,8 +95,6 @@ fn project_snapshot_scan_skips_excluded_paths_and_oversized_files() {
let root = fixture_root();
write_fixture_file(root.path(), "game/index.html", b"<html></html>");
write_fixture_file(root.path(), "assets/manifest.json", b"{}");
write_fixture_file(root.path(), ".agent/runtime/state.json", b"{}");
write_fixture_file(root.path(), ".agent/manifest.json", b"{}");
write_fixture_file(root.path(), "node_modules/pkg/index.js", b"export {};");
write_fixture_file(root.path(), "game/dist/bundle.js", b"bundle");
write_fixture_file(root.path(), "secrets/key.pem", b"private-key");
@@ -111,7 +109,7 @@ fn project_snapshot_scan_skips_excluded_paths_and_oversized_files() {
assert_eq!(
scanned,
vec!["assets/manifest.json".to_string()],
".agent、node_modules、dist 与凭据目录里的文件不能进入候选集合"
"node_modules、dist 与凭据目录里的文件不能进入候选集合"
);
let skipped = scan
.skipped
@@ -125,6 +123,111 @@ fn project_snapshot_scan_skips_excluded_paths_and_oversized_files() {
);
}
#[test]
fn project_snapshot_scan_uploads_whole_agent_directory() {
let root = fixture_root();
write_fixture_file(root.path(), "game/index.html", b"<html></html>");
write_fixture_file(root.path(), ".agent/manifest.json", b"{}");
write_fixture_file(root.path(), ".agent/agent.db", b"sqlite");
write_fixture_file(root.path(), ".agent/agent.db-wal", b"wal");
write_fixture_file(root.path(), ".agent/project.lock", b"{}");
write_fixture_file(root.path(), ".agent/.manifest.json.lock", b"");
write_fixture_file(root.path(), ".agent/conversations/project.jsonl", b"{}\n");
write_fixture_file(root.path(), ".agent/runtime/events/art.jsonl", b"{}\n");
write_fixture_file(
root.path(),
".agent/runtime/command-env/home/.config.json",
b"{}",
);
write_fixture_file(root.path(), ".agent/logs/command.log", b"log");
write_fixture_file(
root.path(),
".agent/checkpoints/0001/manifest.json",
b"{\"files\":[]}",
);
write_fixture_file(
root.path(),
".agent/workbench/resource-layouts/art.json",
b"{}",
);
let scan = scan_fixture(root.path());
let scanned = scan
.files
.iter()
.map(|file| file.relative_path.clone())
.collect::<Vec<_>>();
assert_eq!(
scanned,
vec![
".agent/.manifest.json.lock".to_string(),
".agent/agent.db".to_string(),
".agent/agent.db-wal".to_string(),
".agent/checkpoints/0001/manifest.json".to_string(),
".agent/conversations/project.jsonl".to_string(),
".agent/logs/command.log".to_string(),
".agent/manifest.json".to_string(),
".agent/project.lock".to_string(),
".agent/runtime/command-env/home/.config.json".to_string(),
".agent/runtime/events/art.jsonl".to_string(),
".agent/workbench/resource-layouts/art.json".to_string(),
"game/index.html".to_string(),
],
"`.agent` 是项目身份与 Agent 状态的权威位置,必须整目录参与同步"
);
assert!(
scan.skipped.is_empty(),
"`.agent` 内的普通文件既不跳过也不延后"
);
}
#[test]
fn project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials() {
for relative_path in [
".agent/manifest.json",
".agent/agent.db",
".agent/agent.db-wal",
".agent/agent.db-shm",
".agent/project.lock",
".agent/runtime/events/art.jsonl",
".agent/runtime/locks/append/01.lock",
".agent/checkpoints/0001/manifest.json",
".agent/conversations/project.jsonl",
".agent/workbench/resource-layouts/art.json",
".AGENT/manifest.json",
] {
assert!(
!should_skip_project_snapshot_sync_path(relative_path),
"`.agent` 状态必须参与同步:{relative_path}"
);
}
for relative_path in [
".agent/credentials/platform.json",
".agent/.ssh/id_rsa",
".agent/certs/server.pem",
".agent/node_modules/pkg/index.js",
".agent/runtime/command-env/home/.env",
".agent/runtime/command-env/home/.npmrc",
".agent/backup/game.sql",
".git/config",
"node_modules/pkg/index.js",
"game/dist/bundle.js",
"secrets/key.pem",
"",
] {
assert!(
should_skip_project_snapshot_sync_path(relative_path),
"凭据、版本库与构建产物仍然排除:{relative_path}"
);
}
// 项目索引、checkpoint 与 Agent 上下文继续排除整个 `.agent`,本变更只放开快照同步。
assert!(should_skip_project_snapshot_path(".agent/manifest.json"));
assert!(should_skip_project_index_path(".agent/manifest.json"));
assert!(should_skip_project_index_path(".agent/agent.db"));
}
#[test]
fn project_snapshot_diff_reuses_metadata_and_reports_a_single_modification() {
let root = fixture_root();
@@ -38,6 +38,6 @@ Parent Milestone: `【里程碑】AGC项目定时快照上传-2026-09-17.md`
## 风险与回滚
- 上传体积与带宽:首轮全量可能很大,先设单文件与单次同步总量上限并把超限项记入跳过清单;不静默截断。
- 数据出境边界:只上传项目目录内普通文件,排除 `.agent/runtime``.agent/logs``.git`、构建产物临时文件凭据类文件不在白名单内。
- 数据出境边界2026-09-22 修订):只上传项目目录内普通文件`.agent` 承载项目身份与 Agent 状态,整目录上传(含 conversations、logs、runtime、checkpoint`agent.db`);`.git`、构建产物临时文件凭据类文件不在白名单内。该修订只作用于快照同步,项目索引与 checkpoint 继续排除整个 `.agent`
- 服务端未配置 bucket 时客户端必须失败关闭,不能把本地索引推进成"已同步",否则后续同步会漏传。
- 回滚:客户端可停用触发接线(保留模块与测试)即可回到无上传行为;服务端路由与配置项可单独移除,不影响既有 OSS 前缀与错误报告链路。
@@ -17,7 +17,7 @@ AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev`
- 目标 bucket 配置:`GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_*`,默认 `agc-dev`
- 契约:`shared-contracts::agc_project_snapshots` 新增请求/响应 DTO 与项目 ID、相对路径、摘要校验函数。
- 客户端增量索引:`<AppData>/project-snapshots/<projectId>/index.json`,按用户身份判等,换号后按冷启动全量重算。
- 排除口径:`should_skip_project_snapshot_path`整个 `.agent``.git`、构建与依赖目录、凭据目录、敏感后缀、符号链接与重解析点)
- 排除口径:快照同步使`should_skip_project_snapshot_sync_path`2026-09-22 起)。`.agent` 承载项目身份与 Agent 状态,整目录同步;`.git`、构建与依赖目录、凭据目录、敏感后缀、符号链接与重解析点仍然排除。项目索引、checkpoint、Agent 上下文与 git 检查继续沿用 `should_skip_project_snapshot_path` 的整个 `.agent` 排除口径
## 不做
@@ -32,7 +32,7 @@ AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev`
1. 首次同步上传项目内全部符合条件的普通文件;再次同步在无改动时上传 0 个文件。
2. 只修改一个文件时,差异集合恰好包含一个修改项;删除一个文件时上传集合为空且清单中不再包含该文件。
3. `(字节数, 修改时间)` 未变的文件复用已存摘要,不重复读取内容计算摘要。
4. 排除规则命中项(`.agent/runtime``.agent/logs``.git``node_modules`、构建产物、临时文件、符号链接)与超限文件进入跳过清单,不进入上传集合
4. 项目内 `.agent` 的全部普通文件(`manifest.json``agent.db` 与其 WAL/SHM、`.manifest.json.lock``project.lock`、conversations、logs、runtime、checkpoint、workbench)进入上传集合;`.git``node_modules`、构建产物、凭据目录与敏感后缀仍不进入;超限文件进入跳过或延后清单,不静默丢弃
5. 任一次同步失败(非鉴权类)不推进本地索引,下一次触发重算并重试;鉴权/权限类失败不自动重试。
6. 同一项目的并发触发串行执行,不产生两路重复上传。
7. 工作区窗口关闭与应用退出都会触发一次同步,且关闭路径不因同步失败而阻塞退出超过超时上限。
@@ -25,6 +25,7 @@
- [ ] 未单独配置快照目标时仍使用 agc-dev,只复用资源存储凭据;显式快照目标保持有效,不迁移现存对象。
- [ ] 后台列表按部署渠道查询,显示项目名/ID、用户(昵称 + 陶泥号)、同步时间、文件数、体积和完整性,支持刷新与游标分页(每页 20/50/100 + 上一页/下一页)。
- [ ] ZIP 按清单还原相对路径;不含对象存储摘要目录;空文件可上传与导出。
- [ ] `.agent` 承载项目身份与 Agent 状态,整目录随快照上传,并在后台 ZIP 中按原相对路径还原;归档因此可用于还原项目身份与 AGC 对话历史。凭据、版本库与构建产物仍不进快照。
- [ ] 清单名称/完整性变化在无内容差异时也提交,partial 可恢复 ready,历史缺字段不冒充 ready。
- [ ] 缺失、损坏、越界路径和非完整清单失败关闭;历史未声明完整性的清单明确标记,允许导出已有文件但不称为完整工程。
- [ ] 无后台权限不能读取项目或 ZIP;不泄漏凭据;ZIP 构建有体积、并发和临时文件清理边界。
@@ -39,6 +40,10 @@
| 层次 | 结果 |
| --- | --- |
| 客户端 Rust `project_snapshot` | 22 通过、1 忽略(写入式真实上传 smoke 未运行);含排队退出等待回归 |
| `.agent` 全量上传口径(2026-09-22 修订) | `project_snapshot` 定向 24 通过、1 忽略;新增扫描纳入 `.agent` 与策略单测,覆盖 `.agent` 内凭据、版本库、`.env*` 继续排除,以及项目索引 / checkpoint 口径不变 |
| `.agent` 真实样本复核 | 对 15 个本地 AppData 项目的 487 个 `.agent` 普通文件按新规则复算,0 个仍落在排除集;该复算是脚本复刻规则,不是 Rust 运行时证据 |
| `.agent` 客户端链路运行时 smoke2026-09-22 | 用真实项目副本(`.agent` 164 个文件 / 5.61 MiB,项目 `gameagent-agentsmoke1`)走真实差异引擎 → 本地 api-server → 真实 OSS:首轮 `synced``uploaded=179` / `18,991,590` 字节、跳过与失败均为 0;紧接着第二轮 `no-op` / 上传 0 个文件。`uploaded=179` 恰好等于原口径的 15 个项目文件加 164 个 `.agent` 文件 |
| `.agent` 存储与后台归档(2026-09-22 | 只读 GET 真实 OSS 清单对象(`agc/project-snapshots/v1/<user>/gameagent-agentsmoke1/manifest.json`26,356 字节):`files=179`、其中 `.agent/**` 164 条、`pendingFiles=0`、合计 18,991,590 字节;按清单里的 `大小-摘要` 构造对象键 HEAD 命中 `.agent/manifest.json`(18,319)、`.agent/agent.db`(9,353)、`.agent/conversations/project.jsonl`(1,422,226) 与 `game/package.json`(227)。后台 `download` 返回的 ZIP 解压出 179 条目、164 条在 `.agent/` 下,抽样 6 个文件(含 `agent.db`、会话、运行态文件)与本地 SHA-256 完全一致;正在运行的 dev api-server 列表显示同一项目 `files=179 status=ready` |
| 客户端前端生命周期与启动器 | 3 文件、10 测试通过;完整 AGC typecheck、skill-pack、check-config 通过 |
| 后台页面、API client、路由与样式 | 44 测试通过;admin-web typecheck/build 通过 |
| 后端快照与权限 | 14 定向测试通过;未认证路由矩阵与页签映射 2 测试通过 |
@@ -53,3 +58,5 @@
`npm run dev:api-server -- --api-port 4198 --bgfilter-worker-port 4199 --api-timeout-seconds 600` 编译成功,但当前工作区配置的本地数据库 `xushi-p4wfr``127.0.0.1:3101` 返回 404,启动认证投影无法完成,故 `/healthz` 及完整 HTTP smoke 未通过。仅本任务启动的 API/worker 已停止;没有清库、迁移数据库、改 `.env` 或替换其它项目的验证目标。
尚未替换安装版、构建新客户端发布包或部署;提交推送按用户本轮授权执行。当前条款已有上述自动化与只读存储证据,最终验收仍等待当前工作区数据库就绪后的 HTTP 联调,以及新客户端的隔离实机自动上传验证;完成后再关闭里程碑并清理两份计划。
`.agent` 全量口径已在上表取得真实上传、真实 OSS 清单与后台 ZIP 三层运行时证据。两点边界仍需记住:① 本次 smoke 用的是真实项目的可丢弃副本,其 `projectId` 改成 `gameagent-agentsmoke1`、走 dev 专用账号上传,历史真实项目在下一个同步周期前仍是旧口径清单(不含 `.agent`);② 本地 `api-server.exe` 构建于 2026-09-21 14:06,早于渠道分区提交 `4951b71d7`2026-09-21 16:57),因此本次对象键仍是 `agc/project-snapshots/v1/` 布局、`GET /admin/api/project-snapshots/channels` 在本地返回 404;升级到 v2 键布局需要重建并重启本地 api-server。
@@ -8,6 +8,16 @@
- 影响范围:`jenkins/Jenkinsfile.scheduled-release-trigger``jenkins/scheduled-release-trigger-job-config.xml``jenkins/Jenkinsfile.agc-global-version-issue``scripts/check-production-ops-guardrails.mjs`、开发运维文档、共享开发工作流与 AGC 总版本号技术方案。
- 验证方式:`npm run check:production-ops` 校验 release Job 的 cron、发号、双平台 release 参数、Mac 让位和 Copy Artifact 授权;`npm run check:encoding``git diff --check` 校验文件与补丁。
## 2026-09-22 项目快照 `.agent` 全量上传:远端工程包要能还原项目身份与 Agent 历史
- 背景:快照上传此前复用 `should_skip_project_snapshot_path`,把整个 `.agent``manifest.json``agent.db`、conversations、logs、runtime、checkpoint、workbench、`project.lock`)排除在上传集合之外。后台“项目工程”的「下载完整工程」是按清单逐文件打包的,于是导出的 ZIP 里没有项目身份与对话历史,用户在 AGC 里恢复不出同一个项目。
- 决策:快照同步改用独立口径 `should_skip_project_snapshot_sync_path``apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`)。它与原口径共用同一份组件与后缀规则(提取为 `PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS` / `PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES`),只额外放行两点:`.agent` 组件本身,以及 `.agent` 内的 Agent 状态数据库后缀 `.db`/`.db-wal`/`.db-shm``agent.db` 是项目状态而不是凭据转储)。扫描入口 `src-tauri/src/project_snapshot/scan.rs` 切到新口径。
- 边界:`.agent` 内部的版本库 / 依赖 / 构建目录、凭据目录(`.ssh``credentials``secrets` 等)、`.env*``.pem`/`.key`/`.sql` 等敏感后缀继续排除;符号链接与重解析点照旧在扫描阶段跳过;单文件 64 MiB、单次 512 MiB、单项目 2 GiB 上限不变。项目索引、checkpoint、Agent 上下文与 git 检查继续使用 `should_skip_project_snapshot_path`(仍排除整个 `.agent`)——本变更只放开快照同步。
- 代价与取舍:`.agent` 里的会话记录、运行日志与诊断快照会随项目离机并写入该用户自己的私有前缀,这是“可还原”的代价,属于本轮产品决定。服务端不需要改动:快照路径校验只检查路径形状,后台归档按清单逐文件打包,都不含 `.agent` 特判。模板包导入门禁(`IMPORT_FORBIDDEN_SEGMENTS`)与 CLI 模板发布门禁继续拒绝 `.agent`,不会因为这份 ZIP 而放宽。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs``src-tauri/src/project_snapshot/{scan.rs,tests.rs}`;主规范 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的“2026-09-17 AGC 项目定时快照上传”与“后台工程列表与下载”两节;`docs/project-memory/plans/` 的两份里程碑与实施计划。
- 验证方式:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot`24 passed、0 failed、1 ignored),新增用例覆盖 `.agent` 整目录进入候选集、`.agent` 内凭据与 `.env*` 仍被排除、项目索引与 checkpoint 口径不变;另用同一份规则复算 15 个本机 AppData 项目的 487 个 `.agent` 普通文件,0 个仍落在排除集。
- 运行时证据(2026-09-22):真实项目副本(`.agent` 164 文件 / 5.61 MiB`projectId=gameagent-agentsmoke1`)经真实差异引擎 → 本地 api-server → 真实 OSS `agc-dev``uploaded=179`(= 原口径 15 个项目文件 + 164 个 `.agent` 文件)/18,991,590 字节、`synced` 后第二轮 `no-op`;只读 GET 远端 `agc/project-snapshots/v1/<user>/gameagent-agentsmoke1/manifest.json``files=179``.agent/**` 164 条)、HEAD 命中 `agent.db` 等对象;后台 download 的 ZIP 含 179 条目(164 条 `.agent/`),抽样文件 SHA-256 与本地一致。注意该 bucket 仍是 `v1/` 键布局:本地 `api-server.exe`2026-09-21 14:06)早于渠道分区提交 `4951b71d7`16:57),重建重启后才会写 `v2/{channel}/`
## 2026-09-21 合并 origin/masterSupervisor 永久退役,策划 V1V2 退役落到当前两条产品路径
- 背景:`refactor/split-direct-project`DirectProject 独立聊天容器)与 `origin/master`#355 退役策划 Agent V1/V2)在 2026-09-18 之后各走一条线:本分支删掉 Supervisor 前端链路、把立项策划收敛到 `view/project-development/planning/`master 删掉整套策划 V1/V2(前端会话 / 审批卡 / 适配器 / 类型与 Rust `planning_*_v2` 命令、`planning_gdd_model.rs``planning_policy_v2.rs``planning_session_v2.rs`)只保留 Design Agent。两边都在删 Supervisor,冲突集中在 `App.tsx`、聊天视图(`PlanningChatView``DirectProjectTurn``ToolCallGroup`)、Direct composer / 引用输入区、`styles.css`、Rust direct user item 与 appSurface 用例。
@@ -1824,7 +1824,8 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
- 失败关闭:单个文件失败不推进该文件的索引项,失败文件与剩余文件在下一次周期或下次关闭时重试。鉴权失败(401/403)与格式类拒绝(400/413)是确定性失败,停止本轮剩余请求并等待用户处理后重试;服务端配额或频率拒绝(429)与传输类失败按可重试处理。
- 配额与限流:单文件 64 MiB、单次同步上传预算 512 MiB(超出部分延后到下一次)、单项目常驻上限 2 GiB(客户端在扫描后先判,超限直接给出明确失败;服务端按清单累计体积复核并返回 413);服务端按用户做进程内小时配额(文件 3000 次、清单 120 次)并对同一项目强制 5 秒最小清单间隔,超限返回 429 且带 `Retry-After`。进程内配额只用于抑制异常客户端与失控重试,跨节点配额由"单项目上限 + 清单引用回收"保证。
- 生命周期:同步有界超时(单文件与整次同步分别设上限),项目关闭与应用退出路径不因同步失败而阻塞或延迟退出超过超时上限。
- 上传内容边界:复用项目索引与 checkpoint 同一份 `should_skip_project_snapshot_path` 口径——整个 `.agent`(含 runtime、logs、checkpoint、manifest、project.lock)、版本控制目录、`node_modules`/`target`/`dist`/`build`/`coverage`/`.cache`、凭据目录`.pem`/`.key` 等敏感后缀都不参与同步;符号链接与重解析点同样跳过。单文件(64 MiB)与单次同步总量(512 MiB)各有上限,超限文件进入跳过或延后清单而不是静默丢弃。
- 上传内容边界:`.agent` 是项目身份与 Agent 状态的权威位置,**整目录参与同步**(`manifest.json``agent.db` 及其 WAL/SHM、conversations、logs、checkpoint、workbench、`project.lock``.manifest.json.lock`)。因此快照同步使用独立口径 `should_skip_project_snapshot_sync_path`:它与 `should_skip_project_snapshot_path` 共用同一份组件与后缀规则,只额外放行 `.agent` 组件本身,以及 `.agent` 内的 Agent 状态数据库后缀(`.db`/`.db-wal`/`.db-shm`)。其余排除项在 `.agent` 内同样生效——版本控制目录、`node_modules`/`target`/`dist`/`build`/`coverage`/`.cache`、凭据目录`.pem`/`.key`/`.sql` 等敏感后缀与 `.env*` 都不参与同步;符号链接与重解析点在扫描阶段跳过(不能被当作普通文件传输)。单文件(64 MiB)与单次同步总量(512 MiB)各有上限,超限文件进入跳过或延后清单而不是静默丢弃。
- 该口径只对快照同步生效:项目索引、checkpoint、Agent 上下文与 git 检查继续排除整个 `.agent`。收录 `.agent` 意味着会话记录、运行日志与诊断快照随项目离机并写入该用户自己的私有前缀,这是本轮产品决定(远端工程包要能还原项目身份与 AGC 历史),不是遗漏。
### 契约与兼容
@@ -1848,7 +1849,7 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
- `GET /admin/api/project-snapshots/{userId}/{projectId}/download` 只读取该用户/项目的固定清单与其引用对象,返回 `application/zip` 附件。ZIP 中路径直接使用原始相对路径,不包含 userId、摘要目录或 OSS 前缀;名称使用经过安全处理的项目名和 revision。下载固定本次读到的清单,远端并发回收导致对象缺失则整体失败,不能静默遗漏。
- `partial` 快照下载返回 409`unverified` 历史快照可导出已同步文件,列表明确显示“完整性未知”,动作称“下载已存文件”。`ready` 才显示“下载完整工程”。ZIP 构建核验每一文件的长度与 fnv1a64 摘要,拒绝穿越、绝对路径、重复/大小写冲突路径、非法项目身份;缺失或损坏整体失败,不返回成功的残缺 ZIP。
- ZIP 使用服务端临时文件并限制并发,不将 2 GiB 工程整体驻留内存;成功、失败、客户端取消均清理临时文件。单文件、总量、文件数沿用上传上限,超限明确拒绝。零字节工程文件可以上传和导出。OSS 凭据与签名不下发浏览器,列表失败保留错误而非伪造空列表。
- 工程归档范围与上传策略一致:源码、素材引擎配置按原路径保存,依赖/构建缓存、凭据、`.agent` 会话运行日志排除;此 ZIP 不声明能恢复 AGC 对话历史。后台读取需要目标前缀的 ListObjects 与 GetObject 权限,不新增数据库表,不改 External API。
- 工程归档范围与上传策略一致:源码、素材引擎配置`.agent`(含会话运行日志、checkpoint、`agent.db`)都按原相对路径保存,依赖/构建缓存与凭据排除;因此该 ZIP 可以还原项目身份与 AGC 对话历史(归档只保证字节还原,不承诺跨机器可用性)。后台读取需要目标前缀的 ListObjects 与 GetObject 权限,不新增数据库表,不改 External API。
- 验收覆盖真实单窗口生命周期登记、首次/增量/零字节上传、后台列表分页和权限、ZIP 解压目录及摘要、部分/历史清单、缺失对象、路径拒绝、下载取消清理,并分别报告定向测试和真实环境证据。
### 未决问题