From 64502c9d4ba769d14c726333881638017b226e49 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Feb 2026 17:06:18 +0800 Subject: [PATCH] fix: preserve 4k image size across failover pipeline --- app/(chat)/api/chat/route.ts | 6 ++++ lib/ai/providers/volcano-vision.ts | 47 +++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 1ddd8fd7b..ee4257676 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -2828,6 +2828,12 @@ export async function POST(request: Request) { const volcanoResult = await (volcanoLanguageModel as any).doGenerate({ messages: volcanoMessages, maxImages: 1, // 故障转移时只生成 1 张 + imageParams: + effectiveImageParams || { + aspectRatio: defaultImageQualityParams.aspectRatio, + imageSize: defaultImageQualityParams.imageSize, + imageCount: defaultImageQualityParams.imageCount, + }, }); const imageMarkdown = volcanoResult.text || "图片生成失败"; diff --git a/lib/ai/providers/volcano-vision.ts b/lib/ai/providers/volcano-vision.ts index 912a06b4a..57d71f413 100644 --- a/lib/ai/providers/volcano-vision.ts +++ b/lib/ai/providers/volcano-vision.ts @@ -17,6 +17,15 @@ type VolcanoUrlRewriteConfig = { let cachedRewriteConfig: VolcanoUrlRewriteConfig | null | undefined; const VOLCANO_IMAGE_FETCH_TIMEOUT_MS = 20_000; const VOLCANO_INLINE_MAX_SIDE = 1024; +const DEFAULT_VOLCANO_IMAGE_SIZE = "2K"; + +function normalizeVolcanoImageSize(imageSize?: string): "1K" | "2K" | "4K" { + const normalized = String(imageSize || "").trim().toUpperCase(); + if (normalized === "1K" || normalized === "2K" || normalized === "4K") { + return normalized; + } + return DEFAULT_VOLCANO_IMAGE_SIZE; +} function guessImageMimeTypeFromUrl(url: string): string { const cleanUrl = url.split("?")[0]?.toLowerCase() || ""; @@ -321,16 +330,19 @@ async function extractAndUploadImages(response: VolcanoVisionResponse): Promise< * 将图片数据转换为 Markdown 格式 * 使用特殊格式传递缩略图和原图 URL:![thumb|原图URL](缩略图URL) */ -function formatImageResponse(images: Array<{ thumbnail: string; original: string }>): string { +function formatImageResponse( + images: Array<{ thumbnail: string; original: string }>, + imageSize: string = DEFAULT_VOLCANO_IMAGE_SIZE +): string { if (images.length === 0) { return "生成图片失败,未找到图片数据"; } // 🆕 统一使用 ||| 分隔符格式(与 Gemini 保持一致) // 格式: ![thumb|||原图URL|||尺寸](缩略图URL) - // 豆包固定生成 2K 图片 + const normalizedSize = normalizeVolcanoImageSize(imageSize); return images - .map((img) => `![thumb|||${img.original}|||2K](${img.thumbnail})`) + .map((img) => `![thumb|||${img.original}|||${normalizedSize}](${img.thumbnail})`) .join("\n\n"); } @@ -429,7 +441,8 @@ async function sendVolcanoRequest( apiKey: string, messages: any[], modelId: string, - maxImages: number = 3 // 默认 3 张,故障转移时传 1 + maxImages: number = 3, // 默认 3 张,故障转移时传 1 + imageSize: string = DEFAULT_VOLCANO_IMAGE_SIZE ): Promise { const url = `${baseURL}/api/v3/images/generations`; @@ -465,10 +478,11 @@ async function sendVolcanoRequest( console.log("[Volcano Vision] Image count:", image ? (Array.isArray(image) ? image.length : 1) : 0); // 构建请求体 + const normalizedSize = normalizeVolcanoImageSize(imageSize); const requestBody: any = { model: modelId, prompt, - size: "2K", + size: normalizedSize, watermark: false, response_format: "url", stream: false, @@ -579,7 +593,8 @@ async function* sendVolcanoStreamRequest( baseURL: string, apiKey: string, messages: any[], - modelId: string + modelId: string, + imageSize: string = DEFAULT_VOLCANO_IMAGE_SIZE ): AsyncGenerator { const url = `${baseURL}/api/v3/images/generations`; @@ -593,10 +608,11 @@ async function* sendVolcanoStreamRequest( console.log("[Volcano Vision] Image count:", image ? (Array.isArray(image) ? image.length : 1) : 0); // 构建请求体 + const normalizedSize = normalizeVolcanoImageSize(imageSize); const requestBody: any = { model: modelId, prompt, - size: "2K", + size: normalizedSize, watermark: false, response_format: "url", stream: true, @@ -695,6 +711,7 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) { const activeModelId = options.overrideModelId || modelId; console.log("[Volcano Vision] Using modelId:", activeModelId); console.log("[Volcano Vision] Image params from options:", options.imageParams); + const imageSize = normalizeVolcanoImageSize(options.imageParams?.imageSize); // 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张 const imageCount = options.imageParams?.imageCount || options.maxImages || 1; @@ -703,11 +720,18 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) { try { // 发送请求 - const response = await sendVolcanoRequest(baseURL, apiKey, options.messages, activeModelId, maxImages); + const response = await sendVolcanoRequest( + baseURL, + apiKey, + options.messages, + activeModelId, + maxImages, + imageSize + ); // 提取图片并上传到 S3 const imageUrls = await extractAndUploadImages(response); - const markdownText = formatImageResponse(imageUrls); + const markdownText = formatImageResponse(imageUrls, imageSize); console.log( `[Volcano Vision] Generated ${imageUrls.length} images (uploaded to S3)` @@ -748,14 +772,15 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) { const allImages: Array<{ thumbnail: string; original: string }> = []; // 使用流式请求 - for await (const event of sendVolcanoStreamRequest(baseURL, apiKey, options.messages, modelId)) { + const imageSize = normalizeVolcanoImageSize(options.imageParams?.imageSize); + for await (const event of sendVolcanoStreamRequest(baseURL, apiKey, options.messages, modelId, imageSize)) { if (event.type === "image_generation.partial_succeeded" && event.url) { // 每张图片生成完成后立即上传并发送 try { const imageUrls = await uploadImageToS3(event.url); allImages.push(imageUrls); - const markdown = `![thumb|${imageUrls.original}](${imageUrls.thumbnail})`; + const markdown = `![thumb|||${imageUrls.original}|||${imageSize}](${imageUrls.thumbnail})`; console.log(`[Volcano Vision Stream] Image ${event.image_index} uploaded, sending...`); yield { type: "text-delta" as const, textDelta: markdown + "\n\n" };