fix: poll grsai image tasks and return originals

This commit is contained in:
134 2026-06-15 14:33:31 +08:00
parent 801d2c4644
commit 010d7d1be5
2 changed files with 68 additions and 8 deletions

View File

@ -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<any> {
return pollGrsaiDrawResult({
...params,
timeoutLabel: "GRSAI Nano Banana",
});
}
async function pollGrsaiDrawResult(params: {
baseURL: string;
apiKey: string;
taskId: string;
timeoutMs: number;
apiLogPrefix: string;
timeoutLabel: string;
}): Promise<any> {
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)}秒),请稍后重试或切换备用模型`
);
}

View File

@ -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,