Simplify custom world result editing controls

This commit is contained in:
2026-04-08 19:07:46 +08:00
parent bd9fdcbe31
commit a02f7b6414
125 changed files with 8804 additions and 1462 deletions

View File

@@ -27,7 +27,7 @@ const CHARACTER_VISUAL_PUBLISH_PATH = '/api/character-visual/publish';
const CHARACTER_ANIMATION_PUBLISH_PATH = '/api/animation/publish';
const CUSTOM_WORLD_SCENE_IMAGE_PATH = '/api/custom-world/scene-image';
const DEFAULT_DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/api/v1';
const DEFAULT_DASHSCOPE_SCENE_IMAGE_MODEL = 'wan2.2-t2i-flash';
const DEFAULT_DASHSCOPE_SCENE_IMAGE_MODEL = 'wan2.7-image';
const DASHSCOPE_TASK_POLL_INTERVAL_MS = 2000;
const DASHSCOPE_TASK_TIMEOUT_MS = 150000;
@@ -541,6 +541,43 @@ async function resolveAssetSourcePayload(
};
}
async function resolveAssetSourceAsDataUrl(
rootDir: string,
source: string,
fallbackMessage: string,
) {
if (/^data:image\/[^;]+;base64,/u.test(source)) {
return source;
}
const payload = await resolveAssetSourcePayload(
rootDir,
source,
fallbackMessage,
);
const mimeType = (() => {
switch (payload.extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'webp':
return 'image/webp';
default:
return 'image/png';
}
})();
return `data:${mimeType};base64,${payload.buffer.toString('base64')}`;
}
function resolveDashScopeSceneImageModel(model: string) {
if (/^wan2\.7-image(?:-pro)?$/u.test(model)) {
return model;
}
return DEFAULT_DASHSCOPE_SCENE_IMAGE_MODEL;
}
function resolveImageExtension(
contentTypeHeader: string | string[] | undefined,
sourceUrl: string,
@@ -657,6 +694,44 @@ function getDashScopeImageUrl(taskResponse: Record<string, unknown>) {
}
}
const choices = output && Array.isArray(output.choices) ? output.choices : [];
for (const choice of choices) {
if (!isRecordValue(choice)) {
continue;
}
const message = isRecordValue(choice.message) ? choice.message : null;
const content =
message && Array.isArray(message.content) ? message.content : [];
for (const entry of content) {
if (!isRecordValue(entry)) {
continue;
}
const imageUrl =
typeof entry.image === 'string' && entry.image.trim()
? entry.image.trim()
: typeof entry.url === 'string' && entry.url.trim()
? entry.url.trim()
: '';
if (imageUrl) {
return {
url: imageUrl,
actualPrompt:
typeof entry.actual_prompt === 'string' &&
entry.actual_prompt.trim()
? entry.actual_prompt.trim()
: typeof entry.revised_prompt === 'string' &&
entry.revised_prompt.trim()
? entry.revised_prompt.trim()
: undefined,
};
}
}
}
throw new Error('场景图片生成成功,但没有返回可下载的图片地址。');
}
@@ -886,10 +961,11 @@ function createCustomWorldSceneImagePlugin(
typeof body.size === 'string' && body.size.trim()
? body.size.trim()
: '1280*720';
const model =
const requestedModel =
typeof body.model === 'string' && body.model.trim()
? body.model.trim()
: defaultModel;
const model = resolveDashScopeSceneImageModel(requestedModel);
const worldName =
typeof body.worldName === 'string' ? body.worldName.trim() : '';
const profileId =
@@ -898,6 +974,10 @@ function createCustomWorldSceneImagePlugin(
typeof body.landmarkName === 'string' ? body.landmarkName.trim() : '';
const landmarkId =
typeof body.landmarkId === 'string' ? body.landmarkId.trim() : '';
const referenceImageSrc =
typeof body.referenceImageSrc === 'string'
? body.referenceImageSrc.trim()
: '';
if (!prompt) {
sendJson(res, 400, { error: { message: 'prompt is required.' } });
@@ -912,20 +992,37 @@ function createCustomWorldSceneImagePlugin(
}
try {
const messageContent: Array<{ image: string } | { text: string }> = [];
if (referenceImageSrc) {
messageContent.push({
image: await resolveAssetSourceAsDataUrl(
rootDir,
referenceImageSrc,
'参考图必须来自 public 目录或使用 Data URL。',
),
});
}
messageContent.push({ text: prompt });
const createTaskResponse = await proxyJsonRequest(
`${baseUrl}/services/aigc/text2image/image-synthesis`,
`${baseUrl}/services/aigc/image-generation/generation`,
apiKey,
{
model,
input: {
prompt,
...(negativePrompt ? { negative_prompt: negativePrompt } : {}),
messages: [
{
role: 'user',
content: messageContent,
},
],
},
parameters: {
n: 1,
size,
prompt_extend: true,
watermark: false,
...(negativePrompt ? { negative_prompt: negativePrompt } : {}),
},
},
{
@@ -1023,6 +1120,7 @@ function createCustomWorldSceneImagePlugin(
size,
prompt,
negativePrompt,
referenceImageSrc: referenceImageSrc || undefined,
actualPrompt: imageResult.actualPrompt,
remoteUrl: imageResult.url,
imageSrc,
@@ -1189,6 +1287,7 @@ function createCharacterVisualPublishPlugin(rootDir: string): Plugin {
typeof body.height === 'number' && Number.isFinite(body.height)
? body.height
: 1536;
const updateCharacterOverride = body.updateCharacterOverride !== false;
if (!characterId) {
sendJson(res, 400, { error: { message: 'characterId is required.' } });
@@ -1259,26 +1358,30 @@ function createCharacterVisualPublishPlugin(rootDir: string): Plugin {
'utf8',
);
const overrideMap = await readJsonObjectFile(characterOverridesFilePath);
const existingOverride = overrideMap[characterId];
const nextOverride =
existingOverride &&
typeof existingOverride === 'object' &&
!Array.isArray(existingOverride)
? { ...(existingOverride as Record<string, unknown>) }
: {};
nextOverride.generatedVisualAssetId = assetId;
nextOverride.portrait = masterImagePath;
overrideMap[characterId] = nextOverride;
await writeJsonObjectFile(characterOverridesFilePath, overrideMap);
let overrideMap: Record<string, unknown> = {};
if (updateCharacterOverride) {
overrideMap = await readJsonObjectFile(characterOverridesFilePath);
const existingOverride = overrideMap[characterId];
const nextOverride =
existingOverride &&
typeof existingOverride === 'object' &&
!Array.isArray(existingOverride)
? { ...(existingOverride as Record<string, unknown>) }
: {};
nextOverride.generatedVisualAssetId = assetId;
nextOverride.portrait = masterImagePath;
overrideMap[characterId] = nextOverride;
await writeJsonObjectFile(characterOverridesFilePath, overrideMap);
}
sendJson(res, 200, {
ok: true,
assetId,
portraitPath: masterImagePath,
overrideMap,
saveMessage:
'主形象已发布到 public/generated-characters并更新角色覆盖。',
saveMessage: updateCharacterOverride
? '主形象已发布到 public/generated-characters并更新角色覆盖。'
: '主形象已保存到 public/generated-characters可直接写回当前自定义世界角色。',
});
} catch (error) {
sendJson(res, 500, {
@@ -1333,6 +1436,7 @@ function createCharacterAnimationPublishPlugin(rootDir: string): Plugin {
!Array.isArray(body.animations)
? (body.animations as Record<string, unknown>)
: null;
const updateCharacterOverride = body.updateCharacterOverride !== false;
if (!characterId) {
sendJson(res, 400, { error: { message: 'characterId is required.' } });
@@ -1476,35 +1580,40 @@ function createCharacterAnimationPublishPlugin(rootDir: string): Plugin {
'utf8',
);
const overrideMap = await readJsonObjectFile(characterOverridesFilePath);
const existingOverride = overrideMap[characterId];
const nextOverride =
existingOverride &&
typeof existingOverride === 'object' &&
!Array.isArray(existingOverride)
? { ...(existingOverride as Record<string, unknown>) }
: {};
const existingAnimationMap =
nextOverride.animationMap &&
typeof nextOverride.animationMap === 'object' &&
!Array.isArray(nextOverride.animationMap)
? (nextOverride.animationMap as Record<string, unknown>)
: {};
nextOverride.generatedAnimationSetId = animationSetId;
nextOverride.generatedVisualAssetId = visualAssetId;
nextOverride.animationMap = {
...existingAnimationMap,
...nextAnimationMap,
};
overrideMap[characterId] = nextOverride;
await writeJsonObjectFile(characterOverridesFilePath, overrideMap);
let overrideMap: Record<string, unknown> = {};
if (updateCharacterOverride) {
overrideMap = await readJsonObjectFile(characterOverridesFilePath);
const existingOverride = overrideMap[characterId];
const nextOverride =
existingOverride &&
typeof existingOverride === 'object' &&
!Array.isArray(existingOverride)
? { ...(existingOverride as Record<string, unknown>) }
: {};
const existingAnimationMap =
nextOverride.animationMap &&
typeof nextOverride.animationMap === 'object' &&
!Array.isArray(nextOverride.animationMap)
? (nextOverride.animationMap as Record<string, unknown>)
: {};
nextOverride.generatedAnimationSetId = animationSetId;
nextOverride.generatedVisualAssetId = visualAssetId;
nextOverride.animationMap = {
...existingAnimationMap,
...nextAnimationMap,
};
overrideMap[characterId] = nextOverride;
await writeJsonObjectFile(characterOverridesFilePath, overrideMap);
}
sendJson(res, 200, {
ok: true,
animationSetId,
overrideMap,
saveMessage:
'基础动作资源已发布到 public/generated-animations并更新角色覆盖。',
animationMap: nextAnimationMap,
saveMessage: updateCharacterOverride
? '基础动作资源已发布到 public/generated-animations并更新角色覆盖。'
: '基础动作资源已保存到 public/generated-animations可直接写回当前自定义世界角色。',
});
} catch (error) {
sendJson(res, 500, {