AGC 项目快照换号后全量重传 + IPC 定制协议失败,造成客户端 15s 超时与「卡住」 #504

Closed
opened 2026-09-24 11:08:04 +08:00 by suzmii · 1 comment
Member

关联 #490(状态被钉在「检查失败」的下游症状);本 issue 补上 #490 里「停顿发生在哪一层(远端网络 / IPC 回传 / WebView 主线程)尚未定位」的那一层。

现象

  • 在 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:一次抖动后项目列表被钉成「检查失败」、首页最近项目变空、不自愈。

复现(2026-09-24 10:54~10:58,本机 dev 客户端 + 本地 api-server:8082)

操作:CDP attach 客户端 WebView2 → 注入探针 → 点「春卷冲刺」。

证据 内容
客户端 diagnostics/application.log 10:57:37 project_snapshot.sync.completed projectId=gameagent-6e43b49b… trigger=project-open status=synced revision=1 uploaded=2311 skippedRemote=2306
本地 api-server logs/api-server/api-server-20260924-104207.log 02:55–02:58 UTC 共 2313 次 POST /api/agc/project-snapshots/files(按分钟 278 / 1259 / 774),avg 45.8ms,全部 200;整份日志 0 个 4xx/5xx
同一窗口的其它接口(服务端视角) frontend-config 10 次、recharge-center 7 次、llm/models 2 次,全部 200 且 ≤61ms

一次「打开项目」= 2311 个文件全传一遍,耗时约 3 分钟;其中 2306 个服务端 HEAD 命中,纯白跑。

根因 A:换号后本地快照索引判空 → 全量重传

  • 索引 %APPDATA%\world.genarrative.ai-game-creator\project-snapshots\<projectId>\index.json 按项目存放、只记一个 userId;project_snapshot/mod.rs 里 previous.user_id != session.user_id 就用空索引(project_snapshot/index.rs),于是没有基线 → 全量上传。
  • 同机对照:点之前 userId=user_5916a7…、revision=2;点之后写成 userId=user_a404cb29…、revision=1 → 当前登录账号与索引记录的账号不同。
  • 本机 6 个索引文件里有 4 个不同 userId;09-23 19:47、09-23 20:47、09-24 10:57 三次同形态全量(后两次 skippedRemote=2306,远端已有,纯属白跑)。
  • 对象键含内容摘要、服务端 HEAD 幂等命中即返回 200 + skipped,所以这种白跑完全静默,只能靠服务端日志量暴露。

根因 B:15s 超时出在客户端 IPC 回传,不是后端

  • 服务端在同一窗口对这三个接口都是 200、≤61ms,客户端却报 15000ms 超时;昨天 19:45–19:47 的同类窗口同样(2316 次上传,三个接口 max 61/46/49ms、全 200)。
  • 客户端日志同窗口被 IPC custom protocol failed, Tauri will now use the postMessage interface instead TypeError: Failed to fetch 刷屏:当前 application.log 58 行里 51 行是它(10:56:26–10:57:40 ≈ 0.7 次/秒);空闲时段(09-23 16:14 → 10:56,847 行)只有 59 次 → 失败率放大近三个数量级。每次失败写一行 ~330 字节,日志因此约 10 分钟轮转一次(application.previous.log 恰好 262145 字节)。
  • 客户端所有平台请求都走 @tauri-apps/plugin-http(src/services/clientHttp.ts:342),请求与响应都必须穿过这条 IPC;通道在重传压力下退化后响应回不到 renderer,JS 侧 15s 计时器先到,于是表现为「请求超时」,而服务端早已 200 返回。
  • 已排除:页面无 CSP meta(不是 CSP 拦截);探针在 8 分钟窗口内未检出 WebView 主线程 > 250ms 冻结。

