到期清理补归属登记,并给提交前置阶段加预算
Project CI / Repository checks (pull_request) Successful in 49s
Project CI / Frontend tests (pull_request) Failing after 2m8s
Project CI / Backend tests (pull_request) Successful in 4m6s
Project CI / Native shell tests (pull_request) Successful in 12m28s

到期清理按状态删除超窗占位,区分不出它属于已死会话还是本会话仍在执行。占位创建
后还要走源图解析/直传和 flush 才轮到 POST,而 120 秒只从最终 POST 开始计——前置
阶段慢起来时,定时器会删掉自己正在用的占位,随后 POST 返回 409。

我写在 hook 注释里的两条安全性论证都是错的:120 秒不覆盖前置阶段;「到期重新
判定兜得住」也不成立,重新判定只能识别已收口的占位,识别不出仍在合法运行的。

补 activeInlineGenerationDialogIdsRef 显式登记归属,创建后紧挨着注册、finally
释放,到期清理跳过。

前置阶段此前完全无界:直传是裸 fetch 无 signal,saveEditorProjectLayout 没传
timeoutMs 而 composeAbortSignal 缺失时不设默认值。给整段 40 秒预算而不是逐请求
加超时——后者最坏会因重试累加到远超窗口。客户端最坏 40 + 120 = 160 秒。
另给 saveEditorProjectLayout 补 60 秒超时,那是独立缺陷。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:24:22 +00:00
parent 170806124b
commit aa8ea401a9
6 changed files with 166 additions and 14 deletions
@@ -6140,3 +6140,15 @@
- 刻意不复制子句字面文本:Rust 常量是真值源,OpenAPI 已复制一份,skill 文档再抄第三份就是把同一事实摊到三处——这次漂移正是这么发生的,只是方向相反。文档改为指向 OpenAPI 并写明「本指南刻意不复制」,让下一个读到的人知道那是有意为之而非遗漏。
- 校验面已确认:这批文档由 `external_skill_api.rs` / `external_mcp.rs``include_str!` 编译期内联,SHA 在运行时从内容算出、测试只断言「算出的与返回的一致」,没有钉死具体摘要,改文档无需同步任何清单。api-server 700 通过 / 3 失败(`wallet_refund_outbox` 本机环境失败,与基线一致)。
- 关联文档:`docs/openapi/genarrative-external-v1.openapi.json`
## 2026-08-03 到期清理误删本会话在途占位:补归属登记与前置阶段预算
- 缺陷:到期清理只按 `generationStartedAt + 180 秒` 删除 `requiresLiveSession``generating` 的占位,区分不出它属于已死会话还是本会话仍在执行。占位在创建后还要走源图解析/直传和 `flushProjectPersistence` 才轮到 POST,而 `snapEditorImageToPixelArt` 的 120 秒**只从最终 POST 开始计**。前置阶段慢起来越过窗口时,定时器会删掉本会话正在用的占位并把删除持久化,随后 POST 因占位不存在返回 `409`;若删除的落库晚于 POST 到达,则 completion 找不到占位返回 `Ok(None)`,结果只进素材库、不落画布。
- 我写在 hook 注释里的安全性论证是错的,两条都错:其一「客户端 120 秒就 abort180 秒时不可能还是 generating」——120 秒不覆盖前置阶段;其二「不依赖该推理,到期重新判定本身兜得住」——重新判定只能识别**已经收口**的占位,识别不出**仍在合法运行**的占位,后者正处于要被删除的那个状态。第二条错得更本质:它给了自己和读者一道并不存在的第二防线。
- 前置阶段此前完全无界:直传 `postEditorDirectUploadFile` 是裸 `fetch`、没有 signal`saveEditorProjectLayout``requestJson` 没传 `timeoutMs`(同文件其余接口都写了),而 `composeAbortSignal` 在缺失时不设任何默认值。两者各自还有重试(上传最多 3 次尝试、布局保存最多 4 次)。
- 决策一(归属登记):`activeInlineGenerationDialogIdsRef` 记录本会话仍在执行的占位 id,创建后**紧挨着**注册(中间不能有 await,否则留出「已存在但未登记」的窗口),`finally` 释放;到期清理跳过其中的 id。到期清理本来就只该针对别人留下的孤儿。
- 决策二(整段预算而非逐请求超时):给「占位创建 → POST 发出」整段 40 秒预算。逐个请求加超时的最坏总时长会因重试累加到远超 180 秒窗口,窗口的前提仍不成立;整段封顶后客户端最坏 40 + 120 = 160 秒,落在窗口内并留 20 秒余量。超时抛裸 `Error` 而非 `ApiClientError`,归入未知结果走对账——上传可能已完成、素材可能已落库,正是对账要处理的情形。
- 决策三:`saveEditorProjectLayout``timeoutMs: 60_000`。这是独立缺陷,与本条无关也该修——它被 `flushProjectPersistence` 同步等待在提交路径上,挂住会连带把占位拖过窗口。
- 「窗口计时起点应改为 POST 发出时刻」未采纳:归属登记之后窗口不再需要覆盖本会话,只用于跨标签页;而对孤儿占位只有创建时刻这一个可用时间戳,改起点无从实现。整段预算已经让窗口的前提重新成立。
- 验证:新增两条用例——本会话持有期间超窗不清理、释放归属后同一超窗占位立即清理。去掉归属过滤后两条同时变红。`vitest src/components/image-editor` 923 通过 / 74 文件,typecheck、eslint、check:encoding 通过。
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`