优化Gemini图片生成功能
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2025-12-26 17:55:22 +08:00
parent 08a1bd8007
commit 57a439f063
2 changed files with 118 additions and 70 deletions

View File

@ -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<Array<{ thumbnail:
.then(urls => { 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<Array<{ thumbnail:
for (const result of response.results) {
if (result.url && typeof result.url === "string") {
console.log("[Gemini Image] Found Nano Banana image URL:", result.url.substring(0, 100) + "...");
// Nano Banana 返回的是外部URL,需要下载并上传到 S3
// Nano Banana 返回的是外部URL,必须下载并上传到 S3(国内中转)
// 如果下载失败,抛出错误,不要使用原始URL(官方地址可能无法访问)
const uploadPromise = downloadAndUploadToS3(result.url)
.then(urls => { 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<Array<{ thumbnail:
.then(urls => { 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);
}

View File

@ -526,3 +526,4 @@ curl http://localhost:8002/health