待补证据

  • 每个 invoke 的往返耗时分布:__TAURI_INTERNALS__.invoke 与 __TAURI__.core.invoke 都是 non-writable + non-configurable,页面内无法 patch,需要在 Page.addScriptToEvaluateOnNewDocument 里于页面加载前注入并重启一次客户端。
  • 定制协议 POST 失败的具体原因(CSP 已排除):需要抓 Network.loadingFailed 的 blockedReason / corsErrorStatus。

打算怎么改

  1. 索引按账号隔离(project-snapshots/<projectId>/<userId>/index.json),或 userId 变化时不直接判空、先拉远端 manifest 比对;需要客户端可用的只读接口(新增 GET /api/agc/project-snapshots/manifest)。
  2. 上传前做批量存在性探测(或服务端提供批量 probe),消除 2306/2311 这种纯白跑请求。
  3. 修 Tauri 定制协议 IPC 失败:定位 http://ipc.localhost 在负载下失败的原因;把「每次调用一条 warn」降为「每会话一条」,避免日志雪崩与额外写盘。
  4. 重传期间自保:限制上传并发、下调日志级别,避免与 inspectRecentWorkspaceWithRetry 一类重试叠加。

验证

  • 换号后打开同一项目,api-server 侧 project-snapshots/files 请求数应从 2311 降到仅差异文件数,日志不再刷屏。
  • 重传窗口内三个平台接口不再出现 15s 超时;application.log 不再出现 IPC custom protocol failed。
  • 定向:cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot;门禁 npm run check:encoding、git diff --check。
