收紧画布生成输入V2参数恢复

新增按action统一解码生成输入参数
将失效与不兼容参数回落到当前默认值
恢复和再次提交共用规范V2结果并展示告警
记录原地收紧决策与Review处理结果
补充历史模型和非法参数回归测试
This commit is contained in:
2026-08-04 16:24:31 +08:00
parent a9b462d43f
commit 96b06cc39b
8 changed files with 667 additions and 21 deletions
@@ -29,7 +29,9 @@ import {
calculateEditorUiDesignPrice,
calculateEditorVideoPrice,
canOpenRedrawPanel,
CANVAS_GENERATION_PARAMETER_FALLBACK_WARNING,
CHARACTER_ANIMATION_MODEL,
decodeCanvasGenerationInputs,
DEFAULT_IMAGE_MODEL,
DEFAULT_SOUND_EFFECT_MODEL,
DEFAULT_VIDEO_MODEL,
@@ -543,6 +545,195 @@ describe('ImageCanvasGenerationModel', () => {
});
});
it('keeps valid V2 recipes and supported image aliases canonical', () => {
const valid = decodeCanvasGenerationInputs(
buildImageGenerationInputs('森林城堡', [], {
model: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '16:9',
imageSize: '2K',
}),
);
expect(valid).toMatchObject({ ok: true, warnings: [] });
const alias = decodeCanvasGenerationInputs({
version: 2,
action: 'image.generate',
fields: [
{ id: 'prompt', title: '生成提示词', value: '像素村庄' },
{ id: 'model', title: '模型', value: 'nanobanana2' },
{ id: 'aspectRatio', title: '比例', value: '4:3' },
{ id: 'imageSize', title: '尺寸', value: '0.5K' },
],
references: [],
});
expect(alias).toMatchObject({ ok: true, warnings: [] });
expect(
alias.ok
? alias.inputs.fields.find((field) => field.id === 'model')?.value
: null,
).toBe(IMAGE_MODEL_NANOBANANA2);
});
it('falls invalid image and workflow parameters back within V2', () => {
const image = decodeCanvasGenerationInputs({
version: 2,
action: 'image.generate',
fields: [
{ id: 'prompt', title: '生成提示词', value: '森林城堡' },
{ id: 'model', title: '模型', value: 'removed-image-model' },
{ id: 'style', title: '风格', value: 'oil' },
{ id: 'aspectRatio', title: '比例', value: '7:5' },
{ id: 'imageSize', title: '尺寸', value: '8K' },
],
references: [],
});
expect(image.ok).toBe(true);
if (!image.ok) {
throw new Error('图片 V2 解码失败');
}
expect(
Object.fromEntries(
image.inputs.fields.map((field) => [field.id, field.value]),
),
).toMatchObject({
model: DEFAULT_IMAGE_MODEL,
style: 'none',
aspectRatio: '1:1',
imageSize: '1K',
});
expect(image.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({ fieldIds: ['model'] }),
expect.objectContaining({ fieldIds: ['style'] }),
expect.objectContaining({ fieldIds: ['aspectRatio'] }),
expect.objectContaining({ fieldIds: ['imageSize'] }),
]),
);
expect(image.warnings[0]?.message).toBe(
CANVAS_GENERATION_PARAMETER_FALLBACK_WARNING,
);
const workflow = decodeCanvasGenerationInputs({
version: 2,
action: 'publication.generate',
fields: [
{ id: 'workflowId', title: '宣发类型', value: 'retired-workflow' },
],
references: [],
});
expect(workflow).toMatchObject({
ok: true,
inputs: {
version: 2,
action: 'publication.generate',
fields: [
{
id: 'workflowId',
title: '宣发类型',
value: 'publication-cover-image',
},
],
},
warnings: [expect.objectContaining({ fieldIds: ['workflowId'] })],
});
});
it('falls retired and incompatible video parameters back to current defaults', () => {
const decoded = decodeCanvasGenerationInputs({
version: 2,
action: 'video.generate',
fields: [
{ id: 'prompt', title: '视频描述', value: '追逐镜头' },
{ id: 'model', title: '模型', value: 'veo3.1' },
{ id: 'aspectRatio', title: '比例', value: '7:5' },
{ id: 'resolution', title: '清晰度', value: '1080p' },
{ id: 'durationSeconds', title: '时长', value: 99 },
{ id: 'sound', title: '声音', value: 'surround' },
{ id: 'webSearchEnabled', title: '联网搜索', value: 'yes' },
],
references: [],
});
expect(decoded.ok).toBe(true);
if (!decoded.ok) {
throw new Error('视频 V2 解码失败');
}
expect(
Object.fromEntries(
decoded.inputs.fields.map((field) => [field.id, field.value]),
),
).toMatchObject({
model: DEFAULT_VIDEO_MODEL,
aspectRatio: '16:9',
resolution: '480p',
durationSeconds: 4,
sound: 'on',
webSearchEnabled: true,
});
expect(decoded.warnings.map((warning) => warning.fieldIds)).toEqual(
expect.arrayContaining([
['model'],
['aspectRatio'],
['resolution'],
['durationSeconds'],
['sound'],
['webSearchEnabled'],
]),
);
});
it('falls invalid sound and character animation parameters back to complete defaults', () => {
const sound = decodeCanvasGenerationInputs({
version: 2,
action: 'audio.sound-effect.generate',
fields: [
{ id: 'prompt', title: 'prompt', value: '点击音' },
{ id: 'model', title: 'model', value: 'retired-audio-model' },
{ id: 'durationSeconds', title: 'duration', value: 11 },
],
references: [],
});
expect(sound).toMatchObject({
ok: true,
inputs: {
fields: expect.arrayContaining([
expect.objectContaining({ id: 'model', value: 'audio1.0' }),
expect.objectContaining({ id: 'durationSeconds', value: 5 }),
]),
},
});
const animation = decodeCanvasGenerationInputs({
version: 2,
action: 'character-animation.generate',
fields: [
{ id: 'prompt', title: '动作描述', value: '挥手' },
{ id: 'resolution', title: '清晰度', value: '1080p' },
{ id: 'ratio', title: '比例', value: '7:5' },
{ id: 'frameCount', title: '帧数', value: 40 },
{ id: 'durationSeconds', title: '时长', value: 6 },
],
references: [],
});
expect(animation).toMatchObject({
ok: true,
inputs: {
fields: expect.arrayContaining([
expect.objectContaining({ id: 'resolution', value: '480p' }),
expect.objectContaining({ id: 'ratio', value: 'same' }),
expect.objectContaining({ id: 'frameCount', value: 32 }),
expect.objectContaining({ id: 'durationSeconds', value: 4 }),
]),
},
warnings: expect.arrayContaining([
expect.objectContaining({ fieldIds: ['resolution'] }),
expect.objectContaining({ fieldIds: ['ratio'] }),
expect.objectContaining({
fieldIds: ['frameCount', 'durationSeconds'],
}),
]),
});
});
it('keeps generated prompts and quick edit options stable', () => {
const prompt = buildSpecPrompt('ui', {
playSetting: '消除玩法',