Files
Genarrative/src/components/image-editor/ImageCanvasGenerationModel.ts
T
lhk229 56fe1cb85d
Project CI / Native shell tests (pull_request) Successful in 12m45s
Project CI / Frontend tests (pull_request) Successful in 2m58s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Backend tests (pull_request) Failing after 9s
收紧编辑器内部处理元数据边界
普通用户与公开响应统一移除生成 provider、内部处理模型和抠图审计字段。
普通生成请求移除 segModel,由后端固定抠图策略并清理客户端伪造的保留字段。
手动去背景继承可信源模型,角色动作与去背景失败统一使用稳定用户文案。
后台原始审计继续保留真实 provider 与抠图模型,并补齐 Owner、Public、Admin 和紧凑结果测试。
同步更新前端响应契约、外部 OpenAPI、后端架构文档和项目决策记录。
2026-07-31 04:58:07 +00:00

1418 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ApiClientError } from '../../services/apiClient';
import type { EditorGenerationPricingConfig } from '../../services/image-editor/editorProjectClient';
import type {
EditorCharacterAnimationRatio,
EditorCharacterAnimationResolution,
} from '../../services/image-editor/editorProjectClient';
import {
isEditorInternalProcessingModel,
isGeneratedLayer,
} from './ImageCanvasEditorModel';
import type {
CanvasGenerationDialogState,
CanvasGenerationInputField,
CanvasGenerationInputReference,
CanvasGenerationInputs,
CanvasLayer,
CharacterReferenceImage,
GenerateDialogState,
PublicationMaterialsGameInfo,
PublicationMaterialsWorkflowId,
SpecFormValues,
SpecGenerationType,
} from './ImageCanvasEditorTypes';
import {
getPublicationMaterialsWorkflow,
type PublicationMaterialsWorkflow,
} from './ImageCanvasPublicationMaterialsModel';
// 中文注释:与 api-server/src/editor_generation_config.rs 保持同名模型定价语义,前端仅作为展示兜底。
export const IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2';
export const IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
export const DEFAULT_IMAGE_MODEL = IMAGE_MODEL_NANOBANANA2;
const IMAGE_MODEL_NANOBANANA_ALIASES = new Set([
IMAGE_MODEL_NANOBANANA2,
'nanobanana2',
'nano-banana',
]);
export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2;
export const SPEC_GENERATION_ASPECT_RATIO = '16:9';
export const SPEC_GENERATION_IMAGE_SIZE = '2K';
export const SPEC_GENERATION_SIZE = '2048x1152';
export const SPEC_FRAME_ORIGINAL_SIZE = { width: 2048, height: 1152 };
export const SPEC_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
export const CHARACTER_FRAME_ORIGINAL_SIZE = { width: 2048, height: 2048 };
export const CHARACTER_FRAME_DISPLAY_SIZE = { width: 420, height: 420 };
export const CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE = {
width: 1024,
height: 1024,
};
export const CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE = {
width: 420,
height: 420,
};
export const ICON_FRAME_ORIGINAL_SIZE = { width: 512, height: 512 };
export const ICON_FRAME_DISPLAY_SIZE = { width: 360, height: 360 };
export const UI_DESIGN_FRAME_ORIGINAL_SIZE = { width: 2048, height: 1152 };
export const UI_DESIGN_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
export const PUBLICATION_FRAME_ORIGINAL_SIZE: Record<
PublicationMaterialsWorkflowId,
{ width: number; height: number }
> = {
'publication-cover-image': { width: 720, height: 540 },
'publication-detail-gallery': { width: 720, height: 1280 },
'publication-promo-poster': { width: 1280, height: 720 },
};
export const PUBLICATION_FRAME_DISPLAY_SIZE: Record<
PublicationMaterialsWorkflowId,
{ width: number; height: number }
> = {
'publication-cover-image': { width: 480, height: 360 },
'publication-detail-gallery': { width: 360, height: 640 },
'publication-promo-poster': { width: 560, height: 315 },
};
export const EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG = {
[IMAGE_MODEL_NANOBANANA2]: {
'0.5K': 8,
'1K': 12,
'2K': 24,
},
[IMAGE_MODEL_GPT_IMAGE_2]: {
'1K': 3,
'2K': 5,
},
} as const;
export const SPEC_GENERATION_COST =
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[SPEC_GENERATION_MODEL][
SPEC_GENERATION_IMAGE_SIZE
];
export const DEFAULT_PUBLICATION_GAME_INFO: PublicationMaterialsGameInfo = {
gameName: '',
gameCategories: '',
gameDescription: '',
};
export const DEFAULT_ICON_DESCRIPTIONS = [
'返回按钮',
'设置按钮',
'下一关按钮',
'提示按钮',
'原图按钮',
'冻结按钮',
];
export const DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR = 'auto';
export const UI_DESIGN_ASSET_EXTRACTION_PROMPT =
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。';
export const QUICK_EDIT_SIZE_PRESETS = [
'1024x1024',
'1536x1024',
'2048x1152',
'1024x1536',
] as const;
export const EDITOR_IMAGE_MODEL_OPTIONS = [
{ label: 'nanobanana2', value: IMAGE_MODEL_NANOBANANA2 },
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
] as const;
export const QUICK_EDIT_MODEL_OPTIONS = EDITOR_IMAGE_MODEL_OPTIONS;
export const EDITOR_IMAGE_DIMENSION_OPTIONS = {
[IMAGE_MODEL_NANOBANANA2]: {
aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'],
imageSizes: ['0.5K', '1K', '2K'],
},
[IMAGE_MODEL_GPT_IMAGE_2]: {
aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'],
imageSizes: ['1K', '2K'],
},
} as const;
type EditorImageFrameSize = {
width: number;
height: number;
};
function getSafeImageSizeValue(imageSize: string | null | undefined) {
return (imageSize ?? '1K').trim().toUpperCase();
}
function getImageSizeLongEdge(imageSize: string | null | undefined) {
const normalizedImageSize = getSafeImageSizeValue(imageSize);
if (normalizedImageSize === '0.5K') {
return 512;
}
if (normalizedImageSize === '2K') {
return 2048;
}
return 1024;
}
function resolveAspectRatioNumbers(aspectRatio: string | null | undefined) {
const [rawWidth = 1, rawHeight = 1] = (aspectRatio ?? '1:1')
.split(':')
.map((value) => Number(value));
const width = Number.isFinite(rawWidth) && rawWidth > 0 ? rawWidth : 1;
const height = Number.isFinite(rawHeight) && rawHeight > 0 ? rawHeight : 1;
return { width, height };
}
function resolveImageFrameSizeFromRatio({
aspectRatio,
imageSize,
}: {
aspectRatio: string | null | undefined;
imageSize: string | null | undefined;
}): EditorImageFrameSize {
const longEdge = getImageSizeLongEdge(imageSize);
const ratio = resolveAspectRatioNumbers(aspectRatio);
if (ratio.width >= ratio.height) {
return {
width: longEdge,
height: Math.max(1, Math.round((longEdge * ratio.height) / ratio.width)),
};
}
return {
width: Math.max(1, Math.round((longEdge * ratio.width) / ratio.height)),
height: longEdge,
};
}
export function resolveEditorImageGenerationPixelSize({
model: _model,
aspectRatio,
imageSize,
}: {
model: string | null | undefined;
aspectRatio: string | null | undefined;
imageSize: string | null | undefined;
}): EditorImageFrameSize {
return resolveImageFrameSizeFromRatio({ aspectRatio, imageSize });
}
export function resolveEditorImageSizeLabel({
aspectRatio,
imageSize,
}: {
aspectRatio: string | null | undefined;
imageSize: string | null | undefined;
}) {
return `${aspectRatio?.trim() || '1:1'}·${getSafeImageSizeValue(imageSize)}`;
}
export function inferEditorImageAspectRatio(width: number, height: number) {
const safeWidth = Number.isFinite(width) && width > 0 ? Math.round(width) : 1;
const safeHeight =
Number.isFinite(height) && height > 0 ? Math.round(height) : 1;
const commonRatios = EDITOR_IMAGE_DIMENSION_OPTIONS[IMAGE_MODEL_NANOBANANA2]
.aspectRatios as readonly string[];
const sourceRatio = safeWidth / safeHeight;
return commonRatios.reduce((bestRatio, ratio) => {
const bestNumbers = resolveAspectRatioNumbers(bestRatio);
const ratioNumbers = resolveAspectRatioNumbers(ratio);
const bestDiff = Math.abs(
bestNumbers.width / bestNumbers.height - sourceRatio,
);
const ratioDiff = Math.abs(
ratioNumbers.width / ratioNumbers.height - sourceRatio,
);
return ratioDiff < bestDiff ? ratio : bestRatio;
}, commonRatios[0] ?? '1:1');
}
export function inferEditorImageSizeLabel(width: number, height: number) {
const longEdge = Math.max(width, height);
if (longEdge <= 768) {
return '0.5K';
}
if (longEdge > 1536) {
return '2K';
}
return '1K';
}
export function resizeGenerationPlaceholderToImageSelection<
T extends GenerateDialogState,
>(dialog: T): T {
if (!dialog.placeholder) {
return dialog;
}
const size = resolveEditorImageGenerationPixelSize({
model: dialog.imageModel,
aspectRatio: dialog.aspectRatio,
imageSize: dialog.imageSize,
});
const centerX = dialog.placeholder.x + dialog.placeholder.width / 2;
const centerY = dialog.placeholder.y + dialog.placeholder.height / 2;
return {
...dialog,
placeholder: {
...dialog.placeholder,
x: centerX - size.width / 2,
y: centerY - size.height / 2,
width: size.width,
height: size.height,
originalWidth: size.width,
originalHeight: size.height,
},
} as T;
}
function resolveEvenVideoWidth(
height: number,
aspectRatio: string | null | undefined,
) {
const ratio = resolveAspectRatioNumbers(aspectRatio ?? '16:9');
const rawWidth = Math.max(
1,
Math.round((height * ratio.width) / ratio.height),
);
return rawWidth % 2 === 0 ? rawWidth : rawWidth + 1;
}
export function resolveEditorVideoGenerationPixelSize({
aspectRatio,
resolution,
}: {
aspectRatio: string | null | undefined;
resolution: '480p' | '720p' | '1080p' | string | null | undefined;
}): EditorImageFrameSize {
const height =
resolution === '1080p' ? 1080 : resolution === '720p' ? 720 : 480;
return {
width: resolveEvenVideoWidth(height, aspectRatio),
height,
};
}
export function resizeGenerationPlaceholderToVideoSelection(
dialog: GenerateDialogState,
): GenerateDialogState {
if (!dialog.placeholder) {
return dialog;
}
const size = resolveEditorVideoGenerationPixelSize({
aspectRatio: dialog.videoAspectRatio,
resolution: dialog.videoResolution,
});
const centerX = dialog.placeholder.x + dialog.placeholder.width / 2;
const centerY = dialog.placeholder.y + dialog.placeholder.height / 2;
return {
...dialog,
placeholder: {
...dialog.placeholder,
x: centerX - size.width / 2,
y: centerY - size.height / 2,
width: size.width,
height: size.height,
originalWidth: size.width,
originalHeight: size.height,
},
};
}
export const VIDEO_MODEL_SEEDANCE_2 = 'seedance2.0';
export const VIDEO_MODEL_SEEDANCE_2_FAST = 'seedance2.0-fast';
export const VIDEO_MODEL_KLING_3 = 'kling3.0';
export const VIDEO_MODEL_KLING_3_OMNI = 'kling3.0-omni';
export const VIDEO_MODEL_VEO_3_1 = 'veo3.1';
export const VIDEO_MODEL_VEO_3_1_FAST = 'veo3.1-fast';
export const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST;
export const CHARACTER_ANIMATION_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST;
export const SOUND_EFFECT_MODEL_VIDU = 'audio1.0';
export const DEFAULT_SOUND_EFFECT_MODEL = SOUND_EFFECT_MODEL_VIDU;
export const DEFAULT_SOUND_EFFECT_DURATION_SECONDS = 5;
export const SOUND_EFFECT_DURATION_OPTIONS = Array.from(
{ length: 9 },
(_, index) => index + 2,
);
export const BACKGROUND_MUSIC_MODEL_SUNO = 'chirp-v5';
export const DEFAULT_BACKGROUND_MUSIC_MODEL = BACKGROUND_MUSIC_MODEL_SUNO;
const EDITOR_VIDEO_RESOLUTION_RATE_CONFIG = {
'480p': 10,
'720p': 20,
'1080p': 40,
} as const;
const EDITOR_VIDEO_STANDARD_RESOLUTION_RATE_CONFIG = {
'480p': 12,
'720p': 24,
'1080p': 48,
} as const;
const EDITOR_VIDEO_KLING_RESOLUTION_RATE_CONFIG = {
'480p': 15,
'720p': 30,
'1080p': 60,
} as const;
const EDITOR_VIDEO_KLING_OMNI_RESOLUTION_RATE_CONFIG = {
'480p': 20,
'720p': 40,
'1080p': 80,
} as const;
export const EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG = {
[VIDEO_MODEL_SEEDANCE_2_FAST]: EDITOR_VIDEO_RESOLUTION_RATE_CONFIG,
[VIDEO_MODEL_SEEDANCE_2]: EDITOR_VIDEO_STANDARD_RESOLUTION_RATE_CONFIG,
[VIDEO_MODEL_KLING_3]: EDITOR_VIDEO_KLING_RESOLUTION_RATE_CONFIG,
[VIDEO_MODEL_KLING_3_OMNI]: EDITOR_VIDEO_KLING_OMNI_RESOLUTION_RATE_CONFIG,
// 中文注释:前端不展示 Veo 入口,但客户端类型和后端兼容层仍可接收旧布局回放,定价表必须同步覆盖。
[VIDEO_MODEL_VEO_3_1]: EDITOR_VIDEO_RESOLUTION_RATE_CONFIG,
[VIDEO_MODEL_VEO_3_1_FAST]: EDITOR_VIDEO_RESOLUTION_RATE_CONFIG,
} as const;
export const EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG = {
[SOUND_EFFECT_MODEL_VIDU]: 5,
} as const;
export const EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG = {
[BACKGROUND_MUSIC_MODEL_SUNO]: 12,
} as const;
export const EDITOR_MODEL_MUD_POINT_CONFIG = {
[IMAGE_MODEL_NANOBANANA2]: {
unit: 'perGeneration',
prices: EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_NANOBANANA2],
},
[IMAGE_MODEL_GPT_IMAGE_2]: {
unit: 'perGeneration',
prices: EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2],
},
[VIDEO_MODEL_SEEDANCE_2_FAST]: {
unit: 'perSecond',
prices: EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_SEEDANCE_2_FAST],
},
[VIDEO_MODEL_SEEDANCE_2]: {
unit: 'perSecond',
prices: EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_SEEDANCE_2],
},
[VIDEO_MODEL_KLING_3]: {
unit: 'perSecond',
prices: EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_KLING_3],
},
[VIDEO_MODEL_KLING_3_OMNI]: {
unit: 'perSecond',
prices: EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_KLING_3_OMNI],
},
[VIDEO_MODEL_VEO_3_1]: {
unit: 'perSecond',
prices: EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_VEO_3_1],
},
[VIDEO_MODEL_VEO_3_1_FAST]: {
unit: 'perSecond',
prices: EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[VIDEO_MODEL_VEO_3_1_FAST],
},
[SOUND_EFFECT_MODEL_VIDU]: {
unit: 'perGeneration',
price: EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG[SOUND_EFFECT_MODEL_VIDU],
},
[BACKGROUND_MUSIC_MODEL_SUNO]: {
unit: 'perGeneration',
price:
EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG[
BACKGROUND_MUSIC_MODEL_SUNO
],
},
} satisfies EditorGenerationPricingConfig['models'];
let runtimeEditorGenerationPricingConfig: EditorGenerationPricingConfig = {
models: cloneModelPricing(EDITOR_MODEL_MUD_POINT_CONFIG),
};
export const EDITOR_SOUND_EFFECT_MODEL_OPTIONS = [
{ label: 'Vidu', value: SOUND_EFFECT_MODEL_VIDU },
] as const;
export const SEEDANCE_VIDEO_REFERENCE_LIMITS = {
image: 9,
video: 3,
audio: 3,
} as const;
export const QUICK_EDIT_REFERENCE_LIMIT = 8;
export const CHARACTER_ANIMATION_ACTION_PROMPTS = [
{ label: '待机', text: '待机动作,轻微呼吸起伏。' },
{ label: '行走', text: '循环行走动作,步伐稳定。' },
{ label: '奔跑', text: '循环奔跑动作,动作清晰有力。' },
{ label: '跳跃', text: '起跳、滞空、落地动作。' },
{ label: '攻击', text: '攻击动作,前摇、出手、收招清晰。' },
{ label: '受击', text: '受击后短暂后仰并恢复站姿。' },
{ label: '倒下', text: '倒下动作,重心下落自然。' },
] as const;
export const CHARACTER_ANIMATION_RATIO_OPTIONS: Array<{
label: string;
value: EditorCharacterAnimationRatio;
}> = [
{ label: '与角色图片保持同尺寸', value: 'same' },
{ label: '1:1', value: '1:1' },
{ label: '4:3', value: '4:3' },
{ label: '16:9', value: '16:9' },
{ label: '9:16', value: '9:16' },
{ label: '3:4', value: '3:4' },
];
export const CHARACTER_ANIMATION_DURATION_OPTIONS = [
{ label: '32帧·4秒', frameCount: 32, durationSeconds: 4 },
{ label: '40帧·5秒', frameCount: 40, durationSeconds: 5 },
{ label: '48帧·6秒', frameCount: 48, durationSeconds: 6 },
] as const;
export const VIDEO_FRAME_ORIGINAL_SIZE = { width: 854, height: 480 };
export const VIDEO_FRAME_DISPLAY_SIZE = { width: 854, height: 480 };
export const AUDIO_FRAME_ORIGINAL_SIZE = { width: 420, height: 120 };
export const AUDIO_FRAME_DISPLAY_SIZE = { width: 420, height: 120 };
export const EDITOR_VIDEO_MODEL_OPTIONS = [
{ label: 'Seedance 2.0 Fast', value: VIDEO_MODEL_SEEDANCE_2_FAST },
{ label: 'Seedance 2.0', value: VIDEO_MODEL_SEEDANCE_2 },
{ label: 'Kling 3.0', value: VIDEO_MODEL_KLING_3 },
{ label: 'Kling 3.0 Omni', value: VIDEO_MODEL_KLING_3_OMNI },
] as const;
export const EDITOR_VIDEO_DURATION_OPTIONS = [
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
] as const;
export const EDITOR_VIDEO_ASPECT_RATIO_OPTIONS = [
'16:9',
'9:16',
'1:1',
'4:3',
'3:4',
'21:9',
] as const;
export const EDITOR_VIDEO_RESOLUTION_OPTIONS = [
'480p',
'720p',
'1080p',
] as const;
export const DEFAULT_VIDEO_ASPECT_RATIO = '16:9';
export const DEFAULT_VIDEO_DURATION_SECONDS = 4;
export const DEFAULT_VIDEO_SOUND = 'on';
export const DEFAULT_VIDEO_WEB_SEARCH_ENABLED = true;
export const DEFAULT_SPEC_FORM_VALUES: Record<
SpecGenerationType,
SpecFormValues
> = {
character: {
playSetting: '战棋类RPG玩法',
artStyle: '像素风',
bodyRatio: '3',
characterView:
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
customPrompt: '',
},
ui: {
playSetting: '抓娃娃题材的抓大鹅玩法',
artStyle: '毛茸茸',
bodyRatio: '3',
characterView:
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
customPrompt: '',
},
icon: {
playSetting: '休闲小游戏',
artStyle: '清爽卡通',
bodyRatio: '3',
characterView:
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
customPrompt: '',
},
custom: {
playSetting: '',
artStyle: '',
bodyRatio: '3',
characterView:
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
customPrompt: '',
},
};
export const SPEC_TYPE_LABEL: Record<SpecGenerationType, string> = {
character: '角色规范',
ui: '图标规范',
icon: '图标规范',
custom: '自定义规范',
};
export const CHARACTER_SPEC_VIEW_OPTIONS = [
DEFAULT_SPEC_FORM_VALUES.character.characterView,
'左向三分之二侧身站姿',
'左向三分之二侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯左视图,也禁止生成正面立绘。',
'右向三分之二侧身站姿,保留少量正面信息,强调面部轮廓、胸肩结构与主要装备层次。',
'背向斜侧身站姿,保留少量侧脸信息,突出背部服饰层次、武器挂载与轮廓识别。',
];
export function buildQuickEditSizeOptions(currentSize: string) {
return Array.from(new Set([currentSize, ...QUICK_EDIT_SIZE_PRESETS]));
}
export function normalizeEditorImageModel(model: string | null | undefined) {
const normalizedModel = model?.trim();
if (!normalizedModel) {
return DEFAULT_IMAGE_MODEL;
}
if (IMAGE_MODEL_NANOBANANA_ALIASES.has(normalizedModel)) {
return IMAGE_MODEL_NANOBANANA2;
}
return normalizedModel;
}
export function getEditorImageModelDisplayName(
model: string | null | undefined,
) {
const normalizedModel = normalizeEditorImageModel(model);
return (
EDITOR_IMAGE_MODEL_OPTIONS.find(
(option) => option.value === normalizedModel,
)?.label ?? normalizedModel
);
}
export function isEditorUserVisibleGenerationInputField(
field: CanvasGenerationInputField,
) {
return field.title.trim() !== '处理模型';
}
export function getEditorLayerModelDisplayName(
model: string | null | undefined,
) {
if (!model?.trim() || isEditorInternalProcessingModel(model)) {
return '-';
}
return getEditorImageModelDisplayName(model);
}
export function getEditorGenerationModelDisplayName(
model: string | null | undefined,
) {
const normalizedModel = model?.trim();
if (normalizedModel === BACKGROUND_MUSIC_MODEL_SUNO) {
return 'Suno';
}
const soundEffectLabel = EDITOR_SOUND_EFFECT_MODEL_OPTIONS.find(
(option) => option.value === normalizedModel,
)?.label;
if (soundEffectLabel) {
return soundEffectLabel;
}
const videoLabel = EDITOR_VIDEO_MODEL_OPTIONS.find(
(option) => option.value === normalizedModel,
)?.label;
if (videoLabel) {
return videoLabel;
}
return getEditorImageModelDisplayName(normalizedModel);
}
export function buildQuickEditModelOptions(currentModel: string) {
void currentModel;
const options = [...QUICK_EDIT_MODEL_OPTIONS];
return options;
}
export function isQuickEditUnsupportedAssetKind(layer: CanvasLayer) {
return layer.assetKind === 'icon';
}
export function buildCharacterSpecPrompt(values: SpecFormValues) {
return [
'生成2D 角色美术视觉规范设定图,纯白底板,整齐排布全身标准立绘;固定统一头身比例、勾线粗细恒定;展示待机行走攻击基础动作帧样例,重心对齐不变位,服饰配饰分层结构示意,搭配专属角色色卡标注色号,无多余杂物,精准尺寸标注,高清矢量规范稿',
'禁止模糊、笔触杂乱、光影方向混乱、比例畸形、3D 渲染、实景照片、水印、花纹堆砌、画面抖动错位效果、噪点,',
`玩法设计:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.character.playSetting}`,
`美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.character.artStyle}`,
`头身比:${values.bodyRatio.trim() || DEFAULT_SPEC_FORM_VALUES.character.bodyRatio}`,
`视角要求:${values.characterView.trim() || DEFAULT_SPEC_FORM_VALUES.character.characterView}`,
].join('\n');
}
export function buildUiSpecPrompt(values: SpecFormValues) {
return [
'生成一张完整游戏UI规范汇总设定展板,纯白色干净背景,Figma专业设计稿质感,矢量锐利线条,页面划分九大区域:色彩规范、字体规范、图标规范、按钮规范、组件规范、布局规范、特效规范、IP规范、主视觉。主视觉居中较大显示,其他八个区域环绕主视觉',
'',
`玩法设定:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.ui.playSetting}`,
`美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.ui.artStyle}`,
].join('\n');
}
export function buildIconSpecPrompt(values: SpecFormValues) {
return [
'生成一张游戏图标素材视觉规范展板,纯白色干净背景,展示按钮图标的统一视角、线条粗细、填充风格、描边、阴影、圆角、材质、状态层级和色彩规范,图标样例需要成组排列且风格高度统一。',
'',
`玩法设定:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.icon.playSetting}`,
`美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.icon.artStyle}`,
].join('\n');
}
export function buildSpecPrompt(
type: SpecGenerationType,
values: SpecFormValues,
hasReferenceImage = false,
) {
const prompt =
type === 'character'
? buildCharacterSpecPrompt(values)
: type === 'ui'
? buildUiSpecPrompt(values)
: type === 'icon'
? buildIconSpecPrompt(values)
: values.customPrompt.trim();
if (!hasReferenceImage) {
return prompt;
}
return [
'参考图生成规范:严格参考图1的构图、风格、材质、色彩、形状语言和视觉层级生成本次规范图;参考图只作为美术方向和规范语义依据,不要直接复制参考图中的文字、水印或无关背景。',
prompt,
].join('\n');
}
export function getLayerKindLabel(layer: CanvasLayer) {
if (layer.assetKind === 'spec') {
return '规范';
}
if (layer.assetKind === 'character') {
return '角色';
}
if (layer.assetKind === 'character-animation') {
return '动作';
}
if (layer.assetKind === 'icon') {
return '图标';
}
if (layer.assetKind === 'icon-spritesheet') {
return '图集';
}
if (layer.assetKind === 'icon-spec') {
return '图标规范';
}
if (layer.assetKind === 'publication-material') {
return '宣发素材';
}
if (layer.assetKind === 'ui-design') {
return 'UI设计';
}
if (layer.assetKind === 'video' || layer.mediaType === 'video') {
return '视频';
}
if (layer.assetKind === 'sound-effect') {
return '音效';
}
if (layer.assetKind === 'background-music') {
return '背景音乐';
}
if (layer.mediaType === 'audio') {
return '音频';
}
if (layer.mediaType === 'image-sequence') {
return '序列帧';
}
return null;
}
export function buildCharacterAnimationGenerationInputs(
promptText: string,
sourceLayer: CanvasLayer,
): CanvasGenerationInputs {
return {
fields: createGenerationInputField('动作描述', promptText),
references: createLayerGenerationInputReference('角色图片', sourceLayer),
};
}
export function formatLayerImageType(layer: CanvasLayer) {
if (layer.assetKind === 'spec') {
return '规范图片';
}
if (layer.assetKind === 'character') {
return '角色图片';
}
if (layer.assetKind === 'character-animation') {
return '角色动作序列帧';
}
if (layer.assetKind === 'icon') {
return '图标素材图片';
}
if (layer.assetKind === 'icon-spritesheet') {
return '图标素材图集';
}
if (layer.assetKind === 'icon-spec') {
return '图标规范图片';
}
if (layer.assetKind === 'publication-material') {
return '宣发素材图片';
}
if (layer.assetKind === 'ui-design') {
return 'UI设计图';
}
if (layer.assetKind === 'video' || layer.mediaType === 'video') {
return '生成视频';
}
if (layer.assetKind === 'sound-effect') {
return '游戏音效';
}
if (layer.assetKind === 'background-music') {
return '游戏背景音乐';
}
if (layer.mediaType === 'audio') {
return '生成音频';
}
if (layer.mediaType === 'image-sequence') {
return '序列帧';
}
return isGeneratedLayer(layer) ? '生成图片' : '上传图片';
}
function normalizeImagePriceSize(
model: string,
imageSize: string | null | undefined,
) {
const normalizedImageSize = getSafeImageSizeValue(imageSize);
if (model === IMAGE_MODEL_NANOBANANA2 && normalizedImageSize === '0.5K') {
return '0.5K';
}
if (normalizedImageSize === '2K') {
return '2K';
}
return '1K';
}
function getModelPrices(model: string | null | undefined) {
const normalizedModel = model?.trim() || '';
return normalizedModel
? runtimeEditorGenerationPricingConfig.models[normalizedModel]
: undefined;
}
function readModelTierPrice({
model,
fallbackModel,
tier,
fallbackTier,
}: {
model: string;
fallbackModel: string;
tier: string;
fallbackTier: string;
}) {
const modelPricing = getModelPrices(model) ?? getModelPrices(fallbackModel);
const fallbackPricing = getModelPrices(fallbackModel);
return (
modelPricing?.prices?.[tier] ??
modelPricing?.prices?.[fallbackTier] ??
modelPricing?.price ??
fallbackPricing?.prices?.[tier] ??
fallbackPricing?.prices?.[fallbackTier] ??
fallbackPricing?.price ??
0
);
}
function readModelFlatPrice(model: string, fallbackModel: string) {
return (
getModelPrices(model)?.price ?? getModelPrices(fallbackModel)?.price ?? 0
);
}
export function calculateCharacterAnimationPrice(
model: string | null | undefined,
resolution: EditorCharacterAnimationResolution,
durationSeconds: number,
) {
const normalizedModel = (model ?? CHARACTER_ANIMATION_MODEL).trim();
return (
readModelTierPrice({
model: normalizedModel,
fallbackModel: CHARACTER_ANIMATION_MODEL,
tier: resolution,
fallbackTier: '480p',
}) * durationSeconds
);
}
export function calculateEditorVideoPrice(
model: string | null | undefined,
resolution: '480p' | '720p' | '1080p',
durationSeconds: number,
) {
const normalizedModel = (model ?? DEFAULT_VIDEO_MODEL).trim();
return (
readModelTierPrice({
model: normalizedModel,
fallbackModel: DEFAULT_VIDEO_MODEL,
tier: resolution,
fallbackTier: '480p',
}) * durationSeconds
);
}
export function calculateEditorImageModelPrice(
model: string | null | undefined,
imageSize: string | null | undefined = '1K',
) {
const normalizedModel = normalizeEditorImageModel(model);
const normalizedSize = normalizeImagePriceSize(normalizedModel, imageSize);
return readModelTierPrice({
model: normalizedModel,
fallbackModel: DEFAULT_IMAGE_MODEL,
tier: normalizedSize,
fallbackTier: '1K',
});
}
export function calculateEditorSpecGenerationPrice(
model: string | null | undefined = SPEC_GENERATION_MODEL,
) {
return calculateEditorImageModelPrice(
model ?? SPEC_GENERATION_MODEL,
SPEC_GENERATION_IMAGE_SIZE,
);
}
export function calculateEditorImageGenerationPrice({
kind,
model,
imageSize,
}: {
kind?: string | null;
model?: string | null;
imageSize?: string | null;
} = {}) {
const normalizedKind = kind?.trim();
if (normalizedKind === 'spec') {
return calculateEditorSpecGenerationPrice(model);
}
return calculateEditorImageModelPrice(model, imageSize);
}
export function calculateEditorIconSpritesheetPrice(
model: string | null | undefined,
imageSize: string | null | undefined = '1K',
) {
return calculateEditorImageModelPrice(model, imageSize);
}
export function calculateEditorUiDesignPrice(
model: string | null | undefined,
imageSize: string | null | undefined = '1K',
) {
return calculateEditorImageModelPrice(model, imageSize);
}
export function calculateEditorSoundEffectPrice(
model: string | null | undefined,
) {
const normalizedModel = (model ?? DEFAULT_SOUND_EFFECT_MODEL).trim();
return readModelFlatPrice(normalizedModel, DEFAULT_SOUND_EFFECT_MODEL);
}
export function calculateEditorBackgroundMusicPrice(
model: string | null | undefined = DEFAULT_BACKGROUND_MUSIC_MODEL,
) {
const normalizedModel = (model ?? DEFAULT_BACKGROUND_MUSIC_MODEL).trim();
return readModelFlatPrice(normalizedModel, DEFAULT_BACKGROUND_MUSIC_MODEL);
}
export function applyEditorGenerationPricingConfig(
pricing: EditorGenerationPricingConfig,
) {
runtimeEditorGenerationPricingConfig = {
models: cloneModelPricing(pricing.models),
};
}
function cloneModelPricing<T extends EditorGenerationPricingConfig['models']>(
value: T,
): EditorGenerationPricingConfig['models'] {
return Object.fromEntries(
Object.entries(value).map(([model, pricing]) => [
model,
{
unit: pricing.unit,
...(typeof pricing.price === 'number' ? { price: pricing.price } : {}),
...(pricing.prices ? { prices: { ...pricing.prices } } : {}),
},
]),
);
}
export function resolveCharacterAnimationSourceImageSrc(layer: CanvasLayer) {
if (layer.assetKind === 'character-animation' && layer.thumbnailSrc?.trim()) {
return layer.thumbnailSrc.trim();
}
// 中文注释:角色图已持久化到 OSS 时优先传 objectKey,避免把大 Data URL 塞进 JSON 请求体触发 body limit。
return layer.objectKey?.trim() || layer.src;
}
export function createCanvasLayerReference(
layer: CanvasLayer,
): CharacterReferenceImage {
const mediaType =
layer.mediaType === 'video' || layer.mediaType === 'audio'
? layer.mediaType
: undefined;
return {
id: `canvas-${layer.id}`,
label: layer.title,
src: layer.objectKey?.trim() || layer.src,
...(mediaType ? { mediaType } : {}),
objectKey: layer.objectKey ?? undefined,
assetObjectId: layer.assetObjectId ?? undefined,
resourceId: layer.resourceId,
sourceAssetId: layer.sourceAssetId ?? undefined,
};
}
export function createGenerationInputReference(
title: string,
reference: Pick<
CharacterReferenceImage,
'label' | 'resourceId' | 'sourceAssetId'
>,
): CanvasGenerationInputReference[] {
const resourceId = reference.resourceId?.trim();
if (
resourceId &&
!resourceId.startsWith('local-') &&
!resourceId.startsWith('generation-dialog:')
) {
return [
{
title,
label: reference.label,
refType: 'project-resource',
refId: resourceId,
},
];
}
const sourceAssetId = reference.sourceAssetId?.trim();
return sourceAssetId && !sourceAssetId.startsWith('upload-')
? [
{
title,
label: reference.label,
refType: 'asset',
refId: sourceAssetId,
},
]
: [];
}
export function createLayerGenerationInputReference(
title: string,
layer: CanvasLayer,
): CanvasGenerationInputReference[] {
return createGenerationInputReference(title, {
label: layer.title,
resourceId: layer.resourceId,
sourceAssetId: layer.sourceAssetId,
});
}
export function appendLimitedQuickEditReferences(
references: CharacterReferenceImage[] | undefined,
nextReferences: CharacterReferenceImage[],
) {
const imageReferences = nextReferences.filter(
(reference) => (reference.mediaType ?? 'image') === 'image',
);
return [...(references ?? []), ...imageReferences].slice(
0,
QUICK_EDIT_REFERENCE_LIMIT,
);
}
export function createGenerationInputField(
title: string,
value: string | null | undefined,
): CanvasGenerationInputField[] {
const normalizedValue = value?.trim();
return normalizedValue ? [{ title, value: normalizedValue }] : [];
}
export function buildImageGenerationInputs(
prompt: string,
references?: CharacterReferenceImage[],
): CanvasGenerationInputs {
return {
fields: createGenerationInputField('生成提示词', prompt),
references: (references ?? []).flatMap((reference, index) =>
createGenerationInputReference(`参考图 ${index + 1}`, reference),
),
};
}
export function buildVideoGenerationInputs(
prompt: string,
references?: CharacterReferenceImage[],
): CanvasGenerationInputs {
const mediaIndexes = {
image: 0,
video: 0,
audio: 0,
};
return {
fields: createGenerationInputField('视频描述', prompt),
references: (references ?? []).flatMap((reference) => {
const mediaType =
reference.mediaType === 'video' || reference.mediaType === 'audio'
? reference.mediaType
: 'image';
mediaIndexes[mediaType] += 1;
const title =
mediaType === 'video'
? `参考视频 ${mediaIndexes.video}`
: mediaType === 'audio'
? `参考音频 ${mediaIndexes.audio}`
: `参考图 ${mediaIndexes.image}`;
return createGenerationInputReference(title, reference);
}),
};
}
export function buildSoundEffectGenerationInputs(
prompt: string,
model: string,
duration: number,
): CanvasGenerationInputs {
return {
fields: [
...createGenerationInputField('prompt', prompt),
...createGenerationInputField('model', model),
...createGenerationInputField('duration', `${duration}秒`),
],
references: [],
};
}
export function buildBackgroundMusicGenerationInputs(
gptDescriptionPrompt: string,
): CanvasGenerationInputs {
return {
fields: createGenerationInputField(
'gpt_description_prompt',
gptDescriptionPrompt,
),
references: [],
};
}
export function buildSpecGenerationInputs(
specType: SpecGenerationType,
values: SpecFormValues,
reference?: CharacterReferenceImage | null,
): CanvasGenerationInputs {
const references = reference
? createGenerationInputReference('参考图', reference)
: [];
if (specType === 'custom') {
return {
fields: createGenerationInputField(
'自定义规范提示词',
values.customPrompt,
),
references,
};
}
const baseFields = [
...createGenerationInputField('玩法设定', values.playSetting),
...createGenerationInputField('美术风格', values.artStyle),
];
if (specType === 'character') {
baseFields.push(
...createGenerationInputField('头身比', values.bodyRatio),
...createGenerationInputField('角色视角', values.characterView),
);
}
return {
fields: baseFields,
references,
};
}
export function buildCharacterGenerationInputs(
prompt: string,
specReference: CharacterReferenceImage | null | undefined,
references: CharacterReferenceImage[] | undefined,
): CanvasGenerationInputs {
return {
fields: createGenerationInputField('角色设定', prompt),
references: [
...(specReference
? createGenerationInputReference('角色规范', specReference)
: []),
...(references ?? []).flatMap((reference, index) =>
createGenerationInputReference(`常规参考图 ${index + 1}`, reference),
),
],
};
}
export function buildUiDesignGenerationInputs(
prompt: string,
specReference: CharacterReferenceImage | null | undefined,
references?: CharacterReferenceImage[],
): CanvasGenerationInputs {
return {
fields: createGenerationInputField('用户输入', prompt),
references: [
...(specReference
? createGenerationInputReference('图标规范', specReference)
: []),
...(references ?? []).flatMap((reference, index) =>
createGenerationInputReference(`参考图 ${index + 1}`, reference),
),
],
};
}
function normalizePublicationGameInfo(
gameInfo: PublicationMaterialsGameInfo | null | undefined,
): PublicationMaterialsGameInfo {
return {
gameName: gameInfo?.gameName?.trim() ?? '',
gameCategories: gameInfo?.gameCategories?.trim() ?? '',
gameDescription: gameInfo?.gameDescription?.trim() ?? '',
};
}
export function buildPublicationMaterialsPrompt(
gameInfo: PublicationMaterialsGameInfo | null | undefined,
) {
const normalizedGameInfo = normalizePublicationGameInfo(gameInfo);
return [
...createGenerationInputField('游戏名', normalizedGameInfo.gameName),
...createGenerationInputField(
'游戏分类',
normalizedGameInfo.gameCategories,
),
...createGenerationInputField(
'一句话描述游戏',
normalizedGameInfo.gameDescription,
),
]
.map((field) => `${field.title}${field.value}`)
.join('\n');
}
export function buildPublicationMaterialsGenerationPrompt({
gameInfo,
workflow,
}: {
gameInfo: PublicationMaterialsGameInfo | null | undefined;
workflow: PublicationMaterialsWorkflow;
}) {
const inputPrompt = buildPublicationMaterialsPrompt(gameInfo);
return [
`【宣发素材类型】${workflow.promptLabel ?? workflow.label}`,
inputPrompt
? `【游戏输入】\n${inputPrompt}`
: '【游戏输入】\n请根据参考图生成宣发素材。',
`【生图约束】\n${workflow.promptConstraints.join('\n')}`,
`【参考图约束】\n${workflow.referenceConstraints.join('\n')}`,
`【输出检查】\n${workflow.postProcessConstraints.join('\n')}`,
].join('\n\n');
}
export function buildPublicationMaterialsGenerationInputs(
gameInfo: PublicationMaterialsGameInfo | null | undefined,
references: CharacterReferenceImage[] | undefined,
): CanvasGenerationInputs {
const normalizedGameInfo = normalizePublicationGameInfo(gameInfo);
return {
fields: [
...createGenerationInputField('游戏名', normalizedGameInfo.gameName),
...createGenerationInputField(
'游戏分类',
normalizedGameInfo.gameCategories,
),
...createGenerationInputField(
'一句话描述游戏',
normalizedGameInfo.gameDescription,
),
],
references: (references ?? []).flatMap((reference, index) =>
createGenerationInputReference(`宣发参考图 ${index + 1}`, reference),
),
};
}
export function buildIconGenerationInputs(
iconDescriptions: string[],
specReference: CharacterReferenceImage,
references?: CharacterReferenceImage[],
): CanvasGenerationInputs {
return {
fields: [
{
title: '素材描述',
value: iconDescriptions.join('\n'),
},
],
references: [
...createGenerationInputReference('图标规范', specReference),
...(references ?? []).flatMap((reference, index) =>
createGenerationInputReference(`参考图 ${index + 1}`, reference),
),
],
};
}
export function buildUiDesignAssetExtractionGenerationInputs(
sourceLayer: CanvasLayer,
references?: CharacterReferenceImage[],
): CanvasGenerationInputs {
return {
fields: [
...createGenerationInputField(
'提取提示词',
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
),
],
references: [
...createLayerGenerationInputReference('UI设计图', sourceLayer),
...(references ?? []).flatMap((reference, index) =>
createGenerationInputReference(`参考图 ${index + 1}`, reference),
),
],
};
}
export function buildEditGenerationInputs(
title: '修改要求' | '快速编辑提示词' | '重绘提示词',
prompt: string,
sourceLayer: CanvasLayer,
): CanvasGenerationInputs {
return {
fields: createGenerationInputField(title, prompt),
references: createLayerGenerationInputReference('参考图', sourceLayer),
};
}
export function buildQuickEditGenerationInputs(
title: '快速编辑提示词' | '重绘提示词',
prompt: string,
sourceLayer: CanvasLayer,
references: CharacterReferenceImage[] | undefined,
): CanvasGenerationInputs {
const limitedReferences = (references ?? []).slice(
0,
QUICK_EDIT_REFERENCE_LIMIT,
);
return {
fields: createGenerationInputField(title, prompt),
references: [
...createLayerGenerationInputReference('原图', sourceLayer),
...limitedReferences.flatMap((reference, index) =>
createGenerationInputReference(`参考图 ${index + 1}`, reference),
),
],
};
}
export function isCanvasGenerationDialog(
dialog: GenerateDialogState | null,
): dialog is CanvasGenerationDialogState {
return Boolean(
dialog?.id &&
(dialog.mode === 'generate' ||
dialog.mode === 'spec' ||
dialog.mode === 'character' ||
dialog.mode === 'icon' ||
dialog.mode === 'publication' ||
dialog.mode === 'ui-design' ||
dialog.mode === 'quick-edit' ||
dialog.mode === 'character-animation' ||
dialog.mode === 'video' ||
dialog.mode === 'audio-sound-effect' ||
dialog.mode === 'audio-background-music'),
);
}
export function getGenerationFrameAriaLabel(
dialog: CanvasGenerationDialogState,
) {
if (dialog.mode === 'character') {
return '角色生成占位图';
}
if (dialog.mode === 'spec') {
return '规范生成占位图';
}
if (dialog.mode === 'icon') {
return '图标素材生成占位图';
}
if (dialog.mode === 'publication') {
return '宣发素材生成占位图';
}
if (dialog.mode === 'ui-design') {
return 'UI设计图生成占位图';
}
if (dialog.mode === 'quick-edit') {
return '快速编辑生成占位图';
}
if (dialog.mode === 'video') {
return '视频生成占位图';
}
if (dialog.mode === 'character-animation') {
return '角色动作生成占位图';
}
if (dialog.mode === 'audio-sound-effect') {
return '音效生成占位图';
}
if (dialog.mode === 'audio-background-music') {
return '背景音乐生成占位图';
}
return '图像生成占位图';
}
export function getGenerationFrameLabel(dialog: CanvasGenerationDialogState) {
if (dialog.mode === 'character') {
return 'Character Generator';
}
if (dialog.mode === 'spec') {
return 'Spec Generator';
}
if (dialog.mode === 'icon') {
return 'Icon Generator';
}
if (dialog.mode === 'publication') {
return `${
getPublicationMaterialsWorkflow(
dialog.publicationWorkflowId ?? 'publication-cover-image',
).englishName
} Generator`;
}
if (dialog.mode === 'ui-design') {
return 'UI Design Generator';
}
if (dialog.mode === 'quick-edit') {
return 'Quick Edit Generator';
}
if (dialog.mode === 'video') {
return 'Video Generator';
}
if (dialog.mode === 'character-animation') {
return 'Action Generator';
}
if (dialog.mode === 'audio-sound-effect') {
return 'Sound Generator';
}
if (dialog.mode === 'audio-background-music') {
return 'Music Generator';
}
return 'Image Generator';
}
export function resolveImageGenerationErrorMessage(
error: unknown,
fallbackMessage = '生成图片失败',
) {
if (
error instanceof ApiClientError &&
(error.status === 401 || error.status === 403)
) {
return '请先登录后再生成图片';
}
if (error instanceof ApiClientError && error.status === 409) {
const message = error.message.trim();
if (message && !message.startsWith('请求冲突')) {
return message;
}
const provider =
typeof error.details?.provider === 'string'
? error.details.provider.trim()
: '';
if (provider === 'profile-wallet') {
return '泥点余额不足,请充值后再生成';
}
return '生成任务未完成,请稍后重试';
}
return error instanceof Error && error.message.trim()
? error.message
: fallbackMessage;
}