From a0b139ba34c948adeda03aa6005e25de06bbe478 Mon Sep 17 00:00:00 2001 From: suzmii Date: Thu, 6 Aug 2026 14:42:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=B8=E6=88=8F=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E9=98=9F=E5=88=97=E5=9B=9E=E8=AF=BB=E6=97=B6=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重读未收口的场景权威快照 补充场景队列完成回归测试 同步场景链路与排障约定 --- docs/project-memory/shared-memory/pitfalls.md | 11 ++ ...方案】图片画布游戏场景生成链路-2026-08-04.md | 1 + ...anvasGenerationSubmissionWorkflow.test.tsx | 103 ++++++++++++++++++ ...ImageCanvasGenerationSubmissionWorkflow.ts | 1 + 4 files changed, 116 insertions(+) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 65ad961aa..7cc46f9e4 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -29,6 +29,7 @@ - 处理:把递增封进一个 guard 结构体,递减放在它的 `Drop` 实现里;递增本身用 `fetch_update` 的 CAS,不能用「先读后加」——两个线程同时读到 `max - 1` 各自加一就会越界。拿到资源后立即 `drop(guard)` 让出队列名额,不要让它跟着许可一起活到请求结束。 - 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言 `504`,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或 `--test-threads=1` 掩盖隔离问题。 - 关联:`server-rs/crates/api-server/src/editor_project.rs`(`try_enter_bounded_queue`、`EditorPixelArtSnapQueueGuard`)。 + ## Linux 生产脚本门禁不能假设本地也是 GNU userland - 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T`、`mapfile: command not found`、`/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders` 到 `/private/var/folders` 的系统别名误判为用户符号链接。 @@ -287,6 +288,14 @@ - 验证:画板生成 workflow 测试覆盖 queueState 持续 `running` 到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。 - 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`。 +## 场景队列终态不保证首次项目快照已经收口生成占位 + +- 现象:游戏场景任务已经显示完成,但画布仍保留 `generating` 占位;场景链路又禁止用本地结果补层,因此当前会话可能一直停在生成中。 +- 原因:外部生成任务终态与项目画布投影不是同一个原子观测点。队列轮询先看到 `completed` 后,紧接着的首次项目 GET 仍可能读到同一 `dialogId` 的未收口占位;若调用统一回读函数时没有传 completion dialog ID,函数无法识别该快照仍未完成,也不会执行已有的有界延迟重读。 +- 处理:游戏场景队列调用要把本次占位 `dialogId` 传给 `applyQueuedEditorGenerationProject`。首个快照中该 ID 仍为 unresolved 时,只按既有间隔补读一次项目;不追加本地图层,也不把任务终态直接等同于画布投影终态。其他生成类型若要补同类保护,必须分别复现其权威回填时序后再改,不能用本条场景结论替代验证。 +- 验证:场景 workflow 用两个连续快照复现时序:第一个保留 `scene / generating`,第二个包含场景结果并把同一占位置为 `idle`。修复前只读一次并超时,修复后依次应用两个权威快照。 +- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`、`docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md`。 + ## 图片画布历史不能回退当前权威状态或复活后端已删素材 - 现象:生成占位框移动后开始生成,撤销移动会把仍在运行的生成对象恢复成待生成状态;切换到 2K 或改变比例后撤销位置,旧占位框还可能把当前尺寸回退。上传图层落库后,普通移动撤销可能被提示“可能会使图片消失”并永久卡在栈顶;即使安全检查已放行,直接恢复旧图层快照也会丢失刚回填的资源关联。素材库后端删除关联素材后,更早的移动快照还可能把已删图层重新加入并自动保存;修改素材类型虽然界面提示撤销成功,刷新后却可能从仍指向新类型的 resource 回弹。无稳定 ID 的“修改图片”草稿也可能被 target-null 快照直接关闭。 @@ -3974,6 +3983,7 @@ - 处理:灰度页只能以 `/admin/api/feature-gates` 为数据源,固定目标列表只登记现役功能;新增或退役业务 target 只修改固定目标注册,不得让通用页面依赖业务列表接口。旧 `creation-entry:*` 目标、接口和页面保持退役。 - 验证:`adminRoutes` 必须包含 `gray-release`,admin-web TypeScript/ESLint/Vitest 不得排除灰度页;页面测试必须断言只请求 feature-gates,并继续覆盖现役固定 target、直接 Gate Key 保存与新 target 状态重置。 - 关联:`apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx`、`apps/admin-web/src/app/adminRoutes.ts`、`server-rs/crates/api-server/src/modules/admin.rs`、`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。 + ## 历史钱包消费不能从最近流水或通用订单快照推算 - 现象:后台用户详情要展示累计花费时,直接复用只返回最近 50 条的 `list_profile_wallet_ledger`,或在充值订单每行使用的通用钱包快照里扫描该用户全部流水。 @@ -4096,6 +4106,7 @@ - 处理:从当前 root source 的 seed lane 动态解析全部零依赖首波任务,只对这些 child 容忍 hydration `Pending`,后续 code prototype / preview 仍严格要求 Running/Completed。`streaming / ready` 仍要求当前 revision,`committed` 回复改为依据 finalization 的稳定身份查询,不随后续项目 revision 失效。 - 验证:覆盖 `design-director / art-director / code-director` 三个 Pending 首波 child 均可投影 Completed、`code-prototype` Pending 仍被拒绝;非流式专业 Agent 在 finalization 前无 stream,提交后形成 committed stream,再推进项目 revision 后仍可查询且正文不变。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/response_stream.rs`。 + ## 异步生成结果未知时不能换幂等键重提(2026-07-31) - 现象:生成提交发生客户端超时、连接中断或响应丢失后,调用方创建新的 `Idempotency-Key` 再提交一次;原任务其实已经入队,最终造成重复生成、重复扣费和重复画布 / 素材库写入。 diff --git a/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md b/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md index 7466ae155..5589cad78 100644 --- a/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md +++ b/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md @@ -132,6 +132,7 @@ prompt = 后端完整 Prompt - Provider 失败、执行取消和 lease 耗尽沿用现有扣退费语义。 - Provider 成功后的持久化或画布写回失败语义不在本期调整。 - inline 响应中的通用生成告警必须在队列终态处理前转发;有项目画布占位但响应缺少权威 `project` 快照时,不得追加本地图层或把占位标记为已完成。 +- 队列任务进入 `completed` 后,首次项目快照仍可能短暂保留同一 `dialogId` 的 `generating` 占位。场景提交必须把 completion 的 `dialogId` 传给统一项目回读逻辑;首个权威快照仍未收口时做一次有界延迟重读,不能把该快照当作最终状态永久套用。 ## 7. 数据落点 diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index 2d04cc4c3..99f9a174f 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -1728,6 +1728,109 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { ); }); + it('reloads the authoritative scene project when the first queued snapshot is unresolved', async () => { + vi.useFakeTimers(); + try { + const applyProjectSnapshot = vi.fn(); + const unresolvedProject = { + projectId: 'editor-project-1', + title: '场景队列项目', + viewport: { x: 0, y: 0, scale: 1 }, + layers: [ + { + layerId: 'generation-dialog-scene-queued', + resourceId: 'generation-dialog-scene-queued', + itemType: 'generation-dialog', + dialog: { + id: 'dialog-scene-queued', + mode: 'scene', + status: 'generating', + }, + }, + ], + resources: [], + updatedAt: '2026-08-06T08:00:00.000Z', + }; + const completedProject = { + ...unresolvedProject, + layers: [ + { + layerId: 'layer-scene-result', + resourceId: 'resource-scene-result', + title: '游戏场景 1', + assetKind: 'scene', + }, + { + layerId: 'generation-dialog-scene-queued', + resourceId: 'generation-dialog-scene-queued', + itemType: 'generation-dialog', + dialog: { + id: 'dialog-scene-queued', + mode: 'scene', + status: 'idle', + generatedLayerId: 'layer-scene-result', + }, + }, + ], + updatedAt: '2026-08-06T08:00:01.000Z', + }; + generateEditorSceneMock.mockResolvedValueOnce({ + ...createGenerated({ width: 1024, height: 576 }), + queueState: createQueueState(), + }); + loadEditorProjectMock + .mockResolvedValueOnce(unresolvedProject) + .mockResolvedValueOnce(completedProject); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交当前生成' })); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(loadEditorProjectMock).toHaveBeenCalledTimes(1); + await act(async () => { + await vi.advanceTimersByTimeAsync(650); + }); + expect(loadEditorProjectMock).toHaveBeenCalledTimes(2); + expect(applyProjectSnapshot).toHaveBeenNthCalledWith( + 1, + unresolvedProject, + ); + expect(applyProjectSnapshot).toHaveBeenNthCalledWith(2, completedProject); + } finally { + vi.useRealTimers(); + } + }); + it('does not add a local scene layer when the backend omits its project snapshot', async () => { generateEditorSceneMock.mockResolvedValueOnce( createGenerated({ diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts index 673fed215..d90920491 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts @@ -2091,6 +2091,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({ onQueuedGenerationTask, onWalletBalanceMayHaveChanged, onGenerationWarning, + canvasDialog?.id, ) ) { return;