This commit is contained in:
2026-04-16 15:45:00 +08:00
parent 6363267bca
commit 91b63675eb
43 changed files with 5652 additions and 853 deletions

View File

@@ -1,4 +1,5 @@
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { z } from 'zod';
@@ -16,8 +17,111 @@ export const sceneImageSchema = z.object({
profileId: z.string().trim().optional().default(''),
landmarkName: z.string().trim().optional().default(''),
landmarkId: z.string().trim().optional().default(''),
referenceImageSrc: z.string().trim().optional().default(''),
});
function parseImageDataUrl(source: string) {
const matched = /^data:(image\/[^;]+);base64,(.+)$/u.exec(source);
if (!matched) {
return null;
}
return {
buffer: Buffer.from(matched[2], 'base64'),
mimeType: matched[1],
};
}
async function resolveReferenceImageAsDataUrl(rootDir: string, source: string) {
const trimmedSource = source.trim();
if (!trimmedSource) {
return '';
}
const parsedDataUrl = parseImageDataUrl(trimmedSource);
if (parsedDataUrl) {
return trimmedSource;
}
if (!trimmedSource.startsWith('/')) {
throw badRequest('参考图必须是 Data URL 或 public 目录下的 URL。');
}
const normalizedSource = path.posix
.normalize(trimmedSource)
.replace(/^\/+/u, '');
const absolutePath = path.resolve(
rootDir,
'public',
...normalizedSource.split('/'),
);
const publicRoot = path.resolve(rootDir, 'public');
if (!absolutePath.startsWith(publicRoot)) {
throw badRequest('参考图路径越界。');
}
const buffer = await readFile(absolutePath);
const extension = path.extname(absolutePath).replace(/^\./u, '').toLowerCase();
const mimeType = (() => {
switch (extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'webp':
return 'image/webp';
default:
return 'image/png';
}
})();
return `data:${mimeType};base64,${buffer.toString('base64')}`;
}
function collectStringsByKey(
value: unknown,
targetKey: string,
results: string[],
) {
if (typeof value === 'string') {
return;
}
if (Array.isArray(value)) {
value.forEach((entry) => collectStringsByKey(entry, targetKey, results));
return;
}
if (!value || typeof value !== 'object') {
return;
}
Object.entries(value).forEach(([key, nestedValue]) => {
if (key === targetKey && typeof nestedValue === 'string' && nestedValue.trim()) {
results.push(nestedValue.trim());
return;
}
collectStringsByKey(nestedValue, targetKey, results);
});
}
function findFirstStringByKey(value: unknown, targetKey: string) {
const results: string[] = [];
collectStringsByKey(value, targetKey, results);
return results[0] ?? '';
}
function extractTaskId(payload: Record<string, unknown>) {
return findFirstStringByKey(payload, 'task_id');
}
function extractImageUrls(payload: Record<string, unknown>) {
const urls: string[] = [];
collectStringsByKey(payload, 'image', urls);
collectStringsByKey(payload, 'url', urls);
return [...new Set(urls)];
}
function ensurePayload(
payload: z.infer<typeof sceneImageSchema>,
defaultModel: string,
@@ -38,8 +142,14 @@ export async function generateSceneImage(
) {
const payload = ensurePayload(input, context.config.dashScope.imageModel);
const baseUrl = context.config.dashScope.baseUrl.replace(/\/+$/u, '');
const referenceImage = payload.referenceImageSrc
? await resolveReferenceImageAsDataUrl(
context.config.projectRoot,
payload.referenceImageSrc,
)
: '';
const createResponse = await fetch(
`${baseUrl}/services/aigc/text2image/image-synthesis`,
`${baseUrl}/services/aigc/image-generation/generation`,
{
method: 'POST',
headers: {
@@ -50,16 +160,24 @@ export async function generateSceneImage(
body: JSON.stringify({
model: payload.model,
input: {
prompt: payload.prompt,
...(payload.negativePrompt
? { negative_prompt: payload.negativePrompt }
: {}),
messages: [
{
role: 'user',
content: [
{ text: payload.prompt },
...(referenceImage ? [{ image: referenceImage }] : []),
],
},
],
},
parameters: {
n: 1,
size: payload.size,
prompt_extend: true,
watermark: false,
...(payload.negativePrompt
? { negative_prompt: payload.negativePrompt }
: {}),
},
}),
},
@@ -71,12 +189,8 @@ export async function generateSceneImage(
);
}
const createPayload = JSON.parse(createText) as {
output?: {
task_id?: string;
};
};
const taskId = createPayload.output?.task_id?.trim();
const createPayload = JSON.parse(createText) as Record<string, unknown>;
const taskId = extractTaskId(createPayload);
if (!taskId) {
throw badRequest('场景图片生成任务未返回 task_id');
}
@@ -98,21 +212,11 @@ export async function generateSceneImage(
);
}
const pollPayload = JSON.parse(pollText) as {
output?: {
task_status?: string;
results?: Array<{
url?: string;
actual_prompt?: string;
}>;
};
};
const status = pollPayload.output?.task_status?.trim();
const pollPayload = JSON.parse(pollText) as Record<string, unknown>;
const status = findFirstStringByKey(pollPayload, 'task_status').trim();
if (status === 'SUCCEEDED') {
imageUrl =
pollPayload.output?.results?.find((item) => item.url?.trim())?.url?.trim() || '';
actualPrompt =
pollPayload.output?.results?.find((item) => item.url?.trim())?.actual_prompt?.trim() || '';
imageUrl = extractImageUrls(pollPayload)[0] ?? '';
actualPrompt = findFirstStringByKey(pollPayload, 'actual_prompt').trim();
break;
}
if (status === 'FAILED' || status === 'UNKNOWN') {