lovart3/lib/ai/providers/gemini-image.ts
2025-12-26 17:55:22 +08:00

757 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { uploadFile } from "@/lib/storage/service";
interface GeminiImageConfig {
apiKey: string;
baseURL: string;
}
/**
* 压缩图片生成缩略图(使用 sharp)
*/
async function createThumbnail(buffer: Buffer, mimeType: string): Promise<Buffer> {
try {
const sharp = await import("sharp");
// 创建缩略图(宽度 500px,高度按比例,质量 80%)
const thumbnailBuffer = await sharp.default(buffer)
.resize(500, null, {
fit: "inside",
withoutEnlargement: true,
})
.jpeg({ quality: 80 })
.toBuffer();
console.log(`[Gemini Image] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
return thumbnailBuffer;
} catch (error) {
console.error("[Gemini Image] Failed to create thumbnail:", error);
// 如果失败,返回原图
return buffer;
}
}
/**
* 确保图片是公网可访问的 URL(如果是 Base64,上传到 S3)
*/
async function ensureImageUrl(url: string): Promise<string> {
if (url.startsWith("http")) return url;
if (url.startsWith("data:")) {
try {
console.log("[Gemini Image] Converting Base64 reference image to S3 URL...");
// 解析 Base64
const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
if (!matches) {
console.warn("[Gemini Image] Invalid Base64 format, skipping upload");
return url;
}
const mimeType = matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, "base64");
const ext = mimeType.split("/")[1] || "png";
const filename = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
// 上传到 S3(需要转换为 ArrayBuffer)
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
const result = await uploadFile(filename, arrayBuffer);
console.log(`[Gemini Image] Uploaded reference image to: ${result.url}`);
return result.url;
} catch (error) {
console.error("[Gemini Image] Failed to upload reference image to S3:", error);
// 如果上传失败,返回原 Base64(虽然可能也会失败,但没别的办法)
return url;
}
}
return url;
}
/**
* 将 base64 图片上传到 S3,返回缩略图和原图的 URL
*/
async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jpeg"): Promise<{ thumbnail: string; original: string }> {
try {
// 解析 base64 数据
const buffer = Buffer.from(base64Data, "base64");
// 生成文件名
const ext = mimeType.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] Original size: ${buffer.length} bytes`);
// 生成缩略图
const thumbnailBuffer = await createThumbnail(buffer, mimeType);
// 转换为 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 thumbnail and original 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 upload to S3:", error);
// 如果上传失败,抛出错误,不要返回 Base64,否则会导致前端崩溃和数据库保存失败
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
}
}
/**
* 下载远程URL图片并上传到 S3,返回缩略图和原图的 URL
* 带重试机制,确保成功下载并上传到国内中转
*/
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));
}
}
// 理论上不会到达这里,但 TypeScript 需要
throw new Error("下载失败:未知错误");
}
// Gemini API 响应格式(参考灵魂助手3.0的格式)
interface GeminiResponse {
candidates: Array<{
content: {
parts: Array<{
text?: string;
inline_data?: {
mime_type: string;
data: string;
};
inlineData?: {
mimeType: string;
data: string;
};
}>;
};
}>;
}
/**
* 从文本中提取图片数据
* 返回 { url, isBase64, mimeType, base64Data } 数组
*/
function extractImageDataFromText(text: string): Array<{ url: string; isBase64: boolean; mimeType?: string; base64Data?: string }> {
const images: Array<{ url: string; isBase64: boolean; mimeType?: string; base64Data?: string }> = [];
// 匹配所有 Markdown 图片格式: ![任意文本](URL)
const allImageMatches = text.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g);
for (const match of allImageMatches) {
const url = match[1];
// 检查是否是 base64 格式
const base64Match = url.match(/^data:image\/([^;]+);base64,(.+)$/);
if (base64Match) {
images.push({
url,
isBase64: true,
mimeType: `image/${base64Match[1]}`,
base64Data: base64Match[2],
});
} else {
images.push({ url, isBase64: false });
}
}
return images;
}
/**
* 从 API 响应中提取图片并上传 base64 到 S3
* 返回图片数据数组(包含缩略图和原图 URL)
*/
async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail: string; original: string }>> {
console.log("[Gemini Image] Extracting images from response:", JSON.stringify(response).substring(0, 500)); // 🔍 调试日志
const images: Array<{ thumbnail: string; original: string }> = [];
const uploadPromises: Promise<void>[] = [];
// 1. 尝试 OpenAI 格式 (choices[0].message.content)
if (response.choices && response.choices.length > 0) {
const content = response.choices[0]?.message?.content;
if (content && typeof content === "string") {
console.log("[Gemini Image] Parsing OpenAI format, content:", content.substring(0, 100) + "...");
const imageData = extractImageDataFromText(content);
for (const img of imageData) {
if (img.isBase64 && img.base64Data) {
// 异步上传 base64 到 S3(生成缩略图和原图)
const uploadPromise = uploadBase64ToS3(img.base64Data, img.mimeType)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
} else {
// 非 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 => {
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 after retries:", error);
// 不再 fallback 到原始 URL,而是抛出错误
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
});
uploadPromises.push(uploadPromise);
}
}
}
}
// 2. 尝试 Nano Banana API 格式 (results[].url)
if (response.results && Array.isArray(response.results)) {
console.log("[Gemini Image] Parsing Nano Banana format, results count:", response.results.length);
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(国内中转)
// 如果下载失败,抛出错误,不要使用原始URL(官方地址可能无法访问)
const uploadPromise = downloadAndUploadToS3(result.url)
.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 after retries:", error);
// 不再 fallback 到原始 URL,而是抛出错误
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
});
uploadPromises.push(uploadPromise);
}
}
}
// 3. 尝试 Gemini 格式 (candidates[0].content.parts)
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.text) {
const imageData = extractImageDataFromText(part.text);
for (const img of imageData) {
if (img.isBase64 && img.base64Data) {
const uploadPromise = uploadBase64ToS3(img.base64Data, img.mimeType)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
} else {
// 非 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 => {
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 after retries:", error);
// 不再 fallback 到原始 URL,而是抛出错误
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
});
uploadPromises.push(uploadPromise);
}
}
}
// inline_data 格式
if (part.inline_data && part.inline_data.data) {
const mimeType = part.inline_data.mime_type || "image/png";
const uploadPromise = uploadBase64ToS3(part.inline_data.data, mimeType)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
} else if (part.inlineData && part.inlineData.data) {
const mimeType = part.inlineData.mimeType || "image/png";
const uploadPromise = uploadBase64ToS3(part.inlineData.data, mimeType)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
}
}
}
}
// 等待所有上传完成
await Promise.all(uploadPromises);
console.log("[Gemini Image] Extracted and uploaded", images.length, "images (thumbnail + original)");
return images;
}
/**
* 将图片数据转换为 Markdown 格式
* 使用特殊格式传递缩略图和原图 URL:![thumb|||原图URL|||imageSize](缩略图URL)
* 前端的 ImagePreview 组件会解析这个格式
*
* 注意:不能使用 $$ 作为分隔符,因为会被 Markdown 解析器当作 LaTeX 数学公式!
* 使用 ||| (三个竖线) 作为分隔符,不会与任何 Markdown 语法冲突
*/
function formatImageResponse(
images: Array<{ thumbnail: string; original: string }>,
imageSize: string = "2K"
): string {
if (images.length === 0) {
return "生成图片失败,未找到图片数据";
}
// 返回 Markdown 格式的图片,在 alt 文本中编码原图 URL 和 imageSize
// 使用 ||| 作为分隔符,避免与 Markdown 表格语法和 LaTeX 数学公式冲突
return images.map((img) => `![thumb|||${img.original}|||${imageSize}](${img.thumbnail})`).join("\n\n");
}
/**
* 发送 Gemini 图像生成/编辑请求
* 使用独立的请求格式,不依赖 OpenAI SDK
* 支持发送完整的消息数组(包括文本和图片)
*/
async function sendGeminiRequest(
baseURL: string,
apiKey: string,
messages: any[],
modelId: string,
imageParams?: {
aspectRatio?: string;
imageSize?: string;
}
): Promise<GeminiResponse> {
// 使用 Nano Banana 原生端点(支持完整的图片生成参数)
const url = `${baseURL}/v1/draw/nano-banana`;
// 提取最后一条消息的文本作为prompt
const lastMessage = messages[messages.length - 1];
const prompt = typeof lastMessage.content === 'string'
? lastMessage.content
: lastMessage.content?.find((c: any) => c.type === 'text')?.text || '生成图片';
// 提取所有参考图 (urls)
const urls: string[] = [];
// 遍历所有消息提取图片(通常用户会发送多张参考图)
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());
}
}
});
}
});
// 🆕 处理参考图:上传 Base64 到 S3
const processedUrls: string[] = [];
if (urls.length > 0) {
console.log(`[Gemini Image] Processing ${urls.length} reference images...`);
for (const url of urls) {
const s3Url = await ensureImageUrl(url);
processedUrls.push(s3Url);
}
}
console.log("[Gemini Image] Sending request to:", url);
console.log("[Gemini Image] Using model:", modelId);
console.log("[Gemini Image] Prompt:", prompt.substring(0, 100));
console.log("[Gemini Image] Reference image count:", urls.length);
console.log("[Gemini Image] Image params:", imageParams);
// 使用 Nano Banana 原生格式
const requestBody: any = {
model: modelId,
prompt: prompt,
};
// 添加参考图参数
if (processedUrls.length > 0) {
requestBody.urls = processedUrls;
}
// 添加 Nano Banana 特定参数
if (imageParams?.aspectRatio) {
requestBody.aspectRatio = imageParams.aspectRatio;
}
if (imageParams?.imageSize) {
requestBody.imageSize = imageParams.imageSize;
}
console.log("[Gemini Image] Request body (truncated):", JSON.stringify(requestBody).substring(0, 500) + "...");
// 根据分辨率动态设置超时时间
const timeout = imageParams?.imageSize === "4K" ? 300000 : 120000; // 4K: 5分钟, 其他: 2分钟
console.log(`[Gemini Image] Timeout set to ${timeout/1000} seconds for ${imageParams?.imageSize || "default"}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(requestBody),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
console.error("[Gemini Image] Request failed:", response.status, errorText);
throw new Error(
`Gemini API error: ${response.status} ${response.statusText}`
);
}
// 处理流式响应(SSE格式)
console.log("[Gemini Image] Reading stream response...");
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = "";
let lastValidData: any = null;
if (!reader) {
throw new Error("Response body is null");
}
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("[Gemini Image] Stream completed");
break;
}
// 将新数据添加到缓冲区
buffer += decoder.decode(value, { stream: true });
// 按行分割
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // 保留最后一个不完整的行
for (const line of lines) {
const trimmedLine = line.trim();
// 解析 SSE 格式: "data: {...}"
if (trimmedLine.startsWith("data: ")) {
try {
const jsonStr = trimmedLine.substring(6); // 移除 "data: " 前缀
const data = JSON.parse(jsonStr);
// 保存最新的有效数据
lastValidData = data;
// 记录进度
if (data.progress !== undefined) {
console.log(`[Gemini Image] Progress: ${data.progress}%`);
}
// 如果成功完成,立即返回
if (data.status === "succeeded") {
console.log("[Gemini Image] Generation succeeded!");
console.log("[Gemini Image] Full success data:", JSON.stringify(data).substring(0, 1000)); // 🔍 打印完整成功数据
return data as GeminiResponse;
}
// 如果失败,抛出错误(不被catch捕获)
if (data.status === "failed") {
const errorMsg = `图片生成失败: ${data.failure_reason || data.error || "未知错误"}`;
console.error("[Gemini Image] Generation failed:", data.failure_reason, data.error);
clearTimeout(timeoutId);
throw new Error(errorMsg);
}
} catch (e) {
// 只捕获JSON解析错误,重新抛出其他错误
if (e instanceof Error && e.message.includes("图片生成失败")) {
throw e; // 重新抛出失败错误
}
console.warn("[Gemini Image] Failed to parse SSE line:", trimmedLine);
}
}
}
}
// 流结束后,返回最后一个有效数据
if (lastValidData) {
console.log("[Gemini Image] Returning last valid data");
return lastValidData as GeminiResponse;
}
throw new Error("No valid data received from stream");
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
console.error("[Gemini Image] Request timeout after 120 seconds");
throw new Error("Gemini 请求超时(120秒),将自动切换备用模型");
}
throw error;
}
}
/**
* 创建 Gemini 图像生成 Provider
* 使用与 local-qwen.ts 相同的格式
*/
export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
return {
chat: (modelId: string) => ({
doGenerate: async (options: any) => {
console.log("[Gemini Image] doGenerate called!"); // 调试日志
console.log("[Gemini Image] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
// 🆕 支持覆盖 modelId(用于备用模型切换)
const effectiveModelId = options.overrideModelId || modelId;
console.log("[Gemini Image] Using modelId:", effectiveModelId, options.overrideModelId ? "(overridden)" : "(default)");
console.log("[Gemini Image] Image params from options:", options.imageParams);
try {
// 直接传递完整的消息数组(包括文本和图片)
// 确保imageSize有值,优先级:用户选择 > 默认2K
const imageParams = {
aspectRatio: options.imageParams?.aspectRatio || "auto",
imageSize: options.imageParams?.imageSize || "2K"
};
// 🆕 获取用户选择的图片数量,默认 1 张
const imageCount = Math.min(Math.max(options.imageParams?.imageCount || 1, 1), 2); // Gemini 限制 1-2 张
console.log("[Gemini Image] Final imageParams after defaults:", imageParams);
console.log(`[Gemini Image] Image count: ${imageCount}`);
// 🆕 根据用户选择的数量发起请求
console.log(`[Gemini Image] Starting ${imageCount} concurrent request(s)...`);
const startTime = Date.now();
// 创建请求数组(使用 effectiveModelId)
const requestPromises = Array.from({ length: imageCount }, () =>
sendGeminiRequest(baseURL, apiKey, options.messages, effectiveModelId, imageParams)
);
const responses = await Promise.all(requestPromises);
console.log(`[Gemini Image] All ${imageCount} request(s) completed in ${Date.now() - startTime}ms`);
// 提取图片并上传 base64 到 S3(并行处理)
const extractPromises = responses.map(response => extractAndUploadImages(response));
const imageUrlsArrays = await Promise.all(extractPromises);
// 合并所有请求的图片结果
const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
console.log(
`[Gemini Image] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s) (uploaded to S3)`
);
// 返回 AI SDK v1 格式
return {
text: markdownText,
finishReason: "stop" as const,
usage: {
promptTokens: 0,
completionTokens: markdownText.length,
},
};
} catch (error) {
console.error("[Gemini Image] Generation failed:", error);
// ❌ 抛出错误以触发故障转移(不要吞掉错误)
throw error;
}
},
doStream: async function* (options: any) {
console.log("[Gemini Image] ===== doStream ENTERED! ====="); // 调试日志
console.log("[Gemini Image] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
// 🆕 支持覆盖 modelId(用于备用模型切换)
const effectiveModelId = options.overrideModelId || modelId;
console.log("[Gemini Image] Using modelId:", effectiveModelId, options.overrideModelId ? "(overridden)" : "(default)");
console.log("[Gemini Image] Image params from options:", options.imageParams);
try {
// 直接传递完整的消息数组(包括文本和图片)
// 确保imageSize有值,优先级:用户选择 > 默认2K
const imageParams = {
aspectRatio: options.imageParams?.aspectRatio || "auto",
imageSize: options.imageParams?.imageSize || "2K"
};
// 🆕 获取用户选择的图片数量,默认 1 张
const imageCount = Math.min(Math.max(options.imageParams?.imageCount || 1, 1), 2); // Gemini 限制 1-2 张
console.log("[Gemini Image] Final imageParams after defaults:", imageParams);
console.log(`[Gemini Image Stream] Image count: ${imageCount}`);
// 🆕 根据用户选择的数量发起请求
console.log(`[Gemini Image Stream] Starting ${imageCount} concurrent request(s)...`);
const startTime = Date.now();
// 创建请求数组(使用 effectiveModelId)
const requestPromises = Array.from({ length: imageCount }, () =>
sendGeminiRequest(baseURL, apiKey, options.messages, effectiveModelId, imageParams)
);
const responses = await Promise.all(requestPromises);
console.log(`[Gemini Image Stream] All ${imageCount} request(s) completed in ${Date.now() - startTime}ms`);
// 提取图片并上传 base64 到 S3(并行处理)
const extractPromises = responses.map(response => extractAndUploadImages(response));
const imageUrlsArrays = await Promise.all(extractPromises);
// 合并所有请求的图片结果
const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
console.log(
`[Gemini Image Stream] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s) (uploaded to S3)`
);
// 一次性发送整个图片 markdown(不再分块,因为 base64 图片很大)
yield { type: "text-delta" as const, textDelta: markdownText };
console.log("[Gemini Image Stream] Sent complete markdown");
// 发送完成信号
yield {
type: "finish" as const,
finishReason: "stop" as const,
usage: {
promptTokens: 0,
completionTokens: markdownText.length,
},
};
} catch (error) {
console.error("[Gemini Image Stream] Generation failed:", error);
const errorMessage =
error instanceof Error
? `图片生成失败: ${error.message}`
: "图片生成失败,请稍后重试";
// 发送错误信息
yield { type: "text-delta" as const, textDelta: errorMessage };
yield {
type: "finish" as const,
finishReason: "error" as const,
usage: { promptTokens: 0, completionTokens: 0 },
};
}
},
modelId,
provider: "gemini-image",
specificationVersion: "v1" as const,
defaultObjectGenerationMode: "json" as const,
}),
};
}