Fix/let bgm and sfx async #437
Reference in New Issue
Block a user
Delete Branch "fix/let-bgm-and-sfx-async"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
bgm sfx错误地走了同步生成的路径, 在这里改为任务列表轮询
1 音频提交上下文是单槽,重叠提交会互相覆盖(commit f2a3ba7ac)
apps/ai-game-creator-shell/src/view/project-development/index.tsx(原文指 7863-7868;改后声明 2093、写入 7871、收口读取 7969、切项目清空 8261)
submitResourceCanvasGeneration时,把「这次提交属于哪张占位」写进resourceGenerationAudioSubmissionRef——一个useRef<{taskId, draftId, kind, dispatchedImmediately} | null>单槽;收口handleResourceAssetGenerationSettlement只在audioSubmission?.taskId === settlement.taskId时才走「后端从未受理 → 把面板连原草稿、原请求身份带回来」。
排在本地队列里;第二张提交会把单槽覆盖成它自己。第一条随后「后端从未受理」(
record === null)收口时,槽里已经指向第二条 → 匹配不上 → 丢掉「自动重开面板连草稿」,只剩通用失败路径(占位标 failed + 提示条)。不会卡死,但这条恢复 UX 没了。
Map<string, {...}>(键 = taskId,值里不再重复存 taskId);写入set(task.taskId, …)、收口
get(settlement.taskId)+delete、切项目clear()。同时给宿主测试加了holdFirstAssetGenerationStart开关(把第一次派发挂在「受理」之前),新增用例「两条音频提交重叠:第一条未被受理时,它的面板仍会被带回来」;该用例在旧实现下确实失败
(面板不回来),在新实现下通过。
2 音频账本
kind判定用_ =>兜底,会把未知类型静默记成音效(commit fca111239)apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rsbegin_local_project_audio_generation_task(原文指 425-428)。match request.edit_kind { BackgroundMusic => …, _ => SoundEffect }。prepare_local_project_audio_generation已经把edit_kind白名单在
SoundEffect/BackgroundMusic两个成员上,非音频 kind 在那里就被拒。但兜底臂留着的风险是真实的:枚举新增音频变体、或上游白名单被放宽时,新变体会被静默记成音效
(账本
kind是侧栏条目身份),没有任何信号。other => Err("音频生成不支持该素材类型:{other:?}")(
LocalProjectResourceEditKind已 deriveDebug,不需要新增Display)。原文件的{other:?}建议可直接采用;这条改动不影响任何可达路径的行为。3 面板
draftReleasedRef先置位、onSubmit无返回值,被同步拒绝时草稿会丢(留给你)apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasGenerationPanelView.tsxsubmit()(原文指 242-253)。submit()先draftReleasedRef.current = true(卸载时不再把草稿交回宿主),再
onSubmit({...})(宿主侧是submitResourceCanvasGeneration,返回void),最后无条件onClose()。宿主那三个「同步拒绝但不抛错」的分支(!queue || !draftId、占位已不在画布、占位已在跑且 operationId 不同)只
setResourceWorkbenchNotice后return——注意草稿写进resourceGenerationDraftRef的语句在这些return之后。→ 用户刚敲的提示词真的丢了(原文这句准确)。
①
!queue不会命中——队列实例在组件 render 体内创建(index.tsx8113 起,if (resourceAssetGenerationQueueRef.current === null) {...}),用户能点的时候它一定不是null;②!draftId不会命中——音频浮层每次都传resourceGenerationDraft.draftId;③
input.kind === 'video'不会命中——音频入口的kinds只放行音效 / 背景音乐。真正还剩的是 ④「占位在浮层画出来到点击之间消失」与 ⑤「占位已被另一次 operation 提交」。
所以今天它更像「防线写漏了一格」而不是会被普遍踩到的线上 bug;但只要前提被放宽(例如让浮层
挂在已提交的占位上、或把队列改成懒创建),草稿就会静默丢。
ResourceCanvasAssetGenerationPanelView.tsx)是怎么做的:结构完全一样——面板
submit()里draftReleasedRef.current = true→onSubmit({...})(宿主submitResourceAssetGeneration同样返回void)→ 无条件onClose();草稿也只在宿主受理路径里落
resourceAssetGenerationDraftRef。所以它有同一个洞,但暴露面更小:· 宿主侧只有
!queue一条 early return(index.tsx8166-8170),而这条如上所述不可达;· 它把「不能提交」的判据大多放在面板内部(空提示词 / 空素材名 / 参考失效 / 提示词超限 /
改了原请求输入),这些分支在
draftReleasedRef.current = true之前就return,面板不关、草稿不丢;
· 图片类的「未受理重开」用的是
resourceAssetGenerationPanelSubmissionRef里冻结的整份草稿(含比例 / 尺寸 / 参考)重开,比音频「草稿槽 + 请求身份槽」两处拼装更自洽。
另外图片类每次提交都新铸
taskId(crypto.randomUUID()),没有音频这条「同占位必须复用operation id」的判据——所以音频多出来的 ⑤ 是这次音频后台化新加的防线,不是图片类漏抄。
结论:真要修第 3 条,别只修音频面板——两个面板是同一份契约(都是
onSubmit: (input) => void面板 + 两个宿主调用点 + 各自的用例一起改。
onSubmit改成能回报受理结果:onSubmit: (input) => boolean | void;const accepted = onSubmit({...}); if (accepted === false) { return; },只有受理成功才
draftReleasedRef.current = true+onClose()(保留=== false而不是真值判断:现有用例里传的是
async () => undefined这类返回 Promise 的桩,非false一律按受理算);submitResourceCanvasGeneration三个 early return 改成return false、成功路径return true。影响面:
ResourceCanvasGenerationPanelView的 props 契约 + 宿主调用点 + 现有「点提交即同步关闭」的用例断言(
resourceCanvasGenerationEntry.test.tsx等)。不做这层的替代方案(在宿主里「重开面板」)不稳定:
submit()里onClose()跟在onSubmit()之后,同一事件里的
setResourceGenerationDraft(...)会被随后的关闭覆盖掉。4 音频分支先落账本、后登记 live,留下「排队但还不 live」的竞态窗口(commit 7b1bc1d3e)
apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rsstart_local_project_asset_generation音频分支(原文指 531-543),以及同一函数的图片类分支。begin_local_project_*(写排队记录、释放账本锁)→ 再live_task_ids().insert(task_id)→ 再
spawn后台任务。list_local_project_asset_generations的判据是「非终态 + 不在 live 集合 = 上次运行的残留」,会调用
repair_interrupted_tasks把它收口成failed并写回账本。窗口期里并发的那次
list就会命中。判准确的后果:派发后的任务虽然会把status改回running,但那次收口留下的
finishedAtMillis/error不会被清掉,而侧栏对task.error是无条件渲染的→ 会出现「一条正在跑的任务带着中断失败原因」的假象(任务真正结束时会清掉 error,所以不会
永久错乱,但显示是错的)。
register_live_task_id(task_id) -> bool:只有本轮真的插入了新 id 才有资格回滚——否则同 id 那个正在跑的任务的 live 登记会被误删,
list随后又把它谎报成上次运行的中断残留。音频与图片两条分支都改(同一条命令、同一条顺序约束),并新增用例
a_failed_ledger_write_only_takes_back_the_live_registration_it_inserted覆盖该回滚判据。补充:同一份文档注释本来就写着「本进程派发的任务在
start里先登记 live 再落账本」,这次是把实现修成与注释一致。
5
source_media_type: Some("audio/mpeg")是死参数(commit ae0ab0ddf)apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rsrun_local_project_audio_generation_at(原文指 5470-5473)。derive_local_project_resource_at传generation_mode: Create+source_media_type: Some("audio/mpeg")。Create模式在resolve_resource_edit_source里提前返回,源快照的media_type由
infer_resource_edit_source_media_type(edit_kind, source_path)推(音频恒为audio/mpeg),从头到尾不读
input.source_media_type。所以这个值对生成、对账本记录(账本存的是快照的media_type)、对恢复通道(resume_*从账本回填 + 兜底推断)都没有作用,纯误导。source_media_type: None,并在函数文档里写明「Create 模式不读
input.source_media_type」,避免下一个人又被它骗回去加一个值。验证:
cargo test resource_edit -- --test-threads=160 项全过(含所有音频 / 派生用例)。文档同步(commit 98e42e6da):
docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md与
docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md第 7.9 节第 6 条原先都写着音频通道「传
sourceMediaType: 'audio/mpeg'」,改成「源快照媒体类型由editKind推为audio/mpeg(Create 模式不读该入参)」,与代码一致。
// TODO「提交被同步拒绝时要不要关面板 / 要不要交回草稿」is a question
- `run_local_project_audio_generation_at` 提交 `derive_local_project_resource_at` 时把 `source_media_type` 从 `Some("audio/mpeg")` 改成 `None`(`generation_mode: Create` 下源快照的 媒体类型由 `edit_kind` 推出,这条通道根本不读该入参,留着会让读者以为它有作用) - 该函数文档补一句说明「Create 模式不读 `input.source_media_type`」,避免后续又被加回去