forked from lunanting/cangku
202 lines
7.1 KiB
JavaScript
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,
|
|
};
|