3485bad2d3
- 将所有相关工作流从 "ui-spec" 迁移至 "icon-spec"。 - 优化 `playSetting` 和 `artStyle` 的输入处理逻辑。 - 更新编辑器提示词、提交模型及测试用例以适配 "icon-spec"。 - 在 UI 及后端调用中集成 `EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH` 及相关校验。 - 新增 "icon-spec" 生成与微调的专用 API 路径。 - 移除旧版 "ui-spec" 相关定义及提示词构建器。
585 lines
18 KiB
TypeScript
585 lines
18 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
||
|
||
import { ApiClientError } from '../../services/apiClient';
|
||
import type {
|
||
CanvasGenerationDialogState,
|
||
CanvasLayer,
|
||
} from './ImageCanvasEditorTypes';
|
||
import {
|
||
applyEditorGenerationPricingConfig,
|
||
BACKGROUND_MUSIC_MODEL_SUNO,
|
||
buildCharacterGenerationInputs,
|
||
buildEditGenerationInputs,
|
||
buildIconGenerationInputs,
|
||
buildImageGenerationInputs,
|
||
buildQuickEditGenerationInputs,
|
||
buildQuickEditModelOptions,
|
||
buildSoundEffectGenerationInputs,
|
||
buildSpecGenerationInputs,
|
||
buildSpecPrompt,
|
||
buildUiDesignAssetExtractionGenerationInputs,
|
||
calculateCharacterAnimationPrice,
|
||
calculateEditorBackgroundMusicPrice,
|
||
calculateEditorIconSpritesheetPrice,
|
||
calculateEditorImageGenerationPrice,
|
||
calculateEditorImageModelPrice,
|
||
calculateEditorSoundEffectPrice,
|
||
calculateEditorSpecGenerationPrice,
|
||
calculateEditorUiDesignPrice,
|
||
calculateEditorVideoPrice,
|
||
CHARACTER_ANIMATION_MODEL,
|
||
DEFAULT_IMAGE_MODEL,
|
||
DEFAULT_SOUND_EFFECT_MODEL,
|
||
DEFAULT_VIDEO_MODEL,
|
||
EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG,
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG,
|
||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||
EDITOR_MODEL_MUD_POINT_CONFIG,
|
||
EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG,
|
||
EDITOR_SOUND_EFFECT_MODEL_OPTIONS,
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG,
|
||
EDITOR_VIDEO_MODEL_OPTIONS,
|
||
getGenerationFrameAriaLabel,
|
||
getGenerationFrameLabel,
|
||
IMAGE_MODEL_GPT_IMAGE_2,
|
||
IMAGE_MODEL_NANOBANANA2,
|
||
isQuickEditUnsupportedAssetKind,
|
||
resolveCharacterAnimationSourceImageSrc,
|
||
resolveEditorImageGenerationPixelSize,
|
||
resolveImageGenerationErrorMessage,
|
||
VIDEO_MODEL_KLING_3,
|
||
VIDEO_MODEL_KLING_3_OMNI,
|
||
VIDEO_MODEL_SEEDANCE_2,
|
||
VIDEO_MODEL_VEO_3_1,
|
||
VIDEO_MODEL_VEO_3_1_FAST,
|
||
} from './ImageCanvasGenerationModel';
|
||
|
||
describe('ImageCanvasGenerationModel', () => {
|
||
afterEach(() => {
|
||
applyEditorGenerationPricingConfig({
|
||
models: EDITOR_MODEL_MUD_POINT_CONFIG,
|
||
});
|
||
});
|
||
|
||
it('为所有编辑器生成模型提供显式定价配置', () => {
|
||
expect(
|
||
EDITOR_IMAGE_MODEL_OPTIONS.map((option) => option.value).filter(
|
||
(model) => !(model in EDITOR_MODEL_MUD_POINT_CONFIG),
|
||
),
|
||
).toEqual([]);
|
||
expect(
|
||
EDITOR_VIDEO_MODEL_OPTIONS.map((option) => option.value).filter(
|
||
(model) => !(model in EDITOR_MODEL_MUD_POINT_CONFIG),
|
||
),
|
||
).toEqual([]);
|
||
expect(
|
||
EDITOR_SOUND_EFFECT_MODEL_OPTIONS.map((option) => option.value).filter(
|
||
(model) => !(model in EDITOR_MODEL_MUD_POINT_CONFIG),
|
||
),
|
||
).toEqual([]);
|
||
expect(
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2]['1K'],
|
||
).toBe(3);
|
||
expect(
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2]['2K'],
|
||
).toBe(5);
|
||
expect(
|
||
EDITOR_MODEL_MUD_POINT_CONFIG[CHARACTER_ANIMATION_MODEL],
|
||
).toBeTruthy();
|
||
expect(
|
||
EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG[DEFAULT_SOUND_EFFECT_MODEL],
|
||
).toBe(5);
|
||
expect(
|
||
EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG[
|
||
BACKGROUND_MUSIC_MODEL_SUNO
|
||
],
|
||
).toBe(12);
|
||
// 中文注释:Veo 已从前端入口移除,但客户端类型和后端兼容层仍保留,定价表也必须覆盖,避免旧布局回放或测试请求价格漂移。
|
||
expect(
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_VEO_3_1],
|
||
).toBeTruthy();
|
||
expect(
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_VEO_3_1_FAST],
|
||
).toBeTruthy();
|
||
});
|
||
|
||
it.each([
|
||
[IMAGE_MODEL_NANOBANANA2, '1K', '1:1', 1024, 1024],
|
||
[IMAGE_MODEL_NANOBANANA2, '2K', '2:3', 1365, 2048],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '1:1', 1024, 1024],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '4:3', 1024, 768],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '3:2', 1024, 683],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '2:3', 683, 1024],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '9:16', 576, 1024],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '16:9', 1024, 576],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '1:1', 2048, 2048],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '4:3', 2048, 1536],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '3:2', 2048, 1365],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '2:3', 1365, 2048],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '9:16', 1152, 2048],
|
||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '16:9', 2048, 1152],
|
||
] as const)(
|
||
'%s 的 %s %s 占位尺寸与统一业务交付尺寸一致',
|
||
(model, imageSize, aspectRatio, width, height) => {
|
||
expect(
|
||
resolveEditorImageGenerationPixelSize({
|
||
model,
|
||
aspectRatio,
|
||
imageSize,
|
||
}),
|
||
).toEqual({ width, height });
|
||
},
|
||
);
|
||
|
||
it('所有生成按钮价格都从模型定价配置推导', () => {
|
||
expect(calculateEditorImageModelPrice(DEFAULT_IMAGE_MODEL)).toBe(
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[DEFAULT_IMAGE_MODEL]['1K'],
|
||
);
|
||
expect(calculateEditorImageModelPrice(IMAGE_MODEL_GPT_IMAGE_2, '1K')).toBe(
|
||
3,
|
||
);
|
||
expect(calculateEditorImageModelPrice(IMAGE_MODEL_GPT_IMAGE_2, '2K')).toBe(
|
||
5,
|
||
);
|
||
expect(
|
||
calculateEditorImageGenerationPrice({
|
||
kind: 'character',
|
||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||
imageSize: '1K',
|
||
}),
|
||
).toBe(EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2]['1K']);
|
||
expect(calculateEditorIconSpritesheetPrice(DEFAULT_IMAGE_MODEL)).toBe(
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[DEFAULT_IMAGE_MODEL]['1K'],
|
||
);
|
||
expect(calculateEditorUiDesignPrice(IMAGE_MODEL_GPT_IMAGE_2, '2K')).toBe(
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2]['2K'],
|
||
);
|
||
expect(calculateEditorSpecGenerationPrice(IMAGE_MODEL_GPT_IMAGE_2)).toBe(
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2]['2K'],
|
||
);
|
||
expect(calculateEditorVideoPrice(DEFAULT_VIDEO_MODEL, '720p', 5)).toBe(
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[DEFAULT_VIDEO_MODEL]['720p'] * 5,
|
||
);
|
||
expect(calculateEditorVideoPrice(VIDEO_MODEL_SEEDANCE_2, '480p', 4)).toBe(
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_SEEDANCE_2]['480p'] * 4,
|
||
);
|
||
expect(calculateEditorVideoPrice(VIDEO_MODEL_KLING_3, '480p', 4)).toBe(
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_KLING_3]['480p'] * 4,
|
||
);
|
||
expect(calculateEditorVideoPrice(VIDEO_MODEL_KLING_3, '720p', 5)).toBe(150);
|
||
expect(calculateEditorVideoPrice(VIDEO_MODEL_KLING_3_OMNI, '720p', 5)).toBe(
|
||
200,
|
||
);
|
||
expect(
|
||
calculateCharacterAnimationPrice(CHARACTER_ANIMATION_MODEL, '720p', 6),
|
||
).toBe(
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[CHARACTER_ANIMATION_MODEL]['720p'] *
|
||
6,
|
||
);
|
||
expect(calculateEditorSoundEffectPrice(DEFAULT_SOUND_EFFECT_MODEL)).toBe(
|
||
EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG[DEFAULT_SOUND_EFFECT_MODEL],
|
||
);
|
||
expect(
|
||
calculateEditorBackgroundMusicPrice(BACKGROUND_MUSIC_MODEL_SUNO),
|
||
).toBe(
|
||
EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG[
|
||
BACKGROUND_MUSIC_MODEL_SUNO
|
||
],
|
||
);
|
||
});
|
||
|
||
it('支持用后端下发的模型定价覆盖按钮泥点', () => {
|
||
applyEditorGenerationPricingConfig({
|
||
models: {
|
||
...EDITOR_MODEL_MUD_POINT_CONFIG,
|
||
[IMAGE_MODEL_GPT_IMAGE_2]: {
|
||
unit: 'perGeneration',
|
||
prices: {
|
||
'1K': 29,
|
||
'2K': 58,
|
||
},
|
||
},
|
||
[VIDEO_MODEL_SEEDANCE_2]: {
|
||
unit: 'perSecond',
|
||
prices: {
|
||
...EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_SEEDANCE_2],
|
||
'720p': 26,
|
||
},
|
||
},
|
||
[DEFAULT_SOUND_EFFECT_MODEL]: {
|
||
unit: 'perGeneration',
|
||
price: 15,
|
||
},
|
||
[BACKGROUND_MUSIC_MODEL_SUNO]: {
|
||
unit: 'perGeneration',
|
||
price: 9,
|
||
},
|
||
},
|
||
});
|
||
|
||
expect(calculateEditorImageModelPrice(IMAGE_MODEL_GPT_IMAGE_2, '1K')).toBe(
|
||
29,
|
||
);
|
||
expect(calculateEditorSpecGenerationPrice(IMAGE_MODEL_GPT_IMAGE_2)).toBe(
|
||
58,
|
||
);
|
||
expect(calculateEditorVideoPrice(VIDEO_MODEL_SEEDANCE_2, '720p', 5)).toBe(
|
||
130,
|
||
);
|
||
expect(calculateEditorSoundEffectPrice(DEFAULT_SOUND_EFFECT_MODEL)).toBe(
|
||
15,
|
||
);
|
||
expect(
|
||
calculateEditorBackgroundMusicPrice(BACKGROUND_MUSIC_MODEL_SUNO),
|
||
).toBe(9);
|
||
});
|
||
|
||
it('builds user-facing generation input snapshots instead of backend prompts', () => {
|
||
expect(buildImageGenerationInputs(' 一张明亮主视觉 ')).toEqual({
|
||
fields: [{ title: '生成提示词', value: '一张明亮主视觉' }],
|
||
references: [],
|
||
});
|
||
|
||
expect(
|
||
buildSpecGenerationInputs('character', {
|
||
playSetting: '平台跳跃',
|
||
artStyle: '像素风',
|
||
bodyRatio: '3',
|
||
characterView: '右向三分之二侧身',
|
||
customPrompt: '',
|
||
}),
|
||
).toEqual({
|
||
fields: [
|
||
{ title: '玩法设定', value: '平台跳跃' },
|
||
{ title: '美术风格', value: '像素风' },
|
||
{ title: '头身比', value: '3' },
|
||
{ title: '角色视角', value: '右向三分之二侧身' },
|
||
],
|
||
references: [],
|
||
});
|
||
expect(
|
||
buildSpecGenerationInputs('icon', {
|
||
playSetting: '休闲消除',
|
||
artStyle: '清爽卡通',
|
||
bodyRatio: '3',
|
||
characterView: '',
|
||
customPrompt: '',
|
||
}),
|
||
).toEqual({
|
||
fields: [
|
||
{ title: '玩法设定', value: '休闲消除' },
|
||
{ title: '美术风格', value: '清爽卡通' },
|
||
],
|
||
references: [],
|
||
});
|
||
});
|
||
|
||
it('builds character, icon and edit reference snapshots', () => {
|
||
const sourceLayer = buildSourceLayer();
|
||
|
||
expect(
|
||
buildCharacterGenerationInputs(
|
||
'主角骑士',
|
||
{
|
||
id: 'spec',
|
||
label: '角色规范',
|
||
src: '/spec.png',
|
||
resourceId: 'resource-spec',
|
||
},
|
||
[
|
||
{
|
||
id: 'ref-1',
|
||
label: '盔甲参考',
|
||
src: '/armor.png',
|
||
resourceId: 'resource-armor',
|
||
},
|
||
],
|
||
),
|
||
).toEqual({
|
||
fields: [{ title: '角色设定', value: '主角骑士' }],
|
||
references: [
|
||
{
|
||
title: '角色规范',
|
||
label: '角色规范',
|
||
refType: 'project-resource',
|
||
refId: 'resource-spec',
|
||
},
|
||
{
|
||
title: '常规参考图 1',
|
||
label: '盔甲参考',
|
||
refType: 'project-resource',
|
||
refId: 'resource-armor',
|
||
},
|
||
],
|
||
});
|
||
|
||
expect(
|
||
buildIconGenerationInputs(['返回按钮', '设置按钮'], {
|
||
id: 'icon-spec',
|
||
label: '图标规范',
|
||
src: '/icon-spec.png',
|
||
resourceId: 'resource-icon-spec',
|
||
}),
|
||
).toEqual({
|
||
fields: [{ title: '素材描述', value: '返回按钮\n设置按钮' }],
|
||
references: [
|
||
{
|
||
title: '图标规范',
|
||
label: '图标规范',
|
||
refType: 'project-resource',
|
||
refId: 'resource-icon-spec',
|
||
},
|
||
],
|
||
});
|
||
|
||
expect(
|
||
buildEditGenerationInputs('修改要求', '换成夜晚', sourceLayer),
|
||
).toEqual({
|
||
fields: [{ title: '修改要求', value: '换成夜晚' }],
|
||
references: [
|
||
{
|
||
title: '参考图',
|
||
label: '原图',
|
||
refType: 'project-resource',
|
||
refId: 'resource-source',
|
||
},
|
||
],
|
||
});
|
||
expect(
|
||
buildUiDesignAssetExtractionGenerationInputs(sourceLayer, [
|
||
{
|
||
id: 'ref-ui',
|
||
label: 'UI风格参考',
|
||
src: '/ui-style.png',
|
||
resourceId: 'resource-ui-style',
|
||
},
|
||
]),
|
||
).toEqual({
|
||
fields: [
|
||
{
|
||
title: '提取提示词',
|
||
value:
|
||
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
|
||
},
|
||
],
|
||
references: [
|
||
{
|
||
title: 'UI设计图',
|
||
label: '原图',
|
||
refType: 'project-resource',
|
||
refId: 'resource-source',
|
||
},
|
||
{
|
||
title: '参考图 1',
|
||
label: 'UI风格参考',
|
||
refType: 'project-resource',
|
||
refId: 'resource-ui-style',
|
||
},
|
||
],
|
||
});
|
||
expect(
|
||
buildQuickEditGenerationInputs(
|
||
'快速编辑提示词',
|
||
'让图2下雨',
|
||
sourceLayer,
|
||
[
|
||
{
|
||
id: 'ref-1',
|
||
label: '天空',
|
||
src: '/sky.png',
|
||
resourceId: 'resource-sky',
|
||
},
|
||
],
|
||
),
|
||
).toEqual({
|
||
fields: [{ title: '快速编辑提示词', value: '让图2下雨' }],
|
||
references: [
|
||
{
|
||
title: '原图',
|
||
label: '原图',
|
||
refType: 'project-resource',
|
||
refId: 'resource-source',
|
||
},
|
||
{
|
||
title: '参考图 1',
|
||
label: '天空',
|
||
refType: 'project-resource',
|
||
refId: 'resource-sky',
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
it('keeps custom prompts and quick edit options stable', () => {
|
||
expect(
|
||
buildSpecPrompt('custom', { ...blankSpecValues, customPrompt: '自定义' }),
|
||
).toBe('自定义');
|
||
expect(buildQuickEditModelOptions('nano-banana')).toEqual([
|
||
{
|
||
label: 'nanobanana2',
|
||
value: 'gemini-3.1-flash-image-preview',
|
||
},
|
||
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||
]);
|
||
expect(buildQuickEditModelOptions('nanobanana2')).toEqual([
|
||
{
|
||
label: 'nanobanana2',
|
||
value: 'gemini-3.1-flash-image-preview',
|
||
},
|
||
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||
]);
|
||
expect(
|
||
isQuickEditUnsupportedAssetKind({
|
||
...buildSourceLayer(),
|
||
assetKind: 'icon',
|
||
}),
|
||
).toBe(true);
|
||
expect(
|
||
isQuickEditUnsupportedAssetKind({
|
||
...buildSourceLayer(),
|
||
assetKind: 'icon-spritesheet',
|
||
}),
|
||
).toBe(false);
|
||
});
|
||
|
||
it('adds reference image semantics and snapshots for spec generation references', () => {
|
||
expect(
|
||
buildSpecPrompt(
|
||
'custom',
|
||
{ ...blankSpecValues, customPrompt: '生成一张怪兽规范图' },
|
||
true,
|
||
),
|
||
).toContain('生成一张怪兽规范图');
|
||
expect(
|
||
buildSpecGenerationInputs(
|
||
'custom',
|
||
{ ...blankSpecValues, customPrompt: '生成一张怪兽规范图' },
|
||
{
|
||
id: 'spec-ref',
|
||
label: '参考.png',
|
||
src: '/ref.png',
|
||
resourceId: 'resource-ref',
|
||
},
|
||
),
|
||
).toEqual({
|
||
fields: [{ title: '自定义规范提示词', value: '生成一张怪兽规范图' }],
|
||
references: [
|
||
{
|
||
title: '参考图',
|
||
label: '参考.png',
|
||
refType: 'project-resource',
|
||
refId: 'resource-ref',
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
it('uses objectKey for character animation references before falling back to src', () => {
|
||
expect(
|
||
resolveCharacterAnimationSourceImageSrc({
|
||
...buildSourceLayer(),
|
||
objectKey: 'generated/character.png',
|
||
}),
|
||
).toBe('generated/character.png');
|
||
expect(resolveCharacterAnimationSourceImageSrc(buildSourceLayer())).toBe(
|
||
'/source.png',
|
||
);
|
||
});
|
||
|
||
it('uses the extracted transparent frame when reusing a character animation sequence layer', () => {
|
||
expect(
|
||
resolveCharacterAnimationSourceImageSrc({
|
||
...buildSourceLayer(),
|
||
assetKind: 'character-animation',
|
||
mediaType: 'image-sequence',
|
||
src: '/generated-character-drafts/editor/frame01.png',
|
||
thumbnailSrc: '/generated-character-drafts/editor/frame01.png',
|
||
}),
|
||
).toBe('/generated-character-drafts/editor/frame01.png');
|
||
});
|
||
|
||
it('maps generation dialog mode and authorization errors to user-facing copy', () => {
|
||
const iconDialog: CanvasGenerationDialogState = {
|
||
id: 'dialog-icon',
|
||
mode: 'icon',
|
||
prompt: '',
|
||
status: 'idle',
|
||
};
|
||
|
||
expect(getGenerationFrameAriaLabel(iconDialog)).toBe('图标素材生成占位图');
|
||
expect(getGenerationFrameLabel(iconDialog)).toBe('Icon Generator');
|
||
expect(
|
||
getGenerationFrameAriaLabel({
|
||
...iconDialog,
|
||
mode: 'quick-edit',
|
||
}),
|
||
).toBe('快速编辑生成占位图');
|
||
expect(getGenerationFrameLabel({ ...iconDialog, mode: 'quick-edit' })).toBe(
|
||
'Quick Edit Generator',
|
||
);
|
||
expect(
|
||
buildSoundEffectGenerationInputs('金币掉落叮当声', 'audio1.0', 7),
|
||
).toEqual({
|
||
fields: [
|
||
{ title: 'prompt', value: '金币掉落叮当声' },
|
||
{ title: 'model', value: 'audio1.0' },
|
||
{ title: 'duration', value: '7秒' },
|
||
],
|
||
references: [],
|
||
});
|
||
expect(
|
||
resolveImageGenerationErrorMessage(
|
||
new ApiClientError({
|
||
message: '未授权访问(requestId: one)',
|
||
status: 401,
|
||
code: 'UNAUTHORIZED',
|
||
}),
|
||
),
|
||
).toBe('请先登录后再生成图片');
|
||
expect(
|
||
resolveImageGenerationErrorMessage(
|
||
new ApiClientError({
|
||
message: '请求冲突(requestId: wallet-one)',
|
||
status: 409,
|
||
code: 'CONFLICT',
|
||
details: { provider: 'profile-wallet' },
|
||
}),
|
||
),
|
||
).toBe('泥点余额不足,请充值后再生成');
|
||
expect(
|
||
resolveImageGenerationErrorMessage(
|
||
new ApiClientError({
|
||
message: '请求冲突(requestId: conflict-one)',
|
||
status: 409,
|
||
code: 'CONFLICT',
|
||
}),
|
||
),
|
||
).toBe('生成任务未完成,请稍后重试');
|
||
});
|
||
});
|
||
|
||
const blankSpecValues = {
|
||
playSetting: '',
|
||
artStyle: '',
|
||
bodyRatio: '3',
|
||
characterView: '',
|
||
customPrompt: '',
|
||
};
|
||
|
||
function buildSourceLayer(): CanvasLayer {
|
||
return {
|
||
id: 'layer-source',
|
||
resourceId: 'resource-source',
|
||
title: '原图',
|
||
src: '/source.png',
|
||
x: 0,
|
||
y: 0,
|
||
width: 512,
|
||
height: 512,
|
||
originalWidth: 512,
|
||
originalHeight: 512,
|
||
zIndex: 1,
|
||
sourceType: 'uploaded',
|
||
};
|
||
}
|