为 AGC 新增项目定时快照上传(目标 OSS agc-dev)
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m21s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m4s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m9s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m48s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m32s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m48s
Project CI / Frontend tests (pull_request) Successful in 4m18s
Project CI / Repository checks (pull_request) Successful in 5m32s
Project CI / Native shell tests (pull_request) Successful in 8m12s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m1s
Project CI / Backend tests (pull_request) Successful in 12m32s

- 新增客户端 project_snapshot 模块:项目扫描与排除口径、增量索引、差异对比、上传编排与状态查询
- 新增周期定时器与工作区窗口关闭触发;应用退出只做有界等待,不重复发起同步
- 新增 api-server 两条登录态路由:单文件上传与清单覆盖写入,落在服务端私有前缀内
- platform-oss 新增内部对象精确写入与探测、项目快照对象键构造,并修复 HEAD 读取长度恒为 0
- shared-contracts 新增 agc_project_snapshots DTO 与项目 ID、相对路径、摘要校验
- 新增真实 OSS 存储层冒烟示例与客户端真实链路冒烟用例(默认忽略)
- 登记 check-config 的 native-only 命令白名单,恢复 npm run agc 可启动
- 同步主规范、里程碑、实施计划、开发运维文档与 .env.example
This commit is contained in:
kdletters
2026-09-17 15:45:34 +08:00
parent ebc5dfe7db
commit c722fd844b
24 changed files with 3093 additions and 6 deletions
@@ -0,0 +1,43 @@
# AGC 项目定时快照上传实施计划
Version: 1.0
Status: active
Date: 2026-09-17
Parent Milestone: `【里程碑】AGC项目定时快照上传-2026-09-17.md`
## 修改边界
1. `server-rs/crates/shared-contracts/src/`:新增 `agc_project_snapshots` DTO(单文件上传请求/响应、同步清单信封),只放共享字段,不放 OSS 细节。
2. `apps/ai-game-creator-shell/src-tauri/src/project_snapshot/`:新增客户端模块,包含扫描与排除规则、索引读写、差异对比、上传编排、状态与日志;不修改 `project/` 下既有 manifest 与写锁语义。
3. `apps/ai-game-creator-shell/src-tauri/src/main.rs`:注册新模块、命令与生命周期钩子;`windows.rs` 的窗口关闭与应用退出路径接入触发调用,不改变现有窗口创建/关闭顺序。
4. `server-rs/crates/platform-oss/src/lib.rs`:新增项目快照私有前缀常量与(必要时)独立 bucket 配置入口;不改动既有前缀枚举语义与资源写路径。
5. `server-rs/crates/api-server/src/project_snapshots.rs`:新增路由、鉴权、校验与 OSS 写入;不改动 `error_reports` 与 `assets` 既有路由。
6. `.env.example`:补充 `GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_*` 说明与默认值。
7. `docs/`:主规范已更新;完成后把持久结论合并回主规范并删除本计划。
## 实现顺序
1. 先写 `shared-contracts` DTO 与客户端差异引擎(扫描、排除、索引、diff)及单测,此时无网络依赖,可独立验证。
2. 接上传编排:按差异集合逐文件提交,成功后再提交清单,最后推进索引;用本地 TCP stub server 覆盖成功、幂等跳过、鉴权失败与部分失败路径。
3. 接触发接线:周期定时器、工作区窗口关闭与应用退出;确认关闭路径的有界超时和串行化。
4. 最后接服务端路由与 OSS 写入,补参数校验与幂等跳过测试;服务端完成前客户端按"未配置即失败关闭、不写入索引"处理。
每一步都保留既有失败关闭行为;新模块默认不改变其它同步路径(Runner、项目写锁、Resource Editor)。
## 验证命令
- `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot -- --nocapture`
- `cargo test -p api-server --bin api-server project_snapshots -- --nocapture`
- `cargo fmt -p api-server -p shared-contracts -p platform-oss -- --check`
- `npm run --prefix apps/ai-game-creator-shell typecheck`(若触及前端)
- `npm run check:encoding`
- `git diff --check`
- 运行时按需:`npm run agc` 打开项目观察索引写入与同步日志,关闭窗口确认关闭触发。
## 风险与回滚
- 上传体积与带宽:首轮全量可能很大,先设单文件与单次同步总量上限并把超限项记入跳过清单;不静默截断。
- 数据出境边界:只上传项目目录内普通文件,排除 `.agent/runtime`、`.agent/logs`、`.git`、构建产物与临时文件;凭据类文件不在白名单内。
- 服务端未配置 bucket 时客户端必须失败关闭,不能把本地索引推进成"已同步",否则后续同步会漏传。
- 回滚:客户端可停用触发接线(保留模块与测试)即可回到无上传行为;服务端路由与配置项可单独移除,不影响既有 OSS 前缀与错误报告链路。
@@ -0,0 +1,45 @@
# AGC 项目定时快照上传
Version: 1.0
Status: active
Date: 2026-09-17
Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的“2026-09-17 AGC 项目定时快照上传(agc-dev)”
## 目标
AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev`,并在项目关闭时立即补一次同步;只上传内容发生变化的文件,重复内容不重复上传,远端缺少对应对象时才新建。
## 范围
- 客户端 `src-tauri/src/project_snapshot/`(`scan.rs` / `diff.rs` / `index.rs` / `transport.rs`):项目扫描、排除规则、增量索引与差异对比、上传编排、状态查询。
- 触发接线:工作区窗口存活周期定时器、工作区窗口关闭(`CloseRequested`);应用退出只做有界等待,不重复发起同步。
- 服务端 `POST /api/agc/project-snapshots/files` 与 `POST /api/agc/project-snapshots/manifest`:登录态鉴权、参数校验、私有前缀 OSS 写入、HEAD 幂等跳过。
- 目标 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`、构建与依赖目录、凭据目录、敏感后缀、符号链接与重解析点)。
## 不做
- 不做云端下载/恢复、跨设备合并、版本回滚。
- 不做远端多余对象清理与生命周期策略下发。
- 不新增 SpacetimeDB 表或 procedure,不修改 `/api/external/v1` 与 External OpenAPI。
- 不新增面向用户的上传设置面板与进度 UI。
## 验收标准
1. 首次同步上传项目内全部符合条件的普通文件;再次同步在无改动时上传 0 个文件。
2. 只修改一个文件时,差异集合恰好包含一个修改项;删除一个文件时上传集合为空且清单中不再包含该文件。
3. `(字节数, 修改时间)` 未变的文件复用已存摘要,不重复读取内容计算摘要。
4. 排除规则命中项(`.agent/runtime`、`.agent/logs`、`.git`、`node_modules`、构建产物、临时文件、符号链接)与超限文件进入跳过清单,不进入上传集合。
5. 任一次同步失败(非鉴权类)不推进本地索引,下一次触发重算并重试;鉴权/权限类失败不自动重试。
6. 同一项目的并发触发串行执行,不产生两路重复上传。
7. 工作区窗口关闭与应用退出都会触发一次同步,且关闭路径不因同步失败而阻塞退出超过超时上限。
8. 服务端拒绝越界 `projectId`、相对路径与摘要;相同摘要重复提交走跳过分支且不写入新对象。
9. 新增日志与错误文案不含 Access Token、AccessKey、绝对路径与项目内容。
## 依赖
- 现有 `platform_session`(用户身份与 Access Token)、项目 manifest(稳定 `project_id`)。
- 现有 `platform-oss`(PUT/HEAD、私有访问)、`api-server` 登录态中间件与 `shared-contracts`。
- 现有 AppData 私有文件写入与目录解析工具。
@@ -1524,3 +1524,49 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
- 两个窗口同时对同一项目发起 Runtime 写请求时,用户体验仍由项目级写锁串行决定;本次不引入跨窗口排队提示。
- 平台会话在窗口间传播依赖共享 localStorage 与 Runner 权威;渲染层不做跨窗口事件推送,另一个窗口在下一次会话校验或刷新时收敛。
## 2026-09-17 AGC 项目定时快照上传(agc-dev)
### 目标与非目标
- 目标:AGC 在项目工作区打开期间按固定周期把用户项目增量上传到 OSS `agc-dev`,并在项目关闭(工作区窗口关闭、切回启动器、应用退出)时立即补一次同步;重复内容不重复上传。
- 非目标:不做云端下载/恢复、不做跨设备合并、不做远端多余对象清理、不新增 UI 面板、不修改 `/api/external/v1` 与 OpenAPI、不新增 SpacetimeDB 表。
- 非目标:不把 OSS AccessKey 放进客户端;客户端不直连 OSS。
### 参与入口、状态与跨模块边界
- 触发入口有两个:工作区窗口 `main` 存活期间的周期定时器、工作区窗口关闭事件(`CloseRequested`)。两者共用同一个进程内同步器,同一项目的同步串行执行,周期触发在已有同步进行时直接让位,不排队堆积。
- 应用退出(`RunEvent::Exit`)不重复发起同步:该时刻窗口已销毁,按窗口重新枚举项目只会得到空集;退出路径只负责在有界预算(15 秒)内等待在途同步收尾,让关窗触发的那一次同步能写完索引再退出。
- 客户端扫描、差异对比、索引持久化与上传编排都在 Tauri Rust 进程(`src-tauri/src/project_snapshot/`);WebView 只读状态,不参与差异计算。
- 本地索引是增量对比的唯一依据:`<AppData>/project-snapshots/<projectId>/index.json` 保存上次成功同步的相对路径、`sha256`、字节数和修改时间。项目根使用现有 manifest 的稳定 `project_id` 作为远端身份,路径不再作为身份。
- 远端写入经 `api-server`,客户端只持平台登录态 Access Token。两条登录态路由:`POST /api/agc/project-snapshots/files`(单文件,正文为原始字节,元数据走查询串)与 `POST /api/agc/project-snapshots/manifest`(本次同步后的完整清单)。
- 对象键与清单由服务端决定:文件键为 `agc/project-snapshots/v1/{userId}/{projectId}/files/{sizeBytes}-{checksumDigest}/{relPath}`,清单键为 `agc/project-snapshots/v1/{userId}/{projectId}/manifest.json`。键里带字节数与摘要,因此"对象已存在且长度一致"可以作为内容一致的判据;路径按原始大小写保留,不走 `put_object` 的低位规范化。`agc` 前缀继续是服务端专用私有前缀,通用对象键解析与客户端直传票据都不覆盖它。
- 目标 bucket 使用独立配置 `GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_BUCKET` / `_ENDPOINT` / `_ACCESS_KEY_ID` / `_ACCESS_KEY_SECRET`,默认 `agc-dev` + `oss-rg-china-mainland.aliyuncs.com`,未配置时回退 `ALIYUN_OSS_*`;与"资源 bucket 与备份 bucket 分离"的既有口径一致。
### 正常、失败、重试与幂等行为
- 差异对比口径:先按 `相对路径 + 字节数 + 修改时间` 判定是否候选变更,命中旧记录则复用已存 `sha256`,只有 `(size, mtime)` 变化才重算摘要。产出新增、修改、删除三类集合,只上传新增与修改的文件。
- 每次成功同步的最后一步上传该项目的 `manifest.json`(当前全量文件清单:相对路径、摘要、字节数、同步序号)。删除文件只在清单中消失,本期不删除远端对象;远端清理留给后续里程碑。
- 幂等:同一摘要与字节数的对象重复提交由服务端 HEAD 校验后跳过;探测失败按"未存在"处理并照常 PUT,宁可多传一次也不漏传。索引只在清单写入成功后推进,失败时保留旧索引以便下次重算。
- 失败关闭:单个文件失败不推进整次同步的完成位,失败文件与剩余文件在下一次周期或下次关闭时重试。鉴权失败(401/403)、权限、额度与身份类失败不做自动重试,只记录分类结果并等待用户重新登录后的下一次触发。
- 生命周期:同步有界超时(单文件与整次同步分别设上限),项目关闭与应用退出路径不因同步失败而阻塞或延迟退出超过超时上限。
- 上传内容边界:复用项目索引与 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)各有上限,超限文件进入跳过或延后清单而不是静默丢弃。
### 契约与兼容
- 新增登录态内部路由 `POST /api/agc/project-snapshots/files`,请求 DTO 放在 `shared-contracts`;不属于 `/api/external/v1`,因此不更新 External OpenAPI,与 `/api/error-reports` 同类。
- 服务端校验 `projectId` 形态(拒绝路径分隔符、`..`、控制字符与超长值)、相对路径规范(正斜杠、拒绝绝对路径与穿越)、摘要形态(64 位十六进制)和字节数上限,任何越界返回 4xx 而不是写入 OSS。
- 不改变客户端与 Runner 的本机协议、平台会话语义、项目写锁与 manifest 结构;新增索引文件位于 AppData,不进入用户项目目录。
### 验收标准与证据来源
- 定向 Rust 测试:首次同步全量、仅改一个文件时只产生一个修改项、删除文件只体现在清单、`(size,mtime)` 未变时复用旧摘要、排除规则与上限跳过、同步失败不推进索引、同一项目并发触发串行化。
- 服务端测试:越界 `projectId`/相对路径/摘要被拒;相同摘要重复提交走跳过分支;鉴权缺失返回 401;OSS 未配置返回明确的 5xx 而不是写入空对象。
- 运行时 smoke:AGC 开发态打开项目、观察索引写入与同步日志、关闭工作区窗口后确认关闭触发的那次同步执行;报告为"客户端 diff 已验证 / 服务端已配置环境联调"两层,不合并成一句"已通"。
- 边界:新增日志与错误文案不含 Access Token、AccessKey、绝对路径与项目内容。
### 未决问题
- 远端删除对象清理、配额与保留策略未定;本期只写清单,OSS 侧对象只增不减。
- 目标 bucket 的私有前缀权限与生命周期规则(例如转低频/过期删除)需要在部署环境确认后单独收口。
- 大项目(素材数量多、单文件大)的首轮全量上传耗时与带宽占用未实测;必要时后续里程碑引入并发上限与断点续传。
@@ -540,6 +540,43 @@ curl -fsS --max-time 5 http://127.0.0.1/api/editor/showcase/resources >/dev/null
角色动画源帧 PUT、透明帧 PUT 和最终帧 HEAD 使用 `AppState` 内同一个 OSS HTTP Client/连接池,并受进程级 8 路 OSS permit 保护;BgFilter、阿里云抠图和本地处理不占用该 permit。每个 OSS attempt 最多 3 次(首次 + 2 次重试),退避为 250ms、500ms;只重试 timeout、无 HTTP 响应传输错误、OSS PutObject 的 `400 + RequestTimeout`、PUT 400 错误体读取失败(未解析出 `Code`,按 timeout/transport 归类)、408、429 和 500–599。动作帧 PUT 收到 400 时只读取最多 16 KiB OSS 错误 XML,提取 `Code` 和 `RequestId`;`oss_request_id` 优先使用响应头 `x-oss-request-id`,XML 字段只作回退。错误体读取超时/断流不再按确定性 400 处理:已解析出的 `Code` 优先生效;未解析出 `Code` 时按读取失败原因置 `timeout`/`transport` 并重试,message 追加「错误响应体读取失败」。日志字段包括 `frame_index`、`object_key`、`operation=source_put|final_put|final_head`、`attempt`、`max_attempts`、`retryable`、`will_retry`、`retry_delay_ms`、`permit_wait_ms`、`timeout`、`connect`、`transport`、`oss_code`、`oss_request_id`、`status` 和 `elapsed_ms`。`请求 OSS 失败` 时,`timeout/connect/transport=true` 表示传输类失败;`status=400, oss_code=RequestTimeout, timeout=true`、`status=429` 或 `500–599` 表示暂时性失败,PUT 的 `status=400`、`oss_code` 为空且 `timeout=true` 或 `transport=true`(message 含「错误响应体读取失败」)同样是暂时性失败。除 `RequestTimeout` 和该错误体读取失败两类例外外,其他 400、401/403/404、配置、URL 和签名错误是确定性失败,不会重试。最终帧 HEAD 失败只会重试 HEAD,不会重复 PUT;如果任一帧最终失败,确认整段动作已排空已启动 Future,并检查任务按现有契约退款且没有发布缺帧动画。
### AGC 项目快照上传目标
AGC 客户端按周期与项目关闭时机把用户项目增量上传到 `agc-dev`。客户端只持有平台登录态 Access
Token,经 `POST /api/agc/project-snapshots/files`(单文件原始字节)与
`POST /api/agc/project-snapshots/manifest`(本次同步清单)交给 `api-server`,由服务端写入私有前缀
`agc/project-snapshots/v1/{user}/{project}/`;客户端不直连 OSS,也不持有 OSS 凭据。
```env
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_BUCKET=agc-dev
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ENDPOINT=oss-rg-china-mainland.aliyuncs.com
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ACCESS_KEY_ID=
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ACCESS_KEY_SECRET=
```
专用凭据为空时回退 `ALIYUN_OSS_ACCESS_KEY_ID` / `ALIYUN_OSS_ACCESS_KEY_SECRET`,bucket 与 endpoint
仍默认指向 AGC 发行 bucket,因此回退凭据必须具备目标 bucket 该前缀的 `PutObject` 权限;`api-server`
启动时凭据缺失或只配一半会跳过该客户端,接口返回 `503`,客户端按失败关闭处理:不写空对象,也不推进
本地增量索引,下一次触发重算重试。远端对象只增不减,删除与生命周期规则尚未落地,需要单独收口。
两层可重复的现场验证:
```bash
# 1. 存储层:直接对真实 bucket 做内部前缀写入、读回与清理,并在结束时删除探针对象。
cargo run -p platform-oss --example agc_project_snapshot_live_smoke --manifest-path server-rs/Cargo.toml
# 2. 客户端链路:真实差异引擎 → 本地 api-server → 真实 OSS。第一次必须 synced 且上传 > 0,
# 紧接着的第二次必须 no-op 且上传 0 个文件;需要先取得登录态并指定项目与索引目录。
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml \
project_snapshot_live_sync -- --ignored --nocapture
```
客户端冒烟读取 `GENARRATIVE_AGC_PROJECT_SNAPSHOT_LIVE_PROJECT`(项目绝对路径,建议用可丢弃的副本)、
`..._LIVE_TOKEN`、`..._LIVE_API_BASE_URL`、`..._LIVE_USER_ID` 与 `..._LIVE_CONFIG_DIR`(索引目录,
`cargo test` 进程没有窗口 setup 初始化 AppData 配置目录,必须显式指定);未设置时用例自我跳过。
本地联调可用 `npm run dev:api-server` 起 api-server,并用密码登录(开发态默认允许未知手机号自动注册)
取得 Access Token。
## 生产运维
生产部署当前口径: