cangku/backend/src/services/selectionImageGenerationService.js

202 lines
7.1 KiB
JavaScript

'use strict';
const axios = require('axios');
const { uploadBufferToDefaultS3 } = require('./objectStorage');
const { getSelectionFissionImageGenerationConfig } = require('./selectionFissionAiSettings');
const QWEN_GENERATION_PATH = '/api/v1/services/aigc/multimodal-generation/generation';
function normalizeText(value, maxLength = 1000) {
return String(value ?? '').trim().slice(0, maxLength);
}
function stripTrailingSlash(value) {
return normalizeText(value, 1000).replace(/\/+$/, '');
}
function getProviderConfig(config) {
const provider = config?.provider === 'qwen' ? 'qwen' : 'external';
return { provider, ...(config?.[provider] || {}), timeoutMs: config?.timeoutMs };
}
function resolveQwenImageDimensions() {
return '800*800';
}
function getRequestErrorMessage(error, fallback = '生图请求失败') {
return normalizeText(
error?.response?.data?.error?.message
|| error?.response?.data?.message
|| error?.response?.data?.error
|| error?.message
|| fallback,
1000
) || fallback;
}
async function persistQwenImage(imageUrl, timeoutMs) {
const response = await axios.get(imageUrl, {
responseType: 'arraybuffer',
timeout: Math.min(Number(timeoutMs) || 120000, 120000),
maxContentLength: 20 * 1024 * 1024,
});
const mimeType = normalizeText(response.headers?.['content-type'], 120).split(';')[0] || 'image/png';
const extension = mimeType.includes('jpeg') ? 'jpg' : (mimeType.split('/')[1] || 'png');
const uploaded = await uploadBufferToDefaultS3(
Buffer.from(response.data),
mimeType,
'selection-fission/generated-images',
extension
);
return uploaded.url;
}
async function requestExternalImage(providerConfig, input) {
const response = await axios.post(`${stripTrailingSlash(providerConfig.baseUrl)}/api/external/image/generate`, {
prompt: input.prompt,
negativePrompt: input.negativePrompt,
imageCount: 1,
aspectRatio: '1:1',
imageSize: '1K',
width: 800,
height: 800,
referenceImages: input.referenceImages,
metadata: input.metadata,
}, {
timeout: providerConfig.timeoutMs,
headers: {
Authorization: `Bearer ${providerConfig.apiKey}`,
'x-api-key': providerConfig.apiKey,
'Content-Type': 'application/json',
},
maxBodyLength: 15 * 1024 * 1024,
});
const responseImages = response.data?.data?.images || response.data?.images || [];
const first = Array.isArray(responseImages) ? responseImages[0] : null;
return {
imageUrl: normalizeText(first?.originalUrl || first?.url || first?.thumbnailUrl, 4000),
requestId: normalizeText(response.data?.data?.requestId || response.data?.requestId, 160),
provider: 'external',
model: providerConfig.model || 'provider-default',
raw: response.data,
};
}
async function requestQwenImage(providerConfig, input, dependencies = {}) {
const post = dependencies.post || axios.post;
const persistImage = dependencies.persistImage || persistQwenImage;
const content = input.referenceImages.slice(0, 3).map((imageUrl) => ({ image: imageUrl }));
content.push({ text: input.prompt });
const response = await post(
`${stripTrailingSlash(providerConfig.baseUrl)}${QWEN_GENERATION_PATH}`,
{
model: providerConfig.model,
input: { messages: [{ role: 'user', content }] },
parameters: {
n: 1,
negative_prompt: normalizeText(input.negativePrompt, 500),
prompt_extend: input.metadata?.disablePromptExtend === true
? false
: providerConfig.promptExtend !== false,
watermark: Boolean(providerConfig.watermark),
size: resolveQwenImageDimensions(input.aspectRatio, providerConfig.imageSize),
},
},
{
timeout: providerConfig.timeoutMs,
headers: {
Authorization: `Bearer ${providerConfig.apiKey}`,
'Content-Type': 'application/json',
},
maxBodyLength: 30 * 1024 * 1024,
}
);
const contentRows = response.data?.output?.choices?.[0]?.message?.content || [];
const temporaryImageUrl = Array.isArray(contentRows)
? normalizeText(contentRows.find((row) => row?.image)?.image, 4000)
: '';
if (!temporaryImageUrl) {
const error = new Error(response.data?.message || '千问生图接口未返回图片');
error.code = response.data?.code || 'QWEN_IMAGE_EMPTY';
throw error;
}
const imageUrl = await persistImage(temporaryImageUrl, providerConfig.timeoutMs);
return {
imageUrl,
requestId: normalizeText(response.data?.request_id, 160),
provider: 'qwen',
model: providerConfig.model,
usage: response.data?.usage || null,
raw: response.data,
};
}
async function generateSelectionImage(input = {}) {
const config = await getSelectionFissionImageGenerationConfig();
const providerConfig = { ...getProviderConfig(config), imageSize: '1K' };
if (!providerConfig.baseUrl || !providerConfig.apiKey) {
const error = new Error(
providerConfig.provider === 'qwen'
? '千问生图服务尚未配置 API Key'
: '现有生图服务尚未配置 API Key'
);
error.status = 409;
error.code = 'SELECTION_IMAGE_API_KEY_MISSING';
throw error;
}
try {
return providerConfig.provider === 'qwen'
? await requestQwenImage(providerConfig, input)
: await requestExternalImage(providerConfig, input);
} catch (error) {
if (error.code === 'SELECTION_IMAGE_API_KEY_MISSING') throw error;
const wrapped = new Error(
`${providerConfig.provider === 'qwen' ? '千问官方' : '现有第三方'}生图失败:${getRequestErrorMessage(error)}`
);
wrapped.status = error.response?.status || error.status || 502;
wrapped.code = error.response?.data?.code || error.code || 'SELECTION_IMAGE_REQUEST_FAILED';
throw wrapped;
}
}
async function testSelectionImageGenerationConfig() {
const config = await getSelectionFissionImageGenerationConfig();
const providerConfig = getProviderConfig(config);
if (!providerConfig.apiKey) {
const error = new Error(providerConfig.provider === 'qwen'
? '千问生图服务尚未配置 API Key'
: '现有生图服务尚未配置 API Key');
error.status = 409;
throw error;
}
const input = {
prompt: '生成一张简洁的白色摄影棚背景测试图,不要文字、图标或水印。',
negativePrompt: '文字,水印,标志,复杂物体',
aspectRatio: '1:1',
referenceImages: providerConfig.provider === 'qwen'
? []
: ['https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/jiydyi/image+%2818%29-2026-03-10-16-39-59.webp'],
metadata: { task: 'settings_connection_test' },
};
const result = providerConfig.provider === 'qwen'
? await requestQwenImage({ ...providerConfig, imageSize: '1K' }, input)
: await requestExternalImage({ ...providerConfig, imageSize: '1K' }, input);
return {
provider: result.provider,
model: result.model,
imageUrl: result.imageUrl,
requestId: result.requestId,
message: '生图服务连接成功',
};
}
module.exports = {
QWEN_GENERATION_PATH,
generateSelectionImage,
getProviderConfig,
requestExternalImage,
requestQwenImage,
resolveQwenImageDimensions,
testSelectionImageGenerationConfig,
};