读边界只放宽对象字段的 null,Task 判据改用生成模型
两处评审都成立,且都源自同一个毛病:修复时选错了作用范围/抽象层级。 一、数组元素的 null 不该跟着放宽 上一版把「无差别删 null」整体改成「只删本次抹掉的内联媒体」,连数组元素一起放宽了。 布局数组是唯一没有元素级 shape 校验的写入口(legacy layout_storage_version=0 是新画布的 默认状态,写侧 serialize_editor_layers 只挡内联媒体、不校验元素形状),读回的 null 元素会让 前端 canvasLayoutItemId / isCanvasSettingsLayoutItem 抛 TypeError,整次项目套用被 Promise catch 吞成静默失败;generationInputs 的 fields / references 里混进 null 元素也会让整份元数据 被 isRecord 判非法丢弃——与本来要修的 SFX 症状同类。现在数组元素照旧过滤 null,放宽严格 限定在对象字段(requestedDurationSeconds 这种有意义的空值)。 二、Task 判据从资源分类换成生成模型 assetKind 是 assetKindOverride ?? resourceAssetKind,用户改一次「素材标签」就翻转;换成 resolveLayerResourceAssetKind 仍不够:legacy 布局快照没有 assetKindOverride 键时,旧的 assetKind 会被水合成 override 而 resourceAssetKind 落成 null,资源行查不到时同样是 null—— 恰恰是元数据缺失最常伴随的形态。判据要回答的是「这个 taskId 由谁生成」,所以落在 model 上: 资源行权威字段,registered 图层的 serializeLayer 不回写,改标签不影响,后端 canonicalize_editor_sound_effect_model 只接受 eleven_text_to_sound_v2 一个取值。 - 新增后端回归:布局数组与 fields 数组里的 null 元素被过滤,对象字段的 null 原样保留 - 新增前端回归:资源类型不可解析(resourceAssetKind 为 null)的音效层仍显示完整 taskId - 既有的「改标签」两向回归保持不变,在新判据下同样成立 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -380,9 +380,10 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(dialog).queryByText('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the task id display tied to the resource kind across asset tag overrides', () => {
|
||||
// 中文注释:素材标签只改 assetKindOverride,资源原始类型才是展示口径的依据。背景音乐被
|
||||
// 改标成「音效」不该换用完整 id,音效被改标成「背景音乐」更不能退回截断假值。
|
||||
it('keeps the task id display tied to the generating model across asset tag overrides', () => {
|
||||
// 中文注释:展示口径由「谁生成了这个 taskId」决定,也就是 model。素材标签只改
|
||||
// assetKindOverride,不该影响它:背景音乐被改标成「音效」不能换用完整 id,音效被改标成
|
||||
// 「背景音乐」更不能退回截断假值。
|
||||
const { unmount } = render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
@@ -439,6 +440,37 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(soundEffectDialog).queryByText('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the full sound effect task id when the resource kind is unresolvable', () => {
|
||||
// 中文注释:legacy 布局快照没有 assetKindOverride 键,水合时旧的 assetKind 会被当成
|
||||
// override;资源行又缺 assetKind(或整个 resource 查不到)时 resourceAssetKind 是 null。
|
||||
// 这正是元数据缺失最常伴随的形态,判据不能依赖资源分类,否则又退回截断假值。
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
title: '金币音效',
|
||||
src: '/generated-character-drafts/editor-audios/coin.mp3',
|
||||
mediaType: 'audio',
|
||||
resourceAssetKind: null,
|
||||
assetKindOverride: 'sound-effect',
|
||||
assetKind: 'sound-effect',
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
model: 'eleven_text_to_sound_v2',
|
||||
taskId: 'task-sfx-v2-1234-abcd',
|
||||
generationInputs: {
|
||||
fields: [{ title: '用户描述', value: '金币落地' }],
|
||||
references: [],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '音频信息' });
|
||||
expect(within(dialog).getByText('task-sfx-v2-1234-abcd')).toBeTruthy();
|
||||
expect(within(dialog).queryByText('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not render a dialog when no layer is selected', () => {
|
||||
render(<ImageCanvasMetadataModalView layer={null} onClose={vi.fn()} />);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EDITOR_SOUND_EFFECT_MODEL } from '../../../packages/shared/src/contracts/editorAudio';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import { resolveLayerResourceAssetKind } from './ImageCanvasEditorModel';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { formatTaskIdForDisplay } from './ImageCanvasExportModel';
|
||||
import {
|
||||
@@ -107,12 +107,17 @@ export function ImageCanvasMetadataModalView({
|
||||
<dt>Task</dt>
|
||||
<dd>
|
||||
{/* 中文注释:音效的 taskId 是 operation id 形态,`formatTaskIdForDisplay` 的
|
||||
「取最后一段数字」会把它截成 `0` 这类误导值。判据必须落在资源原始类型上:
|
||||
`layer.assetKind` 是 `assetKindOverride ?? resourceAssetKind`,用户在画布上
|
||||
改一次「素材标签」就能让音效层显示成截断假值、让背景音乐层改用完整 id。
|
||||
soundEffect 那一支只是补充——它和 action 一样长在 generationInputs 上,元数据
|
||||
整块缺失时会一并消失,而那正是这里要兜住的场景。 */}
|
||||
{resolveLayerResourceAssetKind(layer) === 'sound-effect' ||
|
||||
「取最后一段数字」会把它截成 `0`、`1234` 这类误导值。判据要回答的是「这个
|
||||
taskId 由谁生成」,而不是「这个图层被归成哪一类」,所以落在 model 上:它是
|
||||
资源行权威字段(registered 图层的 serializeLayer 不回写它),改「素材标签」
|
||||
不影响它,水合时 resolveHydratedLayerModel 也优先取资源行的值;后端
|
||||
canonicalize_editor_sound_effect_model 只接受 eleven_text_to_sound_v2 一个取值。
|
||||
用 assetKind / resourceAssetKind 判都踩过坑:前者是 assetKindOverride ??
|
||||
resourceAssetKind,用户改一次标签就翻转;后者在 legacy 布局(快照没有
|
||||
assetKindOverride 键,旧 assetKind 被当成 override)和资源行查不到时都是 null。
|
||||
soundEffect 那一支只是补充——它和生成 action 一样长在 generationInputs 上,
|
||||
元数据整块缺失时会一并消失,而那正是这里要兜住的场景。 */}
|
||||
{layer.model === EDITOR_SOUND_EFFECT_MODEL ||
|
||||
layer.generationInputs?.soundEffect?.schemaVersion === 2
|
||||
? layer.taskId || '-'
|
||||
: formatTaskIdForDisplay(layer.taskId)}
|
||||
|
||||
Reference in New Issue
Block a user