From 57a439f063b70bf891dc78a837bed2eb42465954 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 26 Dec 2025 17:55:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96Gemini=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ai/providers/gemini-image.ts | 187 +++++++++++++++++++------------ 图片标注功能-部署完成总结.md | 1 + 2 files changed, 118 insertions(+), 70 deletions(-) diff --git a/lib/ai/providers/gemini-image.ts b/lib/ai/providers/gemini-image.ts index 6b01c539a..babb0b006 100644 --- a/lib/ai/providers/gemini-image.ts +++ b/lib/ai/providers/gemini-image.ts @@ -123,65 +123,101 @@ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jp /** * 下载远程URL图片并上传到 S3,返回缩略图和原图的 URL + * 带重试机制,确保成功下载并上传到国内中转 */ -async function downloadAndUploadToS3(imageUrl: string): Promise<{ thumbnail: string; original: string }> { - try { - console.log(`[Gemini Image] Downloading image from URL: ${imageUrl.substring(0, 100)}...`); - - // 下载图片 - const response = await fetch(imageUrl); - if (!response.ok) { - throw new Error(`Failed to download image: ${response.statusText}`); +async function downloadAndUploadToS3(imageUrl: string, retries = 3): Promise<{ thumbnail: string; original: string }> { + for (let attempt = 0; attempt < retries; attempt++) { + try { + console.log(`[Gemini Image] Downloading image from URL (attempt ${attempt + 1}/${retries}): ${imageUrl.substring(0, 100)}...`); + + // 下载图片(增加超时时间到 30 秒) + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); + + const response = await fetch(imageUrl, { + signal: controller.signal, + headers: { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + }, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // 获取图片数据 + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + if (buffer.length === 0) { + throw new Error("下载的图片数据为空"); + } + + // 从响应头或URL获取MIME类型 + const contentType = response.headers.get("content-type") || "image/jpeg"; + const ext = contentType.split("/")[1] || "jpg"; + + // 生成文件名 + const timestamp = Date.now(); + const randomStr = Math.random().toString(36).slice(2, 8); + const thumbnailFilename = `gemini-thumb-${timestamp}-${randomStr}.${ext}`; + const originalFilename = `gemini-orig-${timestamp}-${randomStr}.${ext}`; + + console.log(`[Gemini Image] Downloaded size: ${buffer.length} bytes`); + + // 生成缩略图 + const thumbnailBuffer = await createThumbnail(buffer, contentType); + + // 转换为 ArrayBuffer + const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( + thumbnailBuffer.byteOffset, + thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength + ) as ArrayBuffer; + const originalArrayBuffer = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) as ArrayBuffer; + + // 并行上传缩略图和原图到 S3 + console.log(`[Gemini Image] Uploading downloaded image to S3...`); + const [thumbnailResult, originalResult] = await Promise.all([ + uploadFile(thumbnailFilename, thumbnailArrayBuffer), + uploadFile(originalFilename, originalArrayBuffer), + ]); + + console.log(`[Gemini Image] Uploaded to S3:`); + console.log(` - Thumbnail: ${thumbnailResult.url}`); + console.log(` - Original: ${originalResult.url}`); + + return { + thumbnail: thumbnailResult.url, + original: originalResult.url, + }; + } catch (error) { + const isLastAttempt = attempt === retries - 1; + console.error( + `[Gemini Image] Download attempt ${attempt + 1}/${retries} failed:`, + error instanceof Error ? error.message : String(error) + ); + + if (isLastAttempt) { + // 最后一次尝试失败,抛出错误(不要 fallback 到原始 URL) + throw new Error( + `图片下载并上传失败(已重试 ${retries} 次): ${error instanceof Error ? error.message : "未知错误"}` + ); + } + + // 等待后重试(指数退避) + const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s... + console.log(`[Gemini Image] Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); } - - // 获取图片数据 - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // 从响应头或URL获取MIME类型 - const contentType = response.headers.get("content-type") || "image/jpeg"; - const ext = contentType.split("/")[1] || "jpg"; - - // 生成文件名 - const timestamp = Date.now(); - const randomStr = Math.random().toString(36).slice(2, 8); - const thumbnailFilename = `openai-thumb-${timestamp}-${randomStr}.${ext}`; - const originalFilename = `openai-orig-${timestamp}-${randomStr}.${ext}`; - - console.log(`[Gemini Image] Downloaded size: ${buffer.length} bytes`); - - // 生成缩略图 - const thumbnailBuffer = await createThumbnail(buffer, contentType); - - // 转换为 ArrayBuffer - const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( - thumbnailBuffer.byteOffset, - thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength - ) as ArrayBuffer; - const originalArrayBuffer = buffer.buffer.slice( - buffer.byteOffset, - buffer.byteOffset + buffer.byteLength - ) as ArrayBuffer; - - // 并行上传缩略图和原图到 S3 - console.log(`[Gemini Image] Uploading downloaded image to S3...`); - const [thumbnailResult, originalResult] = await Promise.all([ - uploadFile(thumbnailFilename, thumbnailArrayBuffer), - uploadFile(originalFilename, originalArrayBuffer), - ]); - - console.log(`[Gemini Image] Uploaded to S3:`); - console.log(` - Thumbnail: ${thumbnailResult.url}`); - console.log(` - Original: ${originalResult.url}`); - - return { - thumbnail: thumbnailResult.url, - original: originalResult.url, - }; - } catch (error) { - console.error("[Gemini Image] Failed to download and upload image:", error); - throw new Error(`图片下载并上传失败: ${error instanceof Error ? error.message : "未知错误"}`); } + + // 理论上不会到达这里,但 TypeScript 需要 + throw new Error("下载失败:未知错误"); } // Gemini API 响应格式(参考灵魂助手3.0的格式) @@ -253,14 +289,17 @@ async function extractAndUploadImages(response: any): Promise { images.push(urls); }); uploadPromises.push(uploadPromise); } else { - // 非 base64 图片(OpenAI URL 等),下载并上传到 S3 + // 非 base64 图片(OpenAI URL 等),必须下载并上传到 S3(国内中转) console.log("[Gemini Image] Detected external URL, will download and upload to S3:", img.url.substring(0, 100) + "..."); const uploadPromise = downloadAndUploadToS3(img.url) - .then(urls => { images.push(urls); }) + .then(urls => { + console.log("[Gemini Image] Successfully downloaded and uploaded external image to S3"); + images.push(urls); + }) .catch(error => { - console.error("[Gemini Image] Failed to download/upload URL, falling back to original:", error); - // 如果下载失败,使用原始URL(可能无法访问,但至少不会崩溃) - images.push({ thumbnail: img.url, original: img.url }); + console.error("[Gemini Image] Failed to download/upload URL after retries:", error); + // 不再 fallback 到原始 URL,而是抛出错误 + throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`); }); uploadPromises.push(uploadPromise); } @@ -274,13 +313,17 @@ async function extractAndUploadImages(response: any): Promise { images.push(urls); }) + .then(urls => { + console.log("[Gemini Image] Successfully downloaded and uploaded Nano Banana image to S3"); + images.push(urls); + }) .catch(error => { - console.error("[Gemini Image] Failed to download/upload Nano Banana URL:", error); - // 如果下载失败,使用原始URL - images.push({ thumbnail: result.url, original: result.url }); + console.error("[Gemini Image] Failed to download/upload Nano Banana URL after retries:", error); + // 不再 fallback 到原始 URL,而是抛出错误 + throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`); }); uploadPromises.push(uploadPromise); } @@ -300,13 +343,17 @@ async function extractAndUploadImages(response: any): Promise { images.push(urls); }); uploadPromises.push(uploadPromise); } else { - // 非 base64 图片(OpenAI URL 等),下载并上传到 S3 + // 非 base64 图片(OpenAI URL 等),必须下载并上传到 S3(国内中转) console.log("[Gemini Image] Detected external URL in Gemini response, will download and upload to S3:", img.url.substring(0, 100) + "..."); const uploadPromise = downloadAndUploadToS3(img.url) - .then(urls => { images.push(urls); }) + .then(urls => { + console.log("[Gemini Image] Successfully downloaded and uploaded Gemini external image to S3"); + images.push(urls); + }) .catch(error => { - console.error("[Gemini Image] Failed to download/upload URL in Gemini response, falling back to original:", error); - images.push({ thumbnail: img.url, original: img.url }); + console.error("[Gemini Image] Failed to download/upload URL in Gemini response after retries:", error); + // 不再 fallback 到原始 URL,而是抛出错误 + throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`); }); uploadPromises.push(uploadPromise); } diff --git a/图片标注功能-部署完成总结.md b/图片标注功能-部署完成总结.md index f0cf30a19..3b46f8314 100644 --- a/图片标注功能-部署完成总结.md +++ b/图片标注功能-部署完成总结.md @@ -526,3 +526,4 @@ curl http://localhost:8002/health +