diff --git a/app/(chat)/api/chat/_lib/image-media.ts b/app/(chat)/api/chat/_lib/image-media.ts index 7ca6da5c3..91e42e09a 100644 --- a/app/(chat)/api/chat/_lib/image-media.ts +++ b/app/(chat)/api/chat/_lib/image-media.ts @@ -581,45 +581,64 @@ export async function convertMessagesWithImages( if (hasAnnotation) { chatApiLogger.info("[Message Convert] 🎨 检测到标注图片,将自动添加系统提示"); } - // 对于 Gemini,需要特殊处理图片 const convertedMessages = []; - + for (const message of preparedMessages) { const content: any[] = []; - + // 🆕 如果是用户消息且有标注图片,在文本前添加系统提示 let hasAddedAnnotationPrompt = false; - + // 处理消息的各个部分 for (const part of message.parts) { if (part.type === "text" && part.text) { let textContent = part.text; - + // 🆕 如果有标注图片且是用户消息,在用户文本前添加系统提示 if (hasAnnotation && message.role === "user" && !hasAddedAnnotationPrompt) { textContent = ANNOTATION_SYSTEM_PROMPT + "\n\n用户请求:" + textContent; hasAddedAnnotationPrompt = true; chatApiLogger.info("[Message Convert] 🎨 已注入标注提示词"); } - + content.push({ type: "text", text: textContent, }); - } else if (part.type === "file" && part.url) { - // 直接使用 URL 格式(不再转换为 Base64) - // gemini-image 和 huanke-gemini provider 内部会处理图片上传 + continue; + } + + const candidateImageUrl = + part.type === "file" + ? part.url || part.data + : part.type === "image" + ? part.image || part.url + : part.type === "image_url" + ? part.image_url?.url || part.url + : undefined; + + if (typeof candidateImageUrl !== "string" || !candidateImageUrl.trim()) { + continue; + } + + const normalizedImageUrl = candidateImageUrl.trim(); + + // 兼容 file/image/image_url 三类输入;Gemini 内部会将图片交给 S3 后再请求生图网关。 + if (part.type === "file" || part.type === "image" || part.type === "image_url") { content.push({ type: "image_url", image_url: { - url: part.url, + url: normalizedImageUrl, }, }); - chatApiLogger.info("[Message Convert] Image kept as URL format:", part.url.substring(0, 80) + "..."); + chatApiLogger.info( + "[Message Convert] Image kept as URL format:", + normalizedImageUrl.substring(0, 80) + "..." + ); } } - + // 🆕 如果有标注图片但消息中没有文本部分,添加一个纯提示文本 if (hasAnnotation && message.role === "user" && !hasAddedAnnotationPrompt && content.length > 0) { content.unshift({ @@ -628,12 +647,10 @@ export async function convertMessagesWithImages( }); chatApiLogger.info("[Message Convert] 🎨 已在消息开头注入标注提示词"); } - + convertedMessages.push({ role: message.role, - content: content.length === 1 && content[0].type === "text" - ? content[0].text - : content, + content: content.length === 1 && content[0].type === "text" ? content[0].text : content, }); } diff --git a/lib/ai/providers/gemini-image.ts b/lib/ai/providers/gemini-image.ts index d2e1c70f2..2c6c57b38 100644 --- a/lib/ai/providers/gemini-image.ts +++ b/lib/ai/providers/gemini-image.ts @@ -715,6 +715,18 @@ async function sendGeminiRequest( imageSize?: string; } ): Promise { + const normalizeAspectRatio = (aspectRatio: string) => { + const normalized = String(aspectRatio || "").trim(); + if (!normalized || normalized.toLowerCase() === "auto") { + return undefined; + } + if (!/^\d+\s*:\s*\d+$/i.test(normalized)) { + return undefined; + } + const [widthRaw, heightRaw] = normalized.split(":").map((item) => item.trim()); + return `${Number(widthRaw)}:${Number(heightRaw)}`; + }; + const isGptImageModel = /^gpt-image/i.test(modelId); const shouldUseGrsaiAsyncPolling = !isGptImageModel && @@ -739,18 +751,20 @@ async function sendGeminiRequest( messages.forEach((msg) => { if (msg.role === "user" && Array.isArray(msg.content)) { msg.content.forEach((contentPart: any) => { - // 支持标准 image_url - if (contentPart.type === "image_url" && contentPart.image_url?.url) { - urls.push(contentPart.image_url.url); - } - // 🆕 支持 CoreMessage 的 image 类型 (可能来自 Attachment) - else if (contentPart.type === "image" && contentPart.image) { - if (typeof contentPart.image === 'string') { - urls.push(contentPart.image); - } else if (contentPart.image instanceof URL) { - urls.push(contentPart.image.toString()); - } + const candidateImageUrl = + contentPart.type === "image_url" + ? contentPart.image_url?.url + : contentPart.type === "file" + ? contentPart.url || contentPart.data + : contentPart.type === "image" + ? contentPart.image || contentPart.url + : undefined; + + if (!candidateImageUrl || typeof candidateImageUrl !== "string") { + return; } + + urls.push(candidateImageUrl); }); } }); @@ -789,16 +803,39 @@ async function sendGeminiRequest( ? await inferImageAspectRatioFromUrl(processedUrls[0]) : null; const resolvedAspectRatio = inferredAspectRatio || requestedAspectRatio; - requestBody.size = resolveGrsaiGptImageSize(resolvedAspectRatio, imageParams?.imageSize); + const normalizedRequestedAspectRatio = normalizeAspectRatio(requestedAspectRatio); + const resolvedSize = resolveGrsaiGptImageSize(resolvedAspectRatio, imageParams?.imageSize); + + // GPT-image(如 gpt-image-2-vip)在不同网关中参数差异较大: + // - 有的网关只识别 size; + // - 有的网关同时支持 aspectRatio/aspect_ratio; + // - 有的会读取 imageSize/image_size; + // 为避免接口兼容问题,保留核心 size 后补齐兼容字段。 + requestBody.size = resolvedSize; + requestBody.imageSize = imageParams?.imageSize; + requestBody.image_size = imageParams?.imageSize; + if (normalizedRequestedAspectRatio) { + requestBody.aspectRatio = normalizedRequestedAspectRatio; + requestBody.aspect_ratio = normalizedRequestedAspectRatio; + requestBody.aspectRatioRaw = requestedAspectRatio; + } requestBody.shutProgress = false; + + if (processedUrls.length > 0) { + requestBody.images = processedUrls; + if (processedUrls.length === 1) { + requestBody.image = processedUrls[0]; + } + requestBody.imageUrls = processedUrls; + } logger.debug( - `${apiLogPrefix} GPT image payload mapping: requestedAspectRatio=${requestedAspectRatio}, resolvedAspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"}, resolvedSize=${requestBody.size}` + `${apiLogPrefix} GPT image payload mapping: requestedAspectRatio=${requestedAspectRatio}, normalizedRequestedAspectRatio=${normalizedRequestedAspectRatio || "n/a"}, resolvedAspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"}, resolvedSize=${resolvedSize}` ); if (inferredAspectRatio) { logger.debug(`${apiLogPrefix} Inferred reference aspect ratio: ${inferredAspectRatio}`); } logger.debug( - `${apiLogPrefix} Resolved size: aspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"} => ${requestBody.size}` + `${apiLogPrefix} Resolved size: aspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"} => ${resolvedSize}` ); } else { // 添加 Nano Banana 特定参数