关联 #490(状态被钉在「检查失败」的下游症状);本 issue 补上 #490 里「停顿发生在哪一层(远端网络 / IPC 回传 / WebView 主线程)尚未定位」的那一层。 ## 现象 - 在 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:一次抖动后项目列表被钉成「检查失败」、首页最近项目变空、不自愈。 ## 复现(2026-09-24 10:54~10:58,本机 dev 客户端 + 本地 api-server:8082) 操作:CDP attach 客户端 WebView2 → 注入探针 → 点「春卷冲刺」。 | 证据 | 内容 | | --- | --- | | 客户端 `diagnostics/application.log` | `10:57:37 project_snapshot.sync.completed projectId=gameagent-6e43b49b… trigger=project-open status=synced revision=1 uploaded=2311 skippedRemote=2306` | | 本地 api-server `logs/api-server/api-server-20260924-104207.log` | 02:55–02:58 UTC 共 **2313** 次 `POST /api/agc/project-snapshots/files`(按分钟 278 / 1259 / 774),avg 45.8ms,**全部 200**;整份日志 0 个 4xx/5xx | | 同一窗口的其它接口(服务端视角) | `frontend-config` 10 次、`recharge-center` 7 次、`llm/models` 2 次,**全部 200 且 ≤61ms** | 一次「打开项目」= 2311 个文件全传一遍,耗时约 3 分钟;其中 2306 个服务端 HEAD 命中,纯白跑。 ## 根因 A:换号后本地快照索引判空 → 全量重传 - 索引 `%APPDATA%\world.genarrative.ai-game-creator\project-snapshots\<projectId>\index.json` **按项目存放、只记一个 `userId`**;`project_snapshot/mod.rs` 里 `previous.user_id != session.user_id` 就用空索引(`project_snapshot/index.rs`),于是没有基线 → 全量上传。 - 同机对照:点之前 `userId=user_5916a7…`、`revision=2`;点之后写成 `userId=user_a404cb29…`、`revision=1` → 当前登录账号与索引记录的账号不同。 - 本机 6 个索引文件里有 **4 个不同 userId**;09-23 19:47、09-23 20:47、09-24 10:57 三次同形态全量(后两次 `skippedRemote=2306`,远端已有,纯属白跑)。 - 对象键含内容摘要、服务端 HEAD 幂等命中即返回 `200 + skipped`,所以这种白跑完全静默,只能靠服务端日志量暴露。 ## 根因 B:15s 超时出在客户端 IPC 回传,不是后端 - 服务端在同一窗口对这三个接口都是 200、≤61ms,客户端却报 15000ms 超时;昨天 19:45–19:47 的同类窗口同样(2316 次上传,三个接口 max 61/46/49ms、全 200)。 - 客户端日志同窗口被 `IPC custom protocol failed, Tauri will now use the postMessage interface instead TypeError: Failed to fetch` 刷屏:当前 `application.log` **58 行里 51 行**是它(10:56:26–10:57:40 ≈ 0.7 次/秒);空闲时段(09-23 16:14 → 10:56,847 行)只有 59 次 → 失败率放大近三个数量级。每次失败写一行 ~330 字节,日志因此约 10 分钟轮转一次(`application.previous.log` 恰好 262145 字节)。 - 客户端所有平台请求都走 `@tauri-apps/plugin-http`(`src/services/clientHttp.ts:342`),请求与响应都必须穿过这条 IPC;通道在重传压力下退化后响应回不到 renderer,JS 侧 15s 计时器先到,于是表现为「请求超时」,而服务端早已 200 返回。 - 已排除:页面无 CSP meta(不是 CSP 拦截);探针在 8 分钟窗口内未检出 WebView 主线程 > 250ms 冻结。 ## 待补证据 - 每个 invoke 的往返耗时分布:`__TAURI_INTERNALS__.invoke` 与 `__TAURI__.core.invoke` 都是 non-writable + non-configurable,页面内无法 patch,需要在 `Page.addScriptToEvaluateOnNewDocument` 里于页面加载前注入并重启一次客户端。 - 定制协议 POST 失败的具体原因(CSP 已排除):需要抓 `Network.loadingFailed` 的 `blockedReason` / `corsErrorStatus`。 ## 打算怎么改 1. 索引按账号隔离(`project-snapshots/<projectId>/<userId>/index.json`),或 `userId` 变化时不直接判空、先拉远端 manifest 比对;需要客户端可用的只读接口(新增 `GET /api/agc/project-snapshots/manifest`)。 2. 上传前做批量存在性探测(或服务端提供批量 probe),消除 2306/2311 这种纯白跑请求。 3. 修 Tauri 定制协议 IPC 失败:定位 `http://ipc.localhost` 在负载下失败的原因;把「每次调用一条 warn」降为「每会话一条」,避免日志雪崩与额外写盘。 4. 重传期间自保:限制上传并发、下调日志级别,避免与 `inspectRecentWorkspaceWithRetry` 一类重试叠加。 ## 验证 - 换号后打开同一项目,api-server 侧 `project-snapshots/files` 请求数应从 2311 降到仅差异文件数,日志不再刷屏。 - 重传窗口内三个平台接口不再出现 15s 超时;`application.log` 不再出现 `IPC custom protocol failed`。 - 定向:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot`;门禁 `npm run check:encoding`、`git diff --check`。
suzmii added the Kind/Bug
Priority
High
2
labels 2026-09-24 11:08:04 +08:00
Author
Member

按 #505 的结论收口:不做客户端落地,改由「数据与 IO 往后端挪」的架构迁移覆盖,本 issue 以 Won't Fix 关闭。

现状(master,真机可复现)

打开项目会整项目重传:春卷冲刺 2311 个文件 / 约 3 分钟 / 2311 次 POST /api/agc/project-snapshots/files,其中 2306 次服务端 HEAD 命中(200 + skipped)纯白跑;同窗口内 /api/runtime/frontend-config、/api/llm/models、/api/profile/recharge-center 在服务端 200 且 ≤61ms 的情况下被客户端报「请求超时(15000 ms)」,并触发 #490 的「检查失败」钉死。根因是本地索引整份只保存一个账号的基线,换号被等价成「本机没有基线」。

关闭原因

本 issue 修的「哪些文件需要上传」正是可以上移到后端的那一层(判定归属)。客户端再落一套基线分桶会与迁移后的实现形成两套判定,因此 #505 关闭,实现只保留在分支 fix/api-timeout(commit 3be8e40bc,定向测试 26 passed / 0 failed),迁移完成后若问题仍在可直接复用该分支。

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

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

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

