feat(ai): support Qwen Image 3.0

This commit is contained in:
134 2026-08-26 16:10:00 +08:00 committed by root
parent 5f6341a318
commit 2a4f8f631b
3 changed files with 71 additions and 23 deletions

View File

@ -125,6 +125,11 @@ function normalizeText(value?: string | null) {
return String(value || "").trim().toLowerCase();
}
export function isAlibabaImageTextToImageModelId(modelId?: string | null) {
const normalized = normalizeText(modelId);
return /^qwen-image-3(?:\.0)?(?:$|[-/])/.test(normalized);
}
function toRecord(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
@ -327,7 +332,7 @@ export function modelSupportsTextToImageDescriptor(params: {
}
if (providerType === "alibaba-image") {
return false;
return isAlibabaImageTextToImageModelId(modelId);
}
return true;

View File

@ -10,6 +10,7 @@
import sharp from "sharp";
import { constrainImageDimensions } from "@/lib/ai/image-size-constraints";
import { isAlibabaImageTextToImageModelId } from "@/lib/ai/model-classification";
import { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order";
import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
import { uploadFile } from "@/lib/storage/service";
@ -535,14 +536,20 @@ async function processResponse(
*/
function formatImageResponse(
images: Array<{ thumbnail: string; original: string }>,
size?: string
size?: string,
requestedSizeLabel?: string,
): string {
if (images.length === 0) {
return "图片生成失败,请重试。";
}
// 将分辨率转换为简短格式(如 "1024*1024" -> "1K")
const sizeLabel = size ? getSizeLabel(size) : "2K";
const normalizedRequestedSize = normalizeImageSizeLabel(requestedSizeLabel);
const sizeLabel = requestedSizeLabel
? normalizedRequestedSize
: size
? getSizeLabel(size)
: "2K";
return images
.map((img) => {
@ -663,8 +670,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
preparedImageUrls.push(await prepareAlibabaInputImageUrl(imageUrl, activeModelId));
}
// 验证:qwen-image-edit 系列模型必须有输入图片
if (preparedImageUrls.length === 0) {
// qwen-image-3.0 同时支持文生图与图生图;旧编辑模型仍必须上传参考图。
if (
preparedImageUrls.length === 0 &&
!isAlibabaImageTextToImageModelId(activeModelId)
) {
const modelName = options.modelDisplayName || modelId;
throw new Error(`「${modelName}」是图片编辑模型,需要至少上传一张图片才能进行编辑。如需纯文生图,请使用其他生图模型。`);
}
@ -683,9 +693,10 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
const imageSize = normalizeImageSizeLabel(options.imageParams?.imageSize);
// 根据分辨率等级确定基准像素
const isQwenImage3 = isAlibabaImageTextToImageModelId(activeModelId);
const baseSizeMap: Record<string, number> = {
"1K": 1024,
"2K": 1536,
"2K": isQwenImage3 ? 2048 : 1536,
"4K": 2048,
};
const baseSize = baseSizeMap[imageSize] || 1536;
@ -725,27 +736,34 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
// 阿里文档在 auto 下默认接近 1024*1024,这会让 4K 选择失效。
// 这里改为根据参考图比例计算 size,确保 1K/2K/4K 档位真正生效。
const referenceImageUrl = preparedImageUrls[0];
const inferredDimensions = await inferImageDimensionsFromImageUrl(referenceImageUrl);
if (inferredDimensions) {
params.size = calculateAlibabaSizeFromDimensions(
inferredDimensions.width,
inferredDimensions.height,
baseSize,
);
if (!referenceImageUrl) {
params.size = `${baseSize}*${baseSize}`;
console.log(
`[Alibaba Image] Auto mode resolved size: imageSize=${imageSize}, reference=${inferredDimensions.width}x${inferredDimensions.height}, inferredAspect=${inferredDimensions.aspectRatio} => size=${params.size}`
`[Alibaba Image] Text-to-image auto mode resolved size: imageSize=${imageSize} => size=${params.size}`
);
} else {
const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl);
if (inferredAspectRatio) {
params.size = calculateAlibabaSizeByAspect(inferredAspectRatio, baseSize);
const inferredDimensions = await inferImageDimensionsFromImageUrl(referenceImageUrl);
if (inferredDimensions) {
params.size = calculateAlibabaSizeFromDimensions(
inferredDimensions.width,
inferredDimensions.height,
baseSize,
);
console.log(
`[Alibaba Image] Auto mode resolved size via aspect fallback: imageSize=${imageSize}, inferredAspect=${inferredAspectRatio} => size=${params.size}`
`[Alibaba Image] Auto mode resolved size: imageSize=${imageSize}, reference=${inferredDimensions.width}x${inferredDimensions.height}, inferredAspect=${inferredDimensions.aspectRatio} => size=${params.size}`
);
} else {
console.log(
`[Alibaba Image] Auto mode keeps provider default size: imageSize=${imageSize}, inferredAspect=unavailable`
);
const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl);
if (inferredAspectRatio) {
params.size = calculateAlibabaSizeByAspect(inferredAspectRatio, baseSize);
console.log(
`[Alibaba Image] Auto mode resolved size via aspect fallback: imageSize=${imageSize}, inferredAspect=${inferredAspectRatio} => size=${params.size}`
);
} else {
console.log(
`[Alibaba Image] Auto mode keeps provider default size: imageSize=${imageSize}, inferredAspect=unavailable`
);
}
}
}
} else {
@ -763,7 +781,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
}
// 根据图片数量确定编辑模式
const editMode = preparedImageUrls.length === 1 ? "单图编辑" : `多图融合 (${preparedImageUrls.length}张)`;
const editMode = preparedImageUrls.length === 0
? "文生图"
: preparedImageUrls.length === 1
? "单图编辑"
: `多图融合 (${preparedImageUrls.length}张)`;
const promptForRequest = buildReferenceImageOrderPrompt(
prompt,
preparedImageUrls
@ -785,7 +807,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
// 处理响应
const images = await processResponse(response);
const markdownText = formatImageResponse(images, params.size);
const markdownText = formatImageResponse(images, params.size, imageSize);
console.log(`[Alibaba Image] Generated ${images.length} images successfully`);

View File

@ -13,6 +13,8 @@ import {
is3DModelDescriptor,
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
isAlibabaImageTextToImageModelId,
modelSupportsTextToImageDescriptor,
} from "@/lib/ai/model-classification";
import {
buildVip98kModelPresets,
@ -29,6 +31,25 @@ const atlasHunyuan3D = {
};
test.describe("AI model media classification", () => {
test("classifies Qwen Image 3.0 as Alibaba text-to-image and image editing", () => {
expect(isAlibabaImageTextToImageModelId("qwen-image-3.0")).toBe(true);
expect(isAlibabaImageTextToImageModelId("qwen-image-edit-plus")).toBe(false);
expect(
modelSupportsTextToImageDescriptor({
providerType: "alibaba-image",
modelId: "qwen-image-3.0",
supportsImageGen: true,
})
).toBe(true);
expect(
modelSupportsTextToImageDescriptor({
providerType: "alibaba-image",
modelId: "qwen-image-edit-plus",
supportsImageGen: true,
})
).toBe(false);
});
test("preserves administrator-defined AtlasCloud display names", () => {
expect(
resolveAtlasCloudDisplayName(