文档:恢复音频生成共享分流并停止跟踪本地方案
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 2m48s
Project CI / Native shell tests (pull_request) Successful in 14m43s

新增共享音频 Composer 重构方案并收口权威设计
更新实施记录、决策日志、踩坑记录与文档索引
停止跟踪 local-docs 中的 T5 实施方案并保留本地文件
This commit is contained in:
2026-08-06 03:03:26 +00:00
parent 17bf54f211
commit 3a363c98ff
7 changed files with 197 additions and 261 deletions
+1
View File
@@ -20,6 +20,7 @@
- [图片画布编辑器 MVP 接入方案](./technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md)
- [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md)
- [画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md)
- [音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)
- [BGM 提示词优化 T6 测试与发布门禁](./【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md)
- [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md)
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
@@ -0,0 +1,171 @@
# 音频生成 Composer 恢复共享分流方案
日期:`2026-08-06`
状态:`已规划,尚未实施`
## 一、目标
图片画布的音效与背景音乐恢复使用同一个音频 composer。`ImageCanvasGenerationComposerView.tsx` 内只保留一个 `ImageCanvasAudioGenerationComposerView`,并在组件内定义:
```ts
const isSoundEffect = dialog.mode === 'audio-sound-effect';
```
`isSoundEffect === true` 渲染现有 SFX 分支,`isSoundEffect === false` 渲染现有 BGM V1 分支。删除完整的独立 BGM composer 文件,但保留确有独立职责的 BGM 纯模型、助手 controller 和预设跑马灯组件。
这次只调整前端视图组织方式,不改变任何生成契约、业务规则、请求时序、计费或持久化语义。
## 二、范围与非目标
### 2.1 必须保留的 BGM 能力
- `gpt_description_prompt` 可见原文、Unicode `White_Space` canonicalization 和 200 字生成限制。
- 30 个预设、字符计数、AI 补全、一键简化和单层交换式撤销。
- AI 处理锁、同步正式提交锁、`generating` 锁和迟到响应隔离。
- 稳定 dialog ID、账号 / 项目 scope 校验和按 dialog ID 写回。
- 固定 `Suno`、当前动态泥点价格和隐藏 `make_instrumental` 的现状。
### 2.2 必须保持不变的现有 SFX 能力
- 固定 Vidu `audio1.0`Prompt 同值映射到现有 `prompt + sound` 请求字段。
- `210` 秒、步长 `1` 秒、默认 `5` 秒。
- 当前 Prompt 规范化、全空白时回退“游戏音效”、1500 字限制、价格和失败态。
- 现有提交 callback、生成占位和完成链路。
- SFX 中不出现 Suno、BGM 预设、AI 补全、一键简化、BGM 字符计数或撤销。
### 2.3 本次明确不做
- 不实现 SFX V2 独有的一键优化、自动中译英、ElevenLabs、自动时长、30 秒、Loop 或 SFX 预设。
- 不修改 BGM 或 SFX 需求原文。
- 不修改 BFF、队列、`platform-audio`、External v1、OpenAPI、SpacetimeDB schema、计费或重试。
- 不把 BGM Prompt controller 泛化为音频通用 controller。
- 不新建配置驱动的 composer 框架、第二套音频组件或其它顺带重构。
## 三、当前问题
当前 `ImageCanvasGenerationComposerView.tsx` 分别渲染 SFX 内部组件和 `ImageCanvasBackgroundMusicGenerationComposerView.tsx`。这层完整视图拆分让同一个音频入口形成两套 composer 边界,而 SFX V2 需求已经表明预设、AI 写回、撤销和锁定等交互并非 BGM 永久独占,继续用“BGM 专属交互较多”作为完整组件分叉理由不再成立。
同时,最近一次合并把共享架构中的 `isSoundEffect` 条件表达式带回了当前 SFX-only 组件,却没有带回变量定义,形成 `isSoundEffect is not defined`。该问题不是增加一个局部常量后就可以收口的长期架构问题;本次重构应恢复单一音频组件,使变量与它控制的两个分支重新处于同一组件边界。
## 四、目标结构
```text
ImageCanvasEditorView
└─ useImageCanvasGenerationSurface
└─ ImageCanvasGenerationComposerView
└─ ImageCanvasAudioGenerationComposerView
├─ isSoundEffect === true → 现有 SFX UI 与行为
└─ isSoundEffect === false → 现有 BGM V1 UI 与行为
└─ ImageCanvasBackgroundMusicPresetMarquee
```
继续保留的 BGM 专项模块:
- `ImageCanvasBackgroundMusicPromptModel.ts`canonicalization、计数、动作资格和 dialog 类型收窄。
- `useImageCanvasBackgroundMusicPromptAssist.ts`:按 dialog ID 隔离的异步助手状态与提交锁。
- `ImageCanvasBackgroundMusicPresetModel.ts`30 个预设与追加规则。
- `ImageCanvasBackgroundMusicPresetMarquee.tsx`:展开、滚动、hover、触摸和 reduced-motion。
删除的完整视图模块:
- `ImageCanvasBackgroundMusicGenerationComposerView.tsx`
- `ImageCanvasBackgroundMusicGenerationComposerView.test.tsx`
删除测试文件不等于删除覆盖;其中全部用例必须迁入总 composer 测试。
## 五、实现边界
### 5.1 单一渲染入口
`ImageCanvasGenerationComposerView``audio-sound-effect``audio-background-music` 只保留一个渲染条件和一个 `ImageCanvasAudioGenerationComposerView` 调用。组件内部以 `isSoundEffect` 选择两棵现有表单子树,不为本次重构新造配置层。
共享组件使用基于 dialog ID 与 mode 的稳定 `key`。连续切换 BGM dialog 时,预设展开、滚动、hover 和触摸状态不得继承;从 SFX 切到 BGM 时,音效时长菜单状态也不得泄漏。
### 5.2 Hook 与类型安全
- React Hook 不得放进 `isSoundEffect` 条件分支。`useState``useRef``useId``useImageCanvasFloatingOptionDismiss` 保持固定调用顺序。
- `GenerateDialogState` 不是可判别联合。BGM 分支继续通过 `toBackgroundMusicGenerationDialog` 取得带稳定 ID 的 `BackgroundMusicGenerationDialogState`
- BGM 缺少稳定 ID 或 `backgroundMusicPromptAssist` 时失败关闭,不渲染不完整的 BGM 表单;SFX 不依赖这两个条件。
### 5.3 两套状态写回不得合并
- SFX 继续走现有 `setGenerateDialog` 路径,保持按 mode 更新、失败态复位和时长写回语义。
- BGM 继续走 `updateCanvasGenerationDialogById(dialog.id, updater)`,不能降级成只比较 mode。助手响应、预设、撤销和提交锁仍按稳定 dialog ID 隔离。
- `backgroundMusicPromptAssist` 可以继续由 surface 传给共享 composer,但只允许 BGM 分支读取或调用。
### 5.4 锁定与提交资格不得串用
|分支|锁定条件|生成资格|
|---|---|---|
|SFX|沿用现有 `dialog.status === 'generating'`|沿用现有 SFX 提交入口和默认 Prompt 规则|
|BGM|AI processing、`submitting``generating` 任一成立|沿用 canonical Prompt 的有效字符与 `1200` code point 规则|
BGM 使用 `PlatformTextField + readOnly + aria-invalid`SFX 继续使用 `AutoGrowTextArea + disabled`。本次不统一这两个输入控件,也不改错误、可访问名称或按钮文案。
## 六、预计代码改动
|文件|最小改动|
|---|---|
|`ImageCanvasGenerationComposerView.tsx`|恢复共享 `ImageCanvasAudioGenerationComposerView``isSoundEffect`;移入现有 BGM JSX、常量和依赖;删除独立 BGM import 与双渲染入口|
|`ImageCanvasGenerationComposerView.test.tsx`|迁入独立 BGM composer 的全部测试,并增加双 mode 隔离与切换回归|
|`ImageCanvasBackgroundMusicGenerationComposerView.tsx`|删除|
|`ImageCanvasBackgroundMusicGenerationComposerView.test.tsx`|覆盖迁完后删除|
|`useImageCanvasBackgroundMusicPromptAssist.ts`|只修正指向独立 composer 的历史注释;不改 controller 行为|
以下生产文件预计不改:`useImageCanvasGenerationSurface.tsx` 的现有 props 接线、BGM Prompt / 预设模型、预设跑马灯、submission workflow、submission model、dialog model、`src/index.css` 的现有 BGM 样式,以及全部后端代码。
如实施时发现必须超出该清单才能保持现有行为,应先停下并重新确认边界,不能借本次组件归并顺带重构。
## 七、测试迁移与回归矩阵
### 7.1 BGM 原覆盖完整迁移
独立组件测试中的下列覆盖必须逐项迁入 `ImageCanvasGenerationComposerView.test.tsx`
- canonical preview 计数、超限展示和不改写输入框。
- 0 / 1 / 2 个有效字符及 200 / 201 / 2000 / 2001 边界。
- 补全、简化、撤销和预设按正确 dialog ID 路由。
- `preparePreset` 拒绝时不写回,预设成功时沿用追加和清快照语义。
- 助手错误与生成错误的展示顺序。
- Suno、泥点价格、生成允许 / 拒绝。
- `completing``simplifying``submitting``generating` 锁定。
- 完整单层撤销按钮矩阵和现有可访问属性。
### 7.2 mode 隔离与切换
- SFX 渲染时不存在 BGM 控件,也不存在本次明确排除的 SFX V2 控件。
- BGM 渲染时不存在 Vidu 和音效时长控件。
- SFX 仍保持 `audio1.0`、2–10 秒、默认 5 秒、当前价格和既有提交参数。
- BGM dialog A 展开预设后切到 dialog B,局部展开与滚动状态不继承。
- SFX 与 BGM 相互切换时,菜单、锁和 Prompt 助手状态不跨 mode 泄漏。
- 缺少稳定 ID 或 controller 的 BGM 失败关闭;同样条件不影响 SFX 正常渲染。
### 7.3 最小验证命令
```powershell
npm run test -- src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasBackgroundMusicPromptModel.test.ts src/components/image-editor/ImageCanvasBackgroundMusicPresetModel.test.ts src/components/image-editor/ImageCanvasBackgroundMusicPresetMarquee.test.tsx src/components/image-editor/useImageCanvasBackgroundMusicPromptAssist.test.tsx
npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx
npm run typecheck
npm run check:encoding
git diff --check
```
删除旧测试文件后,还要确认测试收集清单中不再引用它。若本次改动触发其它现有图片画布测试失败,只修复由共享 composer 归并直接造成的回归,不扩大到无关模块。
## 八、实施顺序
1. 在总 composer 内恢复共享音频组件、`isSoundEffect` 和单一音频渲染入口。
2. 原样迁入 BGM 分支,保留按 ID 写回、锁定、错误顺序、Suno 与预设行为。
3. 迁移独立 BGM 组件测试,并补齐 SFX/BGM 隔离与切换用例。
4. 删除独立 BGM composer 及其测试文件,修正相关注释与文档引用。
5. 运行定向测试、typecheck、编码检查和差异检查;实现完成后再把实际结果补入 T6 后续记录。
## 九、完成定义
- 两个音频 mode 均从同一个 `ImageCanvasAudioGenerationComposerView` 渲染,且组件内存在唯一的 `isSoundEffect` 分流。
- 独立完整 BGM composer 文件和独立测试文件已删除,原测试覆盖无遗漏地迁入总 composer。
- BGM V1 所有已交付能力与正式提交语义不变。
- 现有 SFX UI、Prompt、Vidu、时长、价格、校验和提交链路无回归。
- 没有实现任何 SFX V2 独有功能,没有修改需求原文或后端契约。
- 规定的定向测试、typecheck、编码检查和 `git diff --check` 全部通过。
@@ -6614,3 +6614,10 @@
- 遗留(建议单开,不在本次范围):`loadProjectCoverImage` 里无 timeout / 无 AbortSignal 的 `new Image()` 本身仍是隐患,自动保存路径一样会踩。本次只是把它移出生成链的关键路径,没有消除它。
- 影响范围:`useImageCanvasProjectPersistence.ts``flushProjectPersistence`。不改服务端、不改契约。
- 验证方式:既有用例「flush 等待封面缓存」翻转为「flush 不等封面、但封面链照常跑完并完成上传与资源登记」;新增「封面永不 settle 时 flush 仍返回」——用永不 resolve 的 blob 模拟 `new Image()` 不 settle,并断言 `createProjectCoverSnapshotBlob` 确实被调用过以防用例空过。已实证:回退修复后新用例报 `expected 'false' to be 'true'`。运行 `npx vitest run src/components/image-editor src/components/platform-entry src/services`101 文件 / 1241 项)、`npm run typecheck``npm run lint:eslint``npm run check:encoding`
## 2026-08-06 音效与背景音乐恢复共享音频 Composer
- 决策:图片画布的 `audio-sound-effect``audio-background-music` 只保留一个 `ImageCanvasAudioGenerationComposerView`,组件内以 `isSoundEffect = dialog.mode === 'audio-sound-effect'` 分流。撤销的是完整 `ImageCanvasBackgroundMusicGenerationComposerView` 这一层视图拆分,不撤销 BGM Prompt 纯模型、助手 controller、预设模型或预设跑马灯的独立职责。
- 业务隔离:共享组件不等于共享规则。SFX 继续使用 Vidu `audio1.0`、2–10 秒、默认 5 秒、现有 Prompt 回退、1500 字限制、价格和提交链路;BGM 继续使用 canonical Prompt、200 字生成限制、30 个预设、AI 补全 / 简化、单层撤销、提交锁和 Suno。BGM 按 dialog ID 写回,SFX 继续走现有 `setGenerateDialog`,两条路径不得互换。
- 非目标:本次只规划视图归并,不实现 SFX V2 的 ElevenLabs、中译英、自动时长、30 秒、Loop、一键优化或预设,不修改任何后端、External v1、Schema、计费或需求原文,也不新建配置驱动的 composer 框架。
- 实施状态:当前代码仍保留独立 BGM composer,待按 `docs/project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md` 落地。完成前不得把本决策写成已通过的实现结论;完成时必须迁移全部独立组件测试并同时证明 SFX 与 BGM 两个分支无状态和控件泄漏。
@@ -4201,3 +4201,10 @@
- 原因:两个跨模块测试读写同一进程全局状态,却没有共用隔离边界;只给 accept 后取得的 stream 设置 read timeout 无法约束 accept 本身,payload 读取也缺少总 deadline。
- 处理:全部全局 sink 测试共用一把 test-only 串行锁,并由 RAII guard 在 `Drop` 中无条件清空;测试统一使用 `manifest_invalidation_sink_isolation_` 前缀。relay fixture 对 accept 和 payload 分别使用非阻塞轮询与总 deadline,不使用固定 sleep;生产 loopback、token、连接 / 写入超时和 payload 大小校验保持不变。
- 验证:用 `--test-threads=2` 重复运行统一 filter,覆盖正常 relay、无事件 accept 超时、不完整 payload 超时、panic 展开清理,以及 GUI owner attach 配置与 guard 清理。
## 共享音频 Composer 架构冲突不能按单行选边(2026-08-06)
- 现象:master 的音频 composer 同时承载 SFX 与 BGM,并在组件内定义 `isSoundEffect`;功能分支把 BGM 拆成独立组件后,原组件变成 SFX-only。合并时只把 master 的条件占位表达式带回 SFX-only 组件,没有带回变量定义,最终在测试渲染阶段报 `isSoundEffect is not defined`
- 原因:冲突两侧代表不同组件架构,逐行保留看似有用的 JSX 会把一个架构中的局部条件拼进另一个架构。import 排序、格式检查和只覆盖单一 mode 的测试都不能证明这种组合成立。
- 处理:先确定权威组件边界,再按完整调用链解决冲突。图片画布音频入口当前决策是恢复一个共享 `ImageCanvasAudioGenerationComposerView`,由组件内 `isSoundEffect` 分流;BGM/SFX 的 validator、写回、锁和提交契约仍分别保持。不要只补一个常量后继续维持已经废弃的双 composer 边界。
- 验证:同时渲染 `audio-sound-effect``audio-background-music`,覆盖两个 mode 的正向控件和互斥负向断言、dialog / mode 切换、BGM 稳定 ID 与 controller 缺失的失败关闭,并运行 `ImageCanvasGenerationComposerView.test.tsx` 与 typecheck。
@@ -144,3 +144,9 @@ API 安全响应摘要:
- 本地 API mock 链路 `27/27` 通过,并确认请求 body 的 `model``gpt-5.6-luna`
- 真实 VectorEngine smoke 已尝试:`GET /v1/models``POST /v1/chat/completions` 均在建立 HTTP 连接前以 `fetch failed` 结束,没有返回状态码或模型响应;该结果只能说明当前环境网络不可达,不能判定 `gpt-5.6-luna` 被上游拒绝或支持。
- 网络恢复后重试成功:补全和简化各发送一条最小 Chat Completions 请求,均返回 HTTP 200、`model=gpt-5.6-luna``finish_reason=stop`,并解析出完整四字段 JSON object;补全候选 `126` code points,简化候选 `79` code points。由此确认当前模型和两条助手请求形态可以跑通。
## 2026-08-06 组件架构后续修订
本文前述 T6 数字与文件范围是当时实施完成后的历史记录,不因后续组件归并而改写。新的权威方向是让 SFX 与 BGM 恢复使用 `ImageCanvasGenerationComposerView.tsx` 内同一个音频 composer,并由 `isSoundEffect` 分流;详见[音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)。
该修订目前只完成规划,尚未实施。落地时必须把 `ImageCanvasBackgroundMusicGenerationComposerView.test.tsx` 的全部覆盖迁入 `ImageCanvasGenerationComposerView.test.tsx` 后再删除旧测试文件,并同时重跑 BGM 完整动作矩阵、SFX 现有 Vidu / 时长 / Prompt 回退回归、surface 接线、submission workflow、typecheck、编码检查与差异检查。不得把原 T6 的历史通过数字直接当作共享 composer 已通过的证据。
@@ -2,13 +2,15 @@
日期:`2026-06-18`
更新时间:`2026-08-05`
更新时间:`2026-08-06`
## 范围
本次只在 `/editor/canvas` 图片画布编辑器内新增底部 `生成音乐` 入口,用于生成完整游戏音效或游戏背景音乐。该入口属于画板生成类工具,不新增平台玩法入口、不进入作品发布链路,也不修改现有视觉小说音频生成开关。
2026-08-04 起,本文增加 BGM Prompt 优化 V1.0 口径。该切片只修改 `audio-background-music``audio-sound-effect` 的 UI、Prompt 回退、Vidu `audio1.0` 请求、`2-10` 秒时长和 1500 字限制全部保持不变。共用组件若需要调整,必须按生成器模式隔离行为不得把 BGM 规则扩散到 SFX。
2026-08-04 起,本文增加 BGM Prompt 优化 V1.0 口径。该切片只修改 `audio-background-music``audio-sound-effect` 的 UI、Prompt 回退、Vidu `audio1.0` 请求、`2-10` 秒时长和 1500 字限制全部保持不变。音效与背景音乐使用同一个音频 composer,并由组件内的 `isSoundEffect = dialog.mode === 'audio-sound-effect'` 隔离行为不得把 BGM 规则扩散到 SFX。
2026-08-06 的组件架构修订只撤销完整 BGM composer 的独立视图边界。共享音频 composer 的 SFX 分支保留当前 Vidu、时长、默认 Prompt、1500 字和价格行为,BGM 分支保留本文规定的预设、计数、AI 补全 / 简化、撤销、锁定、canonical Prompt 和 Suno 行为。BGM 纯状态模型、助手 controller 与预设跑马灯仍可保持独立职责;本轮不实现 SFX V2 的 ElevenLabs、中译英、自动时长、30 秒、Loop、一键优化或预设等独有能力。具体实施边界见[音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)。
## 入口与交互
@@ -371,4 +373,5 @@ POST /api/editor/audios/background-music/prompts/simplifications
- BGM 助手入口测试覆盖 canonical 2000 字允许简化、2001 字返回 `400` 且不调用 LLM;两个助手路由 body 超过 `32 KiB` 时返回 `413`;连续合法请求不因本功能新增限流器返回 `429`
- 两个助手成功路由分别产生 `editor_background_music_prompt_completion` / `editor_background_music_prompt_simplification` tracking event,均为 `module_key = editor`、User scope;失败响应沿用普通 route tracking 只记录成功的现状。
- BGM Suno body 仍只包含 `mv``gpt_description_prompt``make_instrumental`,固定 `Suno` 胶囊和动态泥点价格不变;SFX 的 Vidu body、默认 Prompt、时长与 1500 字限制无回归。
- `audio-sound-effect``audio-background-music` 必须由同一个音频 composer 渲染,并在组件内通过 `isSoundEffect` 分支。SFX 不得渲染 BGM 控件或尚未实施的 SFX V2 控件,BGM 不得渲染 Vidu 与音效时长控件;两个 mode 相互切换时,菜单、预设滚动、锁和助手状态不得跨分支泄漏。
- 本切片的定向前端、shared-contracts、`api-server``platform-audio` 和端到端 Prompt 等值测试通过,并执行 `npm run typecheck`、对应 Rust 定向测试、`npm run check:encoding``git diff --check`
@@ -1,259 +0,0 @@
# BGM 生成提示词优化 T5 正式提交与方案 A 锁实施方案
日期:`2026-08-05`
状态:实施完成,待提交
开发分支:`codex/bgm-generation-opt-v1`
## 一、文档定位
本文承接 T5 的具体工程规划、现役链路调研、改动边界、测试设计和后续实施记录。
- 阶段级目标、依赖关系和总体完成定义继续保留在[总任务拆解](./【实施计划】BGM生成提示词优化V1.0任务拆解-2026-08-04.md)。
- 产品与技术口径以[画板音乐生成入口设计](../docs/【编辑器】画板音乐生成入口设计-2026-06-18.md)为权威。本次修订只补齐该既有口径在现役 queue / inline、scope 所有权和动态价格链路上的实现边界,不新增产品能力。
- T4 的组件与交互细节留在[T4 界面与预设交互实施方案](./【实施方案】BGM生成提示词优化T4界面与预设交互-2026-08-04.md);该文第十二节列出的交接风险由本文承接并给出解法。
- 本文不得反向修改需求文件,也不得用实现便利覆盖权威设计。
## 二、实施基线
已完成提交:
- T3`825b0ccf6`
- T3-R2`3fbdf210b`
- T0 补差:`0dc1eed38`
- T4`d825d8181`
### 2.1 现役正式提交链路调研结论
以下为实施前的实测事实,不是推测:
1. `submitImageGeneration``useImageCanvasGenerationSubmissionWorkflow.ts`)服务 `edit` / `image` / `character` / `video` / `audio-sound-effect` / `audio-background-music` 六种 mode,入口没有任何同步锁。现有防重依赖“重渲染后生成按钮 `disabled`、composer 卸载”,属于渲染后生效的状态防护,不满足方案 A 要求的同步互斥。
2. 入口第一行是 `dialog.prompt.trim() || 默认回退`。它同时踩三个问题:JavaScript `String.trim()` 会额外删除首尾 `U+FEFF`,与 Rust 侧不一致;空值回退为“游戏背景音乐”形成用户不可见 Prompt;紧接着**同步**把 dialog 置为 `status:'generating'``composerOpen:false`
3. 第 3 点会直接打死提交锁:Prompt 助手 hook 的 dialog 清理 effect 看到 `composerOpen === false``clearActiveOperation(abort:true)`,把刚 claim 的 `submitting` operation reject 掉;随后 `finishSubmission` 找不到 active operation 返回 `false`,锁形同虚设,dialog 重新打开后还能再次提交。
4. `buildImageGenerationSubmissionPlan` 内部还会独立调用一次 `getDialogDefaultPrompt``ImageCanvasGenerationSubmissionModel.ts`)。只删除入口那一处回退不够,默认文案会从这里回到请求体。
5. 后端生成记录 `prompt` / `actual_prompt`、canvas layer 和内部完成响应已经取自 `normalized.gpt_description_prompt`;但 queue 入口虽然先计算了 `normalized`,实际传给 `enqueue_editor_generation_job_for_caller` 的仍是原始 `payload`。因此“只替换 normalize 即可让队列等值”的旧判断错误,站内 BGM handler 必须在入队前把 canonical Prompt 和固定 `make_instrumental:true` 写回本次入队 payload。
6. Rust 的 `str::trim()` 语义与本需求 canonicalizer 一致(都按 `char::is_whitespace` 删除两端)。真正的跨语言不一致只存在于 TypeScript 侧;BFF 和 `platform-audio` 仍需防御性调用已有 BGM 专用 validator,以拒绝空值和超限值。
7. BGM composer 的提交锁完全来自 `assistState.status === 'submitting'`,与 `dialog.status` 无关。因此 T5 不需要新增 dialog 状态:提交期间 `dialog.status` 保持 `idle`,面板由助手状态锁定。
8. 状态模型对 `submitting``rejectOperation` 会保留 `undoPromptSnapshot`。“后端拒绝时保留提交前撤销快照”已经具备,T5 不需要修改纯状态模型。
9. `generateEditorBackgroundMusic` 当前给正式 POST 配置了 `retryUnsafeMethods:true`、最多两次 transport retry,而站内 queue 每次请求都会生成新的 job ID。UI 同步锁无法阻止 client 内部重发;T5 必须仅对 BGM 正式 POST关闭这项既有自动重试,不能为此新增幂等协议或改动其它生成 mode。
10. `finishSubmission` 返回 `false` 既可能表示 dialog 被删除,也可能表示账号 / 项目 scope 已变化。正式任务不能因此被取消,但本地 dialog、Prompt、composer 和结果层也不能继续只按 `generation-dialog-N` 写回,因为该 ID 不是跨项目全局唯一。
11. 生产和默认配置走 queue:首次成功响应只携带 `queueState`,此时即可判定站内任务已创建。兼容 inline 模式不会暴露中间接受响应,HTTP Promise 要到生成完成后才返回,不能把两种模式写成同一个可观察时序。
12. `vector_engine_audio_generation/tests.rs` 当前没有被父模块挂载,不能作为 T5 验收依据。新增 Rust 用例必须放进实际编译的模块内测试或真正挂载的精简测试模块。
## 三、既有口径下的实现收口
### 3.1 提交锁与 composer 生命周期
T4 交接要求“后端接受后才隐藏 composer”,同时要求普通 `composerOpen=false` 不得提前 reject `submitting`。两者分别处理正常接受时序和用户主动归档 / 切面板,不是新增交互:
- 当前 BGM 面板保持打开时,提交期间继续可见并全锁;queue 模式收到任务已创建的成功响应后才结束 `submitting` 并切入现有占位。
- 用户在等待期间归档或切走面板时,只把原 dialog 变为 inactive / closed,不取消已经发出的正式请求,也不主动重新激活该 dialog。
- dialog 被删除或账号 / 项目 scope 改变时,可以移除临时助手状态;这不代表正式请求已取消。
### 3.2 正式任务处理与本地 UI 写回必须分开
点击时冻结本次 `{账号、项目、dialogId、operation、canonical Prompt、submission plan}`。后续异步阶段遵循两条独立规则:
1. 已发出的正式请求和后端已经创建的任务继续按现役链路处理,不能因 `finishSubmission` 返回 `false` 被误判为取消。
2. `status``composerOpen`、Prompt、错误信息、本地 `applyProjectSnapshot` / asset upsert 和结果层都属于 UI 写回;每次跨 `await` 后写入前都必须重新确认仍是原账号、原项目和原 dialog。scope 已变化或 dialog 已删除时跳过本地 UI 写回,不能按同名 ID 修改新 scope。
普通 `composerOpen=false` 且 scope / dialog 仍匹配时,`finishSubmission` 仍可结算。若随后失败:
- 原 dialog 仍是当前打开面板:恢复 `failed + composerOpen:true`
- 原 dialog 已被用户归档或切走:写入失败状态和 canonical Prompt,但保留关闭 / inactive,不抢回焦点。
- 原 scope 已失效或 dialog 已删除:不再写本地 UI。
因此,`finishSubmission` 的返回值不能取消正式任务,但也不能被解释成“所有本地结果都可无条件写回”。
### 3.3 可观察提交时点与 transport 边界
```text
点击生成
→ 同步 beginSubmissioncanonicalize、写回、资格校验、冻结 scope 与请求值
→ 只发送一次 BGM 正式 POST,不执行 unsafe 自动重试
├─ queue 接受 → finishSubmission({ operation, accepted: true }) → 原 UI 目标仍有效时切 generating / 隐藏 composer → 现有轮询
├─ 首次 POST 非成功或抛错 → finishSubmission({ operation, accepted: false }) → 原 UI 目标仍有效时记录失败
└─ inline 兼容模式 → 无中间接受响应,保持 submitting 到现有终态响应,再结算并处理结果
```
- T5 不新增 Idempotency-Key、持久提交账本、服务端 dedupe 或新的响应阶段;“一次点击一次请求”通过同步 UI 锁和关闭 BGM 正式 POST 的既有 unsafe retry 实现。
- 请求返回 transport 错误时,客户端不能证明服务端一定未接受;T5 不自动重发。用户之后再次明确点击属于新的提交,不在本切片引入跨页面、跨 scope 的 durable exactly-once 设计。
- 保留现有 BGM 正式请求超时;不新增重试、超时或限流。
## 四、改动边界
- 只改 `audio-background-music``edit` / `image` / `character` / `video` / `audio-sound-effect` 的提交代码路径、文案、默认 Prompt 和错误展示一字不动。
- 实现方式为在 `submitImageGeneration` 内增加 BGM 前置分支,分支结束后复用现有 audio 结果处理(队列、占位、素材库、项目刷新、`addAudioResultLayer`),不复制这段逻辑,也不新建平行提交函数。
- 不新增 dialog 状态、不修改纯状态模型、不新增持久提交锁或全局画布锁。
- 只移除 BGM 正式 POST 的 `EDITOR_REQUEST_RETRY_OPTIONS`;不修改共享 retry 配置或其它请求。
- 不新增 Idempotency-Key、dedupe、请求账本、响应字段或 queue 状态。
- 站内 BGM handler 的 queue payload 执行 canonical 写回;不改变 External v1 的请求、幂等键或 payload 等值语义。
- 不修改 Suno 三字段结构、固定模型、现役动态价格配置和计费包装;客户端不提交价格,服务端继续在入队时冻结价格。
- “正式请求不含助手隐藏元数据”只约束本功能前端构造的 submission payload 和固定三字段 Suno body;不在 T5 新增通用 `generationInputs` allowlist 或恶意客户端清洗器。
- 不修改 SpacetimeDB schema、External v1、External OpenAPI 或 LLM 日志策略。
- 不增加新的重试、超时或限流。
## 五、目标文件结构
|文件|T5 职责|
|---|---|
|`useImageCanvasBackgroundMusicPromptAssist.ts`|清理 effect 豁免 `submitting``finishSubmission` 不再要求 composer 打开|
|`useImageCanvasGenerationWorkflow.ts`|把现有完整 Prompt 助手 controller 和当前 scope 接入 submission workflow|
|`useImageCanvasGenerationSubmissionWorkflow.ts`|BGM 前置分支:同步取锁、冻结 scope / 请求值;分离正式任务处理与 scope-safe UI 写回|
|`ImageCanvasGenerationSubmissionModel.ts`|BGM 分支使用冻结的 canonical Prompt,不再经过默认回退|
|`editorProjectClient.ts`|仅移除 BGM 正式 POST 的 unsafe 自动重试,保留现有超时|
|`server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs`|BGM 入站改用 canonical 校验;站内 handler 用 canonical payload 进入 queue|
|`server-rs/crates/platform-audio/src/request.rs`|BGM Suno body builder 防御性 canonicalization|
|对应现役测试文件|覆盖 client 单次发送、scope 所有权、queue payload、body 与持久化等值;不依赖未挂载的 `vector_engine_audio_generation/tests.rs`|
## 六、T5A:助手生命周期豁免
### 6.1 修改
- 清理 effect 的第二个循环:active operation 为 `submitting` 时跳过,不 abort、不 reject、不移除记录。
- `finishSubmission`:移除“composer 仍打开”的前置条件;operation、scope 和 BGM dialog 仍匹配即可结算。
- 返回值只表示这次助手 operation 是否由原 UI 目标成功结算。正式请求已经发出后,该值不负责取消任务;submission workflow 仍需在后续每个 UI 写回点重新检查冻结 scope。
- 其余 6 处 `composerOpen` 判断保持不变。AI 操作入口在 `submitting` 期间已由 `isSubmissionLocked` 挡住,迟到响应、预设、撤销和 `beginSubmission` 的语义不受影响。
### 6.2 验收
- 提交期间把 `composerOpen``false`(选中其它图层、归档、切面板),`submitting` 不被 reject`finishSubmission` 仍能结算。
- 提交期间删除 dialog:UI 状态移除,`finishSubmission` 返回 `false`,不抛错。
- 提交期间切换账号或项目:助手状态被 reset,`finishSubmission` 返回 `false`;正式任务继续,但旧回调不能写入新 scope 的同 ID dialog。
- T3 / T3-R2 既有的 operation ID、跨 dialog、迟到响应、超时和 scope 用例继续通过。
## 七、T5B:前端 BGM 提交分支
### 7.1 同步阶段(必须早于任何 `await`)
1. 用类型收窄取得带稳定 ID 的 BGM dialog;不是 BGM 则走现有通用路径。
2. `beginSubmission(dialog.id)` 在同一个同步边界内完成 canonicalize、输入框写回和正式生成资格校验;状态模型继续是唯一资格判定源,不在 workflow 再写第二套 0 / 200 校验。
3. 返回 `null` 表示重复点击、scope / dialog 不合格或 Prompt 无正式生成资格,直接结束;canonical 写回按现有 controller 结果保留,不发送请求。
4. claim 成功后冻结 `{账号、项目、dialogId、operation、canonical Prompt}`,并以该 Prompt 构造 submission planBGM 分支不得再经过原生 `trim``getDialogDefaultPrompt`
5. 不在同步阶段切换 `status` / `composerOpen`
### 7.2 异步阶段
- 调用 `generateEditorBackgroundMusic` 一次;该 client 不再配置 unsafe retry。
- queue 首次成功响应代表任务已创建:先调用 `finishSubmission({ operation, accepted: true })`;原 UI 目标仍有效时再设 `status:'generating'``composerOpen:false`,随后复用现有 queue 轮询、项目刷新、素材库和结果处理。
- 首次 POST 非成功或抛错:先调用 `finishSubmission({ operation, accepted: false })`。原 dialog 仍是当前面板时恢复 `failed + composerOpen:true`;已归档 / 切走时保留关闭状态并记录失败;scope 已变化或 dialog 已删除时不写本地 UI。
- queue 已接受后的终态成功 / 失败继续走现有正式链路;每次应用 project snapshot、upsert asset、写 dialog 或本地结果层前重新检查冻结 scope 和 dialog。scope 已失效时跳过这些前端后续写回,后端已经接受的任务继续处理;T5 不新增跨 scope 的恢复或刷新机制。
- inline 兼容模式没有 `queueState`:面板保持 `submitting` 到现有 HTTP 终态响应;成功后结算并直接处理现有结果,失败按上一条恢复,不新增中间响应或占位协议。
### 7.3 验收
- 同一 dialog 快速重复点击只产生一次正式 POST;queue 模式只创建一个 queue job 并沿用该 job 的现有计费链路,inline 模式沿用现有终态处理;retryable HTTP 状态或 transport error 也不会由 client 自动重发。
- 空 Prompt、全 Unicode 空白 Prompt 不产生正式请求。
- 1 个有效字符可以提交;201 个 code point 不能提交。
- 提交期间输入框、预设、展开收起、箭头、AI 补全、一键简化、撤销和生成全部锁定,面板保持可见。
- 后端拒绝后 canonical Prompt 和撤销快照保留;当前面板恢复,用户已经归档 / 切走的面板不被自动激活。
- 后端接受后,原 UI 目标仍有效时才进入占位;画布其它功能全程可用。
- 账号 / 项目 A 的迟到成功或失败不能修改账号 / 项目 B 的同 ID dialogqueue 已接受后再切 scope 也遵守该规则。
- inline 模式在终态响应前保持 `submitting`,不为兼容模式新增协议。
- 其余 5 种 mode 的提交行为无差异。
## 八、T5C:后端 canonicalization
### 8.1 修改
- `normalize_editor_background_music_request_with_pricing` 改用 BGM canonical 校验函数:canonicalize、至少 1 个有效字符、不超过 200 个 code point,返回 canonical Prompt。
- 登录态站内 `generate_editor_background_music` handler 在 queue / inline 分流前,用上述结果构造本次 canonical payload:覆盖 `gpt_description_prompt`,并固定 `make_instrumental:true`。queue 分支必须把这份 payload 交给现有 enqueue helper,不能继续序列化原始请求。
- 不在共享 External v1 调用边界新增 payload 改写;External v1 路由、Idempotency-Key、OpenAPI 和现有 payload 等值判断保持不变。
- `build_editor_background_music_task_body` 执行同一套幂等 canonicalization 后再构造 Suno body。
- BGM 请求不接收客户端价格。服务端继续按现役动态定价解析,并把本次价格冻结到 queue job;后续计费 target、资产成本和内部完成响应继续复用该冻结价格,不硬编码 `5`
- SFX 继续使用现有 `normalize_limited_text`Vidu body、默认 Prompt、时长与 1500 字限制不变。
### 8.2 验收
- 空 Prompt、全空白 Prompt 返回 `400`,不创建任务、不扣费。
- 201 个 code point 拒绝;200 个通过。
- 站内 queue 的实际 `request_payload_json.gptDescriptionPrompt` 与 canonical Prompt 逐 code point 等值,`makeInstrumental``true`;不能只断言 normalize 返回值。
- 首尾 `U+0085` 等 Unicode `White_Space` 被删除;`U+200B``U+FEFF`、组合字符和 ZWJ emoji 不被误删;内部空格与 LF / CRLF 原样保留。
- 连续两次 canonicalization 结果一致。
- Suno body 仍只含 `mv``gpt_description_prompt``make_instrumental`
- queue job 的 `price_mud_points`、计费 target 和内部完成响应使用同一次服务端冻结价格;前端请求中没有 `priceMudPoints`
- External v1 契约与幂等回归无差异。
- SFX 相关 Rust 测试无回归。
## 九、T5D:端到端等值
- 逐层断言写回后的输入框、前端 BFF 请求、站内 generation queue payload、Suno body、`editor_project_resource``editor_asset`、canvas layer 以及内部 `EditorAudioGenerateResponse` 的 Prompt 字段逐 code point 完全一致。
- queue 首次响应仍只断言现有 `queueState`;普通站内 job 的 `result_payload_json` 继续使用现有 metadata-only 形态,不为 T5 增加 `prompt` / `actualPrompt` 字段。`prompt` / `actualPrompt` 的完成响应断言落在 inline 或 worker 内部生成响应边界。
- 断言本功能前端构造的正式 submission payload 不含预设 ID、分组、颜色、助手系统模板、内部引导、AI 快照或格式 / 完整性 / 残句判断字段;Suno body 的 key 集合精确等于三个固定字段。
- 不把上条扩展成 BFF 对任意 `generationInputs` 的通用 allowlist / sanitizer,本切片只保证本功能客户端不发送这些隐藏元数据。
- 用例至少覆盖:含首尾边界空白、内部换行和零宽字符的 Prompt;仅 1 个有效字符的 Prompt;恰好 200 个 code point 的 Prompt。
## 十、测试计划
|层级|场景|期望|
|---|---|---|
|助手 hook|提交期间 `composerOpen=false`|`submitting` 不被 reject`finishSubmission` 可结算|
|助手 hook|提交期间删除 dialog|移除 UI 状态,`finishSubmission` 返回 `false`,不抛错|
|助手 hook|提交期间切换账号 / 项目|状态 reset;正式任务继续,旧 UI 不写入新 scope|
|BGM client|retryable HTTP 状态 / transport error|一次调用只发送一次 POST,不执行 unsafe retry|
|前端提交|双击生成|一次 client 调用、一次正式请求|
|前端提交|空 / 全空白 Prompt|不发请求,保留 canonical 文本|
|前端提交|1 个有效字符 / 200 / 201|前两者可提交,201 拒绝|
|前端提交|当前面板后端拒绝|面板恢复,canonical Prompt 与撤销快照保留|
|前端提交|归档 / 切面板后拒绝|记录失败但保持关闭,不抢焦点|
|前端提交|queue 接受|先结算再切占位,画布其它功能可用|
|前端提交|queue 接受后切 scope,再返回终态|后端已创建任务不被视为取消;新 scope 同 ID dialog 与本地画布不被旧回调修改|
|前端提交|inline 成功 / 失败|终态响应前保持 `submitting`,随后走现有结果 / 失败链路|
|后端|空、全空白、201|`400`,不创建任务|
|后端|`U+200B` / `U+FEFF` / 组合字符 / ZWJ emoji|不被误删|
|后端|幂等性|两次 canonicalization 结果一致|
|后端 queue|站内边界空白 Prompt|实际 `request_payload_json` 使用 canonical Prompt,固定 instrumental,冻结动态价格|
|后端 queue|一次站内正式 POST|创建一个 queue job,并沿用该 job 的单次计费链路|
|平台 body|边界空白 + 内部换行 + 零宽字符|三个固定字段,Prompt 逐 code point 等值|
|端到端|边界空白 + 内部换行 + 零宽字符|前端、queue、Suno、resource、asset、canvas 与内部完成响应等值|
|回归|External v1|路由、OpenAPI、Idempotency-Key 与 payload 等值语义无差异|
|回归|SFX|Vidu body、默认 Prompt、时长与 1500 字限制不变|
|回归|图片 / 视频 / 角色 / edit|提交路径与文案无差异|
Rust 测试必须落入 `cargo test -p api-server` 实际会编译执行的位置,例如 `generation.rs` / `publish.rs` 的内联测试或新挂载的精简测试模块;不得把未挂载的 `vector_engine_audio_generation/tests.rs` 当作通过证据。T5 不借此清理整份历史测试文件,也不扩展计费系统。
## 十一、建议实施顺序
1. T5A:助手生命周期豁免(可与 T5C 并行)。
2. T5C:站内 canonical queue payload 与 `platform-audio` 防御性 canonicalization。
3. BGM client:移除正式 POST 的 unsafe retry;补 controller / scope 接线。
4. T5B:前端 BGM 提交分支和 scope-safe UI 写回(依赖 T5A)。
5. T5Dqueue / inline 分流与端到端等值测试。
6. 定向测试、类型检查、ESLint、Rust 定向测试、编码检查与差异检查。
7. 真实浏览器验证:以默认 queue 模式走通“提交成功进占位”“当前面板失败恢复”“归档后失败不抢焦点”三条路径;inline 兼容语义由定向测试覆盖,不新增联调协议。
建议一个提交:`前后端:收紧BGM生成提交链路`
## 十二、风险与交接
- 本切片是发布切点。T4 与 T5 之间不可发布,因为 T4 面板已按 canonical 口径展示,而正式提交仍走旧 `trim` 与默认回退。
- 空 Prompt 收紧后,历史上依赖默认回退成功的空提交会被前端拦截并被后端拒绝。这是需求明确要求的行为变化。
- 提交期间面板保持可见是既定口径,但与当前“点击立即切占位”的手感不同,需要在浏览器验证中确认无违和。
- 正式请求没有可取消的 `AbortController`。请求发出后即使 dialog 被删除或 scope 切换,也可能已经被后端接受并扣费;实现不得把这类情况当作“已取消”。
- BGM 正式 POST 关闭 unsafe retry 后,transport 结果未知时不会自动重发;T5 不引入 durable 幂等或对账系统。用户随后再次明确点击是新提交,这是本切片保留的既有边界。
- scope 切换会清理临时助手锁。正式任务继续由后端处理,但旧异步回调必须停止本地 UI 写回;T5 不新增跨页面持久锁。
- inline 兼容模式无法观察“上游任务已创建但尚未完成”的中间时刻,因此锁会保持到现有 HTTP 终态响应;生产和默认 queue 行为不受该兼容边界影响。
- 现役 BGM 价格由服务端配置解析并在入队时冻结;T5 不修改价格、计费规则或钱包账本。
- T6 需要汇总本切片测试,并复核完成定义中与提交链路相关的条目。
## 十三、实施记录
|日期|阶段|状态|记录|
|---|---|---|---|
|2026-08-05|T5 初始规划|已被本轮修订取代|完成正式提交链路初查;后续评审确认“queue 已使用 normalized”“权威设计无需修改”两项判断不成立。|
|2026-08-05|T5 规划修订|完成|收口 unsafe POST retry、scope-safe UI 写回、站内 canonical queue payload、queue / inline 可观察时序、动态价格和有效测试落点;明确不新增幂等协议、持久锁、响应字段、通用 sanitizer 或计费设计。|
|2026-08-05|T5A 助手生命周期|完成|修改 `useImageCanvasBackgroundMusicPromptAssist.ts` 及定向测试:`submitting` 不再因普通 composer 关闭被清理,dialog 删除和 scope 切换仍清理,`finishSubmission` 继续校验 operation、scope 与 BGM dialog。|
|2026-08-05|T5B / T5D 正式提交与等值链路|完成|修改 BGM submission model、submission workflow、workflow 接线、BGM `generationInputs` helper、正式 client 及定向测试:同步 claim 早于首个 `await`canonical Prompt 是唯一请求值,关闭 unsafe POST retryqueue / inline 按既定时序结算;冻结 scope 版本和 UI 所有权,覆盖首次响应及 queue 终态前的 `A → B → A` 往返、当前面板失败恢复和归档后不抢焦点;其它生成 mode 保持原路径。|
|2026-08-05|T5C 后端 canonicalization|完成|修改站内 BGM handler、queue 准备边界、`platform-audio` Suno body builder 及现役测试:登录态 queue / inline 使用 canonical Prompt 并固定 `makeInstrumental=true`queue 准备边界绑定 canonical payload 与动态冻结价格,实际 queue serializer 对该 payload 序列化;External v1 继续保留原始 payload 和既有幂等语义,SFX 未改。|
|2026-08-05|T5 修复前验证|完成|前端 5 个定向测试文件 `193/193` 通过,`npm run typecheck` 与变更文件 ESLint 通过;`platform-audio` `31/31`、窄口径 `api-server` BGM 定向 `28/28`、登录态 queue 准备 `1/1`、External v1 `7/7` 通过。该记录保留当时命令口径,后续验收以本表最新记录为准。|
|2026-08-05|T5 评审修复|完成|拆分钱包账号所有权、任务列表账号内项目所有权(同时校验 `currentUserId + projectId`)与 dialog/canvas 完整所有权;真实 transport 层覆盖 BGM 正式 POST 遇到 `429``503``TypeError` 均只发送一次;补齐 External v1 BGM 原始 payload 保留、同原值幂等重放及 canonical 等价值冲突回归。未修改 SFX、其它生成 mode、通用 retry、计费、Schema、External 路由 / DTO / OpenAPI 或 worker。|
|2026-08-05|T5 修复后验证|完成|前端 6 个定向测试文件 `199/199` 通过,其中真实 transport `3/3``npm run typecheck` 与变更文件 ESLint 通过;`platform-audio` `31/31`、广义 `cargo test -p api-server background_music` `35/35`、External v1 既有回归 `7/7` 通过;Rust 格式、编码与 `git diff --check` 通过。真实 queue 浏览器 smoke 会创建正式任务并可能扣费,本切片不执行。|
后续每次实施只在本节追加:
- 完成的切片;
- 实际修改文件;
- 验证命令与结果;
- 尚存风险;
- 对下一阶段的交接。