From 010d7d1be523eb6577f1aecde38f4d5c0c7e7688 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 15 Jun 2026 14:33:31 +0800 Subject: [PATCH] fix: poll grsai image tasks and return originals --- lib/ai/providers/gemini-image.ts | 40 ++++++++++++++++++++++++++++++- lib/external-image-api/service.ts | 36 ++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/ai/providers/gemini-image.ts b/lib/ai/providers/gemini-image.ts index 1419accff..b4209b01b 100644 --- a/lib/ai/providers/gemini-image.ts +++ b/lib/ai/providers/gemini-image.ts @@ -664,6 +664,30 @@ async function sendGrsaiGptImageRequest(params: { responseKeys: isRecord(data) ? Object.keys(data).slice(0, 12) : [], }); + if (!hasGrsaiImages(data)) { + const status = getGrsaiStatus(data); + if (status === "failed" || status === "error") { + throw new Error(`图片生成失败: ${getGrsaiErrorMessage(data)}`); + } + + const taskId = getGrsaiTaskId(data); + if (taskId) { + logger.info(`${apiLogPrefix} Native response has no images, polling task result`, { + taskId, + progress: getGrsaiProgress(data), + status: status || null, + }); + return pollGrsaiDrawResult({ + baseURL, + apiKey, + taskId, + timeoutMs: timeout, + apiLogPrefix, + timeoutLabel: "Grsai GPT Image", + }); + } + } + return data; } catch (error) { clearTimeout(timeoutId); @@ -827,6 +851,20 @@ async function pollGrsaiNanoBananaResult(params: { taskId: string; timeoutMs: number; apiLogPrefix: string; +}): Promise { + return pollGrsaiDrawResult({ + ...params, + timeoutLabel: "GRSAI Nano Banana", + }); +} + +async function pollGrsaiDrawResult(params: { + baseURL: string; + apiKey: string; + taskId: string; + timeoutMs: number; + apiLogPrefix: string; + timeoutLabel: string; }): Promise { const resultUrl = `${params.baseURL.replace(/\/+$/, "")}/v1/draw/result`; const deadline = Date.now() + params.timeoutMs; @@ -882,7 +920,7 @@ async function pollGrsaiNanoBananaResult(params: { lastPayload: JSON.stringify(lastPayload || {}).substring(0, 500), }); throw new Error( - `GRSAI Nano Banana 异步任务超时(${Math.round(params.timeoutMs / 1000)}秒),请稍后重试或切换备用模型` + `${params.timeoutLabel} 异步任务超时(${Math.round(params.timeoutMs / 1000)}秒),请稍后重试或切换备用模型` ); } diff --git a/lib/external-image-api/service.ts b/lib/external-image-api/service.ts index 46c7acf6f..f37280991 100644 --- a/lib/external-image-api/service.ts +++ b/lib/external-image-api/service.ts @@ -51,6 +51,7 @@ export class ExternalImageApiGenerationError extends Error { type ExternalImageApiGeneratedImage = { id: number; + url: string; originalUrl: string; thumbnailUrl: string; imageSize: string; @@ -484,6 +485,18 @@ function filterCompatibleCandidates( return acceptedCandidates; } +function parseGeneratedImageMarkdownMeta(alt: string) { + const metaMatch = alt.match(/thumb\|\|\|([^|]+)\|\|\|(.+)$/i); + if (!metaMatch) { + return null; + } + + return { + originalUrl: normalizeText(metaMatch[1]), + imageSize: normalizeText(metaMatch[2]), + }; +} + export async function ensureHostedImageMarkdown( markdown: string, imageSize = "2K", @@ -510,13 +523,21 @@ export async function ensureHostedImageMarkdown( for (const match of matches) { const raw = match[0]; - const sourceUrl = match[2] || ""; + const alt = match[1] || ""; + const thumbnailSourceUrl = normalizeText(match[2]); + const metadata = parseGeneratedImageMarkdownMeta(alt); + const originalSourceUrl = metadata?.originalUrl || thumbnailSourceUrl; + const resolvedImageSize = metadata?.imageSize || imageSize; const start = match.index ?? -1; if (start < 0) continue; rebuilt += markdown.slice(cursor, start); - const hostedUrl = await ensureHostedImageUrl(sourceUrl); - rebuilt += `![thumb|||${hostedUrl}|||${imageSize}](${hostedUrl})`; + const hostedOriginalUrl = await ensureHostedImageUrl(originalSourceUrl); + const hostedThumbnailUrl = + thumbnailSourceUrl && thumbnailSourceUrl !== originalSourceUrl + ? await ensureHostedImageUrl(thumbnailSourceUrl) + : hostedOriginalUrl; + rebuilt += `![thumb|||${hostedOriginalUrl}|||${resolvedImageSize}](${hostedThumbnailUrl})`; cursor = start + raw.length; } @@ -533,13 +554,14 @@ export function extractGeneratedImagesFromMarkdown(markdown: string) { const thumbnailUrl = match[2] || ""; let originalUrl = thumbnailUrl; let imageSize = "2K"; - const metaMatch = alt.match(/thumb\|\|\|([^|]+)\|\|\|(.+)$/i); - if (metaMatch) { - originalUrl = normalizeText(metaMatch[1]) || thumbnailUrl; - imageSize = normalizeText(metaMatch[2]) || imageSize; + const metadata = parseGeneratedImageMarkdownMeta(alt); + if (metadata) { + originalUrl = metadata.originalUrl || thumbnailUrl; + imageSize = metadata.imageSize || imageSize; } return { id: index + 1, + url: originalUrl, originalUrl, thumbnailUrl, imageSize,