import "server-only"; import { uploadFile } from "@/lib/storage/service"; interface GeminiImageConfig { apiKey: string; baseURL: string; } /** * 压缩图片生成缩略图(使用 sharp) */ async function createThumbnail(buffer: Buffer, mimeType: string): Promise { 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 { 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> { console.log("[Gemini Image] Extracting images from response:", JSON.stringify(response).substring(0, 500)); // 🔍 调试日志 const images: Array<{ thumbnail: string; original: string }> = []; const uploadPromises: Promise[] = []; // 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 { // 使用 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, }), }; }