复核与重新打开条件

迁移上线后按原复现步骤核对三条:① api-server 侧 project-snapshots/files 请求数等于真实差异文件数(不再是文件数);② 换号后首次打开该项目不再整项目重传;③ 该窗口内三个平台接口不再 15s 超时、客户端 diagnostics/application.log 不再出现 IPC custom protocol failed。任一条不成立就直接恢复跟踪(排障口径:客户端 project_snapshot.sync.* 与 api-server 各 route 的 http.response 按同一时间窗两侧对齐)。

建议迁移侧有独立的 issue / 里程碑文档承载「数据与 IO 往后端挪」,并在其中引用本 issue 收口——这条链路目前没有对应的规范文档。

@kdletters

按 #505 的结论收口:不做客户端落地,改由「数据与 IO 往后端挪」的架构迁移覆盖,本 issue 以 Won't Fix 关闭。 ## 现状(master,真机可复现) 打开项目会整项目重传:春卷冲刺 2311 个文件 / 约 3 分钟 / 2311 次 `POST /api/agc/project-snapshots/files`,其中 2306 次服务端 HEAD 命中(`200 + skipped`)纯白跑;同窗口内 `/api/runtime/frontend-config`、`/api/llm/models`、`/api/profile/recharge-center` 在服务端 200 且 ≤61ms 的情况下被客户端报「请求超时(15000 ms)」,并触发 #490 的「检查失败」钉死。根因是本地索引整份只保存一个账号的基线,换号被等价成「本机没有基线」。 ## 关闭原因 本 issue 修的「哪些文件需要上传」正是可以上移到后端的那一层(判定归属)。客户端再落一套基线分桶会与迁移后的实现形成两套判定,因此 #505 关闭,实现只保留在分支 `fix/api-timeout`(commit `3be8e40bc`,定向测试 26 passed / 0 failed),迁移完成后若问题仍在可直接复用该分支。 分层说明:本 issue 的客户端改动只碰本地持久化格式(`index.json`)与一个 native-only 诊断视图的字段,没有触碰客户端与服务端的路由、DTO、对象键和清单结构,即不涉及协议层;差异判定上移后新增的 manifest / diff 交互才是协议层变更。 ## 交给迁移侧(不随实现位置改变的三条约束) 1. 基线只能按 `(userId, projectId)` 两元组归属——远端对象键第一段就是 `userId`,换号后旧基线对新账号无效,任何新设计都不能跨账号共用或互相覆盖。 2. 任何一次同步只要清单写入没有成功,就绝不能推进基线,否则下一轮立刻退化为整项目重发(已在真机数据上验证)。 3. 本机扫描与读文件无法上移(服务端拿不到用户磁盘):`扫描 + 读字节 + 发字节` 必须留在客户端,「差异判定」才是可上移的那一层。 ## 复核与重新打开条件 迁移上线后按原复现步骤核对三条:① api-server 侧 `project-snapshots/files` 请求数等于真实差异文件数(不再是文件数);② 换号后首次打开该项目不再整项目重传;③ 该窗口内三个平台接口不再 15s 超时、客户端 `diagnostics/application.log` 不再出现 `IPC custom protocol failed`。任一条不成立就直接恢复跟踪(排障口径:客户端 `project_snapshot.sync.*` 与 api-server 各 route 的 `http.response` 按同一时间窗两侧对齐)。 建议迁移侧有独立的 issue / 里程碑文档承载「数据与 IO 往后端挪」,并在其中引用本 issue 收口——这条链路目前没有对应的规范文档。 @kdletters
suzmii added the
Reviewed
Confirmed
1
label 2026-09-24 12:15:22 +08:00
suzmii reopened this issue 2026-09-24 12:16:30 +08:00
suzmii added
Reviewed
Won't Fix
3
and removed
Reviewed
Confirmed
1
labels 2026-09-24 12:19:25 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#504