From 771965ea40f999dee21d7a2c2c5c2c2bf6cf10b0 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 24 Sep 2026 12:05:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BD=95=20AGC=20=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E6=8D=A2=E5=8F=B7=E9=87=8D=E4=BC=A0=E4=BF=AE=E5=A4=8D=E6=9C=AA?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=EF=BC=9A=E6=A0=B9=E5=9B=A0=E4=B8=8E=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E7=BA=A6=E6=9D=9F=E5=86=99=E5=85=A5=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=EF=BC=8C=E6=96=87=E6=A1=A3=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E5=88=B0=E7=8E=B0=E8=A1=8C=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - decision-log 改写 2026-09-24 条目:只固化根因与「数据与 IO 往后端挪」必须保留的三条约束,注明客户端索引分桶实现保留在分支 fix/api-timeout(3be8e40bc)且 PR #505 已关闭 - decision-log 补充分层结论:本次改动只碰本地持久化格式与 native-only 诊断视图字段,未涉及客户端与服务端的路由、DTO、对象键与清单结构,即不涉及协议层 - pitfalls 同条目把处理口径改为暂缓落地,验证口径改为按分支状态描述 - 主规范索引描述回退为整份只保存一个账号基线、换号后冷启动全量对比,并指向 issue #504 与 decision-log - 快照里程碑文档回退索引口径,验收项 13 标为随架构迁移重新定义 --- .../【里程碑】AGC项目定时快照上传-2026-09-17.md | 4 ++-- docs/project-memory/shared-memory/decision-log.md | 15 +++++++-------- docs/project-memory/shared-memory/pitfalls.md | 6 +++--- ...方案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/project-memory/plans/【里程碑】AGC项目定时快照上传-2026-09-17.md b/docs/project-memory/plans/【里程碑】AGC项目定时快照上传-2026-09-17.md index e3d046bea..9f94ef36a 100644 --- a/docs/project-memory/plans/【里程碑】AGC项目定时快照上传-2026-09-17.md +++ b/docs/project-memory/plans/【里程碑】AGC项目定时快照上传-2026-09-17.md @@ -16,7 +16,7 @@ AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev` - 服务端 `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、相对路径、摘要校验函数。 -- 客户端增量索引:`/project-snapshots//index.json`,2026-09-24 起为 schema v2,按账号分桶保存各账号基线;换号只让该账号自己没有基线(首次同步该项目才全量),切回旧账号不再重传。v1 单账号索引按它记录的 `userId` 迁移。 +- 客户端增量索引:`/project-snapshots//index.json`,整份只保存一个账号的基线,同步前按用户身份判等,换号后按冷启动全量重算。已知问题:换号后打开项目会把整个工程重传一遍(issue #504),修复随「数据与 IO 往后端挪」的架构迁移一并收口,不在此里程碑内单独落地。 - 排除口径:快照同步使用 `should_skip_project_snapshot_sync_path`(2026-09-22 起)。`.agent` 承载项目身份与 Agent 状态,整目录同步;`.git`、构建与依赖目录、凭据目录、敏感后缀、符号链接与重解析点仍然排除。项目索引、checkpoint、Agent 上下文与 git 检查继续沿用 `should_skip_project_snapshot_path` 的整个 `.agent` 排除口径。 ## 不做 @@ -41,7 +41,7 @@ AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev` 10. 清单写入成功后,上一版清单里不再被引用的对象被回收;上一版清单不可读时整轮不删除任何对象。 11. 单项目超过 2 GiB 时客户端明确失败、服务端按 413 拒绝;超过服务端小时配额或 5 秒最小间隔时返回 429 且带 `Retry-After`。 12. 同步期间被改写的文件既不上传也不推进索引,沿用上一轮记录,且不会被误判成删除。 -13. 账号 A 同步后切到 B 再切回 A,A 的差异集合为空(不重传);B 首次同步该项目仍全量上传;索引在换号后同时保留两个账号的基线。 +13. (暂缓,不在本里程碑内验收)账号 A 同步后切到 B 再切回 A 时 A 的差异集合为空——该口径随「数据与 IO 往后端挪」的架构迁移重新定义;客户端索引分桶修复未落地(issue #504、PR #505 已关闭,实现在分支 `fix/api-timeout`)。 ## 依赖 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 33432924b..90d12d066 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9330,13 +9330,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 边界:SpacetimeDB 表结构与公开契约字段不变(`entryUrl` 仍是 string),只是取值从绝对 URL 变为相对路径;历史版本已冻结的绝对值不改写,admin 页与详情页展示口径不变。线上 dev / release 的 nginx 已按同源路径改动并 reload,`/etc/genarrative/api-server.env` 已删除模板变量;api-server 未重启,新写入要等下次重启。 - 验证:`cargo check -p api-server --tests`、`cargo test -p api-server game_distribution`(27 passed)、`cargo fmt --all --check`、`npx vitest run src/components/game-distribution`(57 passed)、`npm run check:nginx-spa-routes`、`npm run check:encoding`(5060 文件)、`npm run check:doc-index`、`git diff --check` 全部通过;三份 nginx 模板渲染后 `nginx -t` 语法通过;dev 线上实测 `/games/game_2dcd…4955/` 与 `./assets/index-2Ws3zHlS.js` 均 200。 -## 2026-09-24 AGC 项目快照索引按账号分桶:换号不再整项目重传 +## 2026-09-24 AGC 项目快照「换号后整项目重传」的根因与迁移约束(客户端索引修复未落地) - 背景:AGC 客户端在项目打开期间把本地工程增量上传到 OSS,判定依据是 `/project-snapshots//index.json`。该文件整份只保存一个账号的基线,同步前用 `previous.user_id == session.user_id` 判等,不等就换成空基线;而远端对象键第一段正是 `userId`,于是换号被等价成「本机没有基线」。issue #504 的真机复现(CDP attach dev 客户端 + 点「春卷冲刺」):账号 A 留下的基线与当前账号不匹配 → 一次性重传 2311 个文件,约 3 分钟、2311 次 `POST /api/agc/project-snapshots/files`,其中 2306 次服务端 HEAD 命中纯白跑;同窗口内 `/api/runtime/frontend-config`、`/api/llm/models`、`/api/profile/recharge-center` 在服务端 200 且 ≤61ms 的情况下被客户端报 15s 超时(同进程 IPC 被重传压垮),并触发 #490 的「检查失败」钉死。本机 6 个索引文件里有 4 个不同 `userId`,09-23 19:47、09-23 20:47、09-24 10:57 三次同形态全量重传。 -- 决策:索引文件升级为 schema v2,结构改为 `baselines: { userId → 该账号基线 }`;同步只读当前登录账号的桶,写回也只替换自己那一个桶,其它账号的基线原样保留。基线字段与 v1 一致(`syncRevision` / `syncedAtMs` / `projectName` / `pendingFiles` / `files`)。某账号在本机首次同步该项目仍然全量上传,这是必要行为而不是漏洞。 -- 决策(迁移):读取时按 JSON 值判定版本,v1 文件按其记录的 `userId` 迁移进对应桶(`userId` 为空或 `projectId` 不符时丢弃),格式升级本身不产生额外重传;v1 缺少的 `projectName` / `pendingFiles` 迁移后仍表示「完整性未知」,不伪造已完成。文件保持 v1 形态直到下一次成功同步写回 v2。 -- 决策(诊断):`read_local_project_snapshot_state` 增补 `baselineCount` 与 `baselinePresent`,`fileCount` / `syncRevision` / `syncedAtMs` 改为当前登录账号的基线口径——此前这三项会把另一个账号的基线报成本账号的。 -- 影响面:`apps/ai-game-creator-shell/src-tauri/src/project_snapshot/{index.rs,mod.rs,tests.rs}`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/plans/【里程碑】AGC项目定时快照上传-2026-09-17.md`、本文件与 pitfalls。 -- 边界:不新增路由、不改服务端、不改公开契约;远端对象键与清单结构不变;索引仍是 AppData 私有文件、不进项目目录;旧客户端读到 v2 文件会判「版本不符」→ 该账号一次全量,属可接受回退代价。 -- 未覆盖:一次「项目 + 新账号」的新组合仍会把项目全量发给服务端(服务端逐个 HEAD 跳过);要彻底消掉这批白跑请求,需要客户端可读的远端清单或批量存在性探测,另开事项。 -- 验证:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot`(含「账号切换后基线互不覆盖」「切回旧账号差异为空」「v1 索引迁移进对应账号桶」三条新增/改写用例)、`cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check`、`npm run check:encoding`、`git diff --check`。 +- 结论(本条目只固化根因,不定架构):客户端基线分桶的实现已完成并验证,但**未合入 master**——这条链路正被「数据与 IO 往后端挪」的架构迁移覆盖,落地在客户端本地索引上会与该迁移形成两套实现。实现保留在分支 `fix/api-timeout`(commit `3be8e40bc`),对应 PR #505 已按迁移方向关闭;迁移完成后若该问题仍在,可直接复用该分支或重开 PR。 +- 迁移必须保留的约束(不随实现位置改变):① 基线只能按 `(userId, projectId)` 两元组归属,远端对象键第一段就是 `userId`,换号后旧基线对新账号无效,任何新设计都不能跨账号共用或互相覆盖;② 任何一次同步只要清单/清单写入没有成功,就绝不能推进基线,否则下一轮立刻退化为整项目重发(这条已在 #504 上验证);③ 本机扫描与读文件无法上移,服务端拿不到用户磁盘,`扫描 + 读字节 + 发字节` 必须留在客户端,「差异判定」才是可上移到后端的那一层。 +- 分层(回应「是否涉及协议层」):本次改动只碰本地持久化格式(`index.json` schema)与一个 native-only 诊断视图的字段(`read_local_project_snapshot_state` 的 `baselineCount` / `baselinePresent`),**没有触碰客户端↔服务端的路由 / DTO / 对象键 / 清单结构**,即不涉及协议层。若差异判定上移到后端,新增的 `manifest/diff` 交互才是协议层变更,需要按规范单独定义。 +- 边界:不新增路由、不改服务端、不改公开契约;索引是 AppData 私有文件、不进项目目录;分支上的实现只影响 `project_snapshot/index.rs`、`mod.rs`、`tests.rs`,删改范围可控。 +- 验证(分支 `fix/api-timeout` 上已通过,非 master 状态):`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell project_snapshot` 26 passed / 0 failed / 1 ignored(含「账号切换后基线互不覆盖」「切回旧账号差异为空」「v1 索引迁移进对应账号桶」)、`cargo fmt --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。 +- 关联:issue #504、issue #490、PR #505(closed)、`apps/ai-game-creator-shell/src-tauri/src/project_snapshot/index.rs`。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 8c73f5be0..d8cfc2d5f 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5952,7 +5952,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **现象**:打开 AGC 项目后客户端短时间无响应,`/api/runtime/frontend-config`、`/api/llm/models`、`/api/profile/recharge-center` 报「请求超时(15000 ms)」;同时本地 api-server 日志被 `POST /api/agc/project-snapshots/files` 刷屏(全是 200、`latency_ms` 58~61ms、每 60ms 一条)。紧接着项目列表还会被钉成「检查失败」(#490)。 - **原因**:本地快照索引整份只保存一个账号的基线,同步前按 `user_id` 判等,不等就换成空基线 → 换号后打开项目即整项目重传(春卷冲刺 2311 个文件、约 3 分钟)。对象键带内容摘要、服务端 HEAD 命中即 `200 + skipped`,这批请求全是白跑却完全静默,只有服务端日志量能看出来。真正超时的那一层在客户端:同一窗口服务端对三个接口都是 200、≤61ms,而客户端 webview 的 Tauri 定制协议 IPC 在重传压力下大面积失败(`IPC custom protocol failed … TypeError: Failed to fetch`,刷屏时 ≈0.7 次/秒,而空闲时段 847 行日志里只有 59 次;每次失败还写一行 ~330 字节日志),响应回不到 renderer,JS 侧 15s 计时器先到。 -- **处理(现行口径)**:索引升级为按账号分桶(schema v2,见 decision-log 2026-09-24「AGC 项目快照索引按账号分桶」),换号只让该账号自己没有基线,切回旧账号不再重传。 +- **处理(现行口径)**:暂不落地客户端修复——这条链路归入「数据与 IO 往后端挪」的架构迁移,客户端再单独做基线分桶会与之形成两套实现。按账号分桶的实现在分支 `fix/api-timeout`(commit `3be8e40bc`)完成并通过验证,PR #505 已按迁移方向关闭;迁移完成后若该问题仍在,可直接复用该分支或重开 PR。 - **排障口径**:遇到「后端日志刷屏 + 客户端报超时」这种组合,先把两侧对齐到同一时间窗:客户端 `diagnostics/application.log` 的 `project_snapshot.sync.*`(看 `uploaded=` 是否等于项目文件数、`skippedRemote=` 是否接近它)对 `logs/api-server/api-server-*.log` 里各 route 的 `http.response`(状态码与 `latency_ms`)。客户端报超时但服务端 200 且毫秒级返回,说明堵在客户端侧,别只盯后端。 -- **验证**:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot` 里「账号切换后基线互不覆盖」「切回旧账号差异为空」两条用例;运行态可用 `read_local_project_snapshot_state` 看 `baselineCount` / `baselinePresent`(换号前后各持自己的基线)。 -- **关联**:issue #504、issue #490、`apps/ai-game-creator-shell/src-tauri/src/project_snapshot/index.rs`、`docs/project-memory/shared-memory/decision-log.md`(2026-09-24 同名条目)。 +- **验证**:分支 `fix/api-timeout` 上 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell project_snapshot` 26 passed / 0 failed(含「账号切换后基线互不覆盖」「切回旧账号差异为空」);master 状态无该修复,运行态排障仍可用 `read_local_project_snapshot_state` 观察当前账号的 `fileCount` / `syncRevision`。 +- **关联**:issue #504、issue #490、PR #505(closed)、`apps/ai-game-creator-shell/src-tauri/src/project_snapshot/index.rs`、`docs/project-memory/shared-memory/decision-log.md`(2026-09-24 条目含迁移必须保留的三条约束)。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index abc488c4b..73e0b3ed7 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1642,7 +1642,7 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面 - 触发入口为项目生命周期登记、已登记项目的周期定时器及窗口关闭事件(`CloseRequested`)。它们共用同一个进程内同步器,同一项目的同步串行执行,周期触发在已有同步进行时直接让位,不排队堆积。 - 应用退出(`RunEvent::Exit`)不重复发起同步;退出路径只负责在有界预算(15 秒)内等待在途同步收尾,让关窗触发的那一次同步有机会写完索引再退出。超过预算不能声明最后状态已经上传。 - 客户端扫描、差异对比、索引持久化与上传编排都在 Tauri Rust 进程(`src-tauri/src/project_snapshot/`);WebView 只读状态,不参与差异计算。 -- 本地索引是增量对比的唯一依据:`/project-snapshots//index.json`(schema v2)**按账号分桶**保存各账号上次成功同步的相对路径、校验和、字节数和修改时间。远端对象键第一段就是 `userId`,所以基线只对写入它的账号成立——换号不会再让别的账号的基线失效,也不会被别的账号覆盖;只有该账号在本机确实没有基线(首次同步该项目)时才全量上传。v1 的单账号索引在读取时按它记录的 `userId` 迁移进对应桶,格式升级本身不额外触发一次重传。项目根使用现有 manifest 的稳定 `project_id` 作为远端身份,路径不再作为身份。 +- 本地索引是增量对比的唯一依据:`/project-snapshots//index.json` 保存上次成功同步的相对路径、校验和、字节数和修改时间。项目根使用现有 manifest 的稳定 `project_id` 作为远端身份,路径不再作为身份。索引整份只保存一个账号的基线,换号后按冷启动全量对比(已知问题与迁移约束见 issue #504 与 `docs/project-memory/shared-memory/decision-log.md` 2026-09-24 条目)。 - 可观测性按产品口径收敛到本机日志:同步结果、失败分类、延后与跳过计数只写入 AppData 诊断日志(`project_snapshot.sync.*` 前缀),客户端界面不暴露上传状态、时间线或入口按钮。`read_local_project_snapshot_state` 与 `sync_local_project_snapshot` 两条命令仅作为 native-only 的排障与联调入口登记,不在渲染层调用。 - 远端写入经 `api-server`,客户端只持平台登录态 Access Token。两条登录态路由:`POST /api/agc/project-snapshots/files`(单文件,正文为原始字节,元数据走查询串)与 `POST /api/agc/project-snapshots/manifest`(本次同步后的完整清单)。 - 对象键与清单由服务端决定:文件键为 `agc/project-snapshots/v2/{channel}/{userId}/{projectId}/files/{sizeBytes}-{checksumDigest}/{relPath}`,清单键为 `agc/project-snapshots/v2/{channel}/{userId}/{projectId}/manifest.json`;`channel` 是本部署渠道(`GENARRATIVE_AGC_PROJECT_SNAPSHOT_CHANNEL`,缺省沿用 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL`),同一 bucket 因此天然按渠道分区,开发与正式部署互不可见对方项目。键里带字节数与摘要,因此"对象已存在且长度一致"可以作为内容一致的判据;路径按原始大小写保留,不走 `put_object` 的低位规范化。`agc` 前缀(含历史无渠道的 `agc/project-snapshots/v1/`)继续是服务端专用私有前缀,通用对象键解析与客户端直传票据都不覆盖它。后台“项目工程”按渠道查询与下载,渠道名非法时失败关闭,历史 v1 对象不再列出。