修复 AGC 换号后整项目快照重传:索引按账号分桶,不再整项目重发(#504) #505

Closed
suzmii wants to merge 2 commits from fix/api-timeout into master
Member

关联 #504(本 PR 修的就是它定位到的根因 A),下游症状见 #490。

问题

换号后打开项目会整项目重传一遍:真机复现(CDP attach dev 客户端 + 点「春卷冲刺」)是一次同步重传 2311 个文件、约 3 分钟、2311 次 POST /api/agc/project-snapshots/files(其中 2306 次服务端 HEAD 命中,纯白跑),本地 api-server 日志被刷屏;同窗口内 /api/runtime/frontend-config、/api/llm/models、/api/profile/recharge-center 在服务端 200 且 ≤61ms 返回的情况下被客户端报「请求超时(15000 ms)」,界面同时失去响应。

原因

<AppData>/project-snapshots/<projectId>/index.json 整份只保存一个账号的基线,同步前用 previous.user_id == session.user_id 判等,不等就换成空基线;而远端对象键第一段正是 userId。于是:

  • 账号 A 留下的基线与当前账号 B 不匹配 → B 被当成「本机没有基线」→ 全量上传;
  • B 同步后又把 A 的基线覆盖掉 → 切回 A 再全量一次。

本机 6 个索引文件里有 4 个不同 userId,09-23 19:47 / 09-23 20:47 / 09-24 10:57 三次同形态全量重传,服务端 logs/api-server/*.log 与客户端 diagnostics/application.log 的 project_snapshot.sync.* 两侧对得上。白跑请求本身完全静默(HEAD 命中即 200 + skipped),只有服务端日志量能看出来;客户端 15s 超时的那一层在 webview 的定制协议 IPC(刷屏时 IPC custom protocol failed … TypeError: Failed to fetch ≈0.7 次/秒,空闲时段 847 行日志里只有 59 次)。

改动

  1. project_snapshot/index.rs:索引文件升级为 schema v2,结构改为 baselines: { userId → 该账号基线 }。读只读当前账号的桶,写只替换当前账号的桶。
  2. project_snapshot/mod.rs:同步改用 read_project_snapshot_baseline / write_project_snapshot_baseline,删掉 user_id 判等换空索引的分支;ProjectSnapshotStateView 增补 baselineCount / baselinePresent,并把 fileCount / syncRevision / syncedAtMs 改成当前登录账号的口径(此前会把另一个账号的基线报成本账号的)。
  3. 迁移:读取时按 JSON 的 schemaVersion 选分支,v1 文件按它记录的 userId 迁移进对应桶(userId 为空或 projectId 不符则丢弃),v1 缺失的 projectName / pendingFiles 迁移后仍是「完整性未知」,不伪造已完成。文件保持 v1 形态直到下一次成功同步写回 v2,所以升级本身不产生额外重传。
  4. 用例:新增「账号切换后基线互不覆盖」「切回旧账号差异为空(不重传)」,把原索引往返用例改用基线 API,把原「legacy 索引完整性未知」用例改写成「v1 索引迁移进对应账号桶」。
  5. 文档:快照主规范、快照里程碑(含新增验收项 13)、docs/project-memory/shared-memory/decision-log.md 与 pitfalls.md。

验证

  • cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot
  • cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check
  • npm run check:encoding、git diff --check

未覆盖(另开事项)

一次「项目 + 新账号」的新组合仍会把项目全量发给服务端(服务端逐个 HEAD 跳过)。要彻底消掉这批白跑请求,需要客户端可读的远端清单或批量存在性探测;定制协议 IPC 在压力下的失败原因也还没定位(#504 的「待补证据」)。

关联 #504(本 PR 修的就是它定位到的根因 A),下游症状见 #490。 ## 问题 换号后打开项目会**整项目重传**一遍:真机复现(CDP attach dev 客户端 + 点「春卷冲刺」)是一次同步重传 2311 个文件、约 3 分钟、2311 次 `POST /api/agc/project-snapshots/files`(其中 2306 次服务端 HEAD 命中,纯白跑),本地 api-server 日志被刷屏;同窗口内 `/api/runtime/frontend-config`、`/api/llm/models`、`/api/profile/recharge-center` 在服务端 200 且 ≤61ms 返回的情况下被客户端报「请求超时(15000 ms)」,界面同时失去响应。 ## 原因 `<AppData>/project-snapshots/<projectId>/index.json` 整份只保存**一个**账号的基线,同步前用 `previous.user_id == session.user_id` 判等,不等就换成空基线;而远端对象键第一段正是 `userId`。于是: - 账号 A 留下的基线与当前账号 B 不匹配 → B 被当成「本机没有基线」→ 全量上传; - B 同步后又把 A 的基线覆盖掉 → 切回 A 再全量一次。 本机 6 个索引文件里有 4 个不同 `userId`,09-23 19:47 / 09-23 20:47 / 09-24 10:57 三次同形态全量重传,服务端 `logs/api-server/*.log` 与客户端 `diagnostics/application.log` 的 `project_snapshot.sync.*` 两侧对得上。白跑请求本身完全静默(HEAD 命中即 `200 + skipped`),只有服务端日志量能看出来;客户端 15s 超时的那一层在 webview 的定制协议 IPC(刷屏时 `IPC custom protocol failed … TypeError: Failed to fetch` ≈0.7 次/秒,空闲时段 847 行日志里只有 59 次)。 ## 改动 1. `project_snapshot/index.rs`:索引文件升级为 schema v2,结构改为 `baselines: { userId → 该账号基线 }`。读只读当前账号的桶,写只替换当前账号的桶。 2. `project_snapshot/mod.rs`:同步改用 `read_project_snapshot_baseline` / `write_project_snapshot_baseline`,删掉 `user_id` 判等换空索引的分支;`ProjectSnapshotStateView` 增补 `baselineCount` / `baselinePresent`,并把 `fileCount` / `syncRevision` / `syncedAtMs` 改成当前登录账号的口径(此前会把另一个账号的基线报成本账号的)。 3. 迁移:读取时按 JSON 的 `schemaVersion` 选分支,v1 文件按它记录的 `userId` 迁移进对应桶(`userId` 为空或 `projectId` 不符则丢弃),v1 缺失的 `projectName` / `pendingFiles` 迁移后仍是「完整性未知」,不伪造已完成。文件保持 v1 形态直到下一次成功同步写回 v2,所以升级本身不产生额外重传。 4. 用例:新增「账号切换后基线互不覆盖」「切回旧账号差异为空(不重传)」,把原索引往返用例改用基线 API,把原「legacy 索引完整性未知」用例改写成「v1 索引迁移进对应账号桶」。 5. 文档:快照主规范、快照里程碑(含新增验收项 13)、`docs/project-memory/shared-memory/decision-log.md` 与 `pitfalls.md`。 ## 验证 - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot` - `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` - `npm run check:encoding`、`git diff --check` ## 未覆盖(另开事项) 一次「项目 + 新账号」的新组合仍会把项目全量发给服务端(服务端逐个 HEAD 跳过)。要彻底消掉这批白跑请求,需要客户端可读的远端清单或批量存在性探测;定制协议 IPC 在压力下的失败原因也还没定位(#504 的「待补证据」)。
suzmii added the Kind/Bug
Priority
High
2
labels 2026-09-24 11:24:12 +08:00
suzmii added 1 commit 2026-09-24 11:24:13 +08:00
修复 AGC 换号后整项目快照重传导致的日志刷屏与客户端超时
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m20s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Successful in 4m52s
Project CI / Native shell tests (pull_request) Successful in 5m59s
Project CI / Frontend tests (pull_request) Successful in 1m56s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m0s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m24s
Project CI / Repository checks (pull_request) Successful in 1m50s
3be8e40bc2
- 项目快照索引升级为 schema v2:按账号分桶保存基线,换号不再让其它账号的基线失效,也不会被其它账号覆盖
- 同步只读取并写回当前登录账号的基线,其它账号的基线原样保留
- 读取时按 JSON 版本判定并迁移 v1 单账号索引到它自己记录的 userId 桶,格式升级不额外触发一次重传
- read_local_project_snapshot_state 增补 baselineCount / baselinePresent,fileCount / syncRevision / syncedAtMs 改为当前登录账号的基线口径
- 新增与改写用例:账号切换后基线互不覆盖、切回旧账号差异为空、v1 索引迁移进对应账号桶
- 同步更新快照主规范、快照里程碑文档、shared-memory decision-log 与 pitfalls
suzmii added 1 commit 2026-09-24 12:05:43 +08:00
记录 AGC 快照换号重传修复未落地:根因与迁移约束写入共享记忆,文档回退到现行状态
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m2s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m41s
Project CI / Frontend tests (pull_request) Successful in 2m18s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m22s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m47s
Project CI / Native shell tests (pull_request) Successful in 6m21s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m8s
771965ea40
- decision-log 改写 2026-09-24 条目:只固化根因与「数据与 IO 往后端挪」必须保留的三条约束,注明客户端索引分桶实现保留在分支 fix/api-timeout(3be8e40bc)且 PR #505 已关闭
- decision-log 补充分层结论:本次改动只碰本地持久化格式与 native-only 诊断视图字段,未涉及客户端与服务端的路由、DTO、对象键与清单结构,即不涉及协议层
- pitfalls 同条目把处理口径改为暂缓落地,验证口径改为按分支状态描述
- 主规范索引描述回退为整份只保存一个账号基线、换号后冷启动全量对比,并指向 issue #504 与 decision-log
- 快照里程碑文档回退索引口径,验收项 13 标为随架构迁移重新定义
Author
Member

按「数据与 IO 往后端挪」的架构方向关闭本 PR,不在客户端侧落地。

结论:本 PR 修的那一层(“哪些文件需要上传”的基线判定)正是可以上移到后端的那一层;在客户端本地索引上再落一套,会与迁移后的实现形成两套基线。代码保留在分支 fix/api-timeout(实现 commit 3be8e40bc,定向测试 26 passed / 0 failed),迁移完成后若 #504 描述的问题仍在,可直接复用该分支或重开本 PR。诊断现场数据与复现步骤都在 #504。

分层说明(回应“是否涉及协议层”):本 PR 只碰本地持久化格式(index.json)与一个 native-only 诊断视图的字段(read_local_project_snapshot_state 的 baselineCount / baselinePresent),没有触碰客户端与服务端的路由、DTO、对象键和清单结构,即不涉及协议层。差异判定上移到后端时新增的 manifest / diff 交互才是协议层变更。

交给迁移侧的三条约束(不随实现位置改变):

  1. 基线只能按 (userId, projectId) 两元组归属——远端对象键第一段就是 userId,换号后旧基线对新账号无效,任何新设计都不能跨账号共用或互相覆盖。
  2. 任何一次同步只要清单写入没有成功,就绝不能推进基线,否则下一轮立刻退化为整项目重发(这条在 #504 上已用真机数据验证)。
  3. 本机扫描与读文件无法上移(服务端拿不到用户磁盘):扫描 + 读字节 + 发字节 必须留在客户端,“差异判定”才是可上移的那一层。

已同步记录到共享记忆(docs/project-memory/shared-memory/decision-log.md、pitfalls.md 的 2026-09-24 条目)。

按「数据与 IO 往后端挪」的架构方向关闭本 PR,不在客户端侧落地。 结论:本 PR 修的那一层(“哪些文件需要上传”的基线判定)正是可以上移到后端的那一层;在客户端本地索引上再落一套,会与迁移后的实现形成两套基线。代码保留在分支 `fix/api-timeout`(实现 commit `3be8e40bc`,定向测试 26 passed / 0 failed),迁移完成后若 #504 描述的问题仍在,可直接复用该分支或重开本 PR。诊断现场数据与复现步骤都在 #504。 分层说明(回应“是否涉及协议层”):本 PR 只碰本地持久化格式(`index.json`)与一个 native-only 诊断视图的字段(`read_local_project_snapshot_state` 的 `baselineCount` / `baselinePresent`),**没有触碰客户端与服务端的路由、DTO、对象键和清单结构**,即不涉及协议层。差异判定上移到后端时新增的 manifest / diff 交互才是协议层变更。 交给迁移侧的三条约束(不随实现位置改变): 1. 基线只能按 `(userId, projectId)` 两元组归属——远端对象键第一段就是 `userId`,换号后旧基线对新账号无效,任何新设计都不能跨账号共用或互相覆盖。 2. 任何一次同步只要清单写入没有成功,就绝不能推进基线,否则下一轮立刻退化为整项目重发(这条在 #504 上已用真机数据验证)。 3. 本机扫描与读文件无法上移(服务端拿不到用户磁盘):`扫描 + 读字节 + 发字节` 必须留在客户端,“差异判定”才是可上移的那一层。 已同步记录到共享记忆(`docs/project-memory/shared-memory/decision-log.md`、`pitfalls.md` 的 2026-09-24 条目)。
suzmii closed this pull request 2026-09-24 12:06:32 +08:00
Some checks are pending
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m2s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m41s
Project CI / Frontend tests (pull_request) Successful in 2m18s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m22s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m47s
Project CI / Native shell tests (pull_request) Successful in 6m21s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m8s

Pull request closed

Sign in to join this conversation.