fix(volcano): retry image generation with inline base64 on remote fetch timeout
This commit is contained in:
parent
998094636c
commit
bfec61aad1
@ -15,6 +15,83 @@ type VolcanoUrlRewriteConfig = {
|
||||
};
|
||||
|
||||
let cachedRewriteConfig: VolcanoUrlRewriteConfig | null | undefined;
|
||||
const VOLCANO_IMAGE_FETCH_TIMEOUT_MS = 20_000;
|
||||
const VOLCANO_INLINE_MAX_SIDE = 1024;
|
||||
|
||||
function guessImageMimeTypeFromUrl(url: string): string {
|
||||
const cleanUrl = url.split("?")[0]?.toLowerCase() || "";
|
||||
if (cleanUrl.endsWith(".png")) return "image/png";
|
||||
if (cleanUrl.endsWith(".webp")) return "image/webp";
|
||||
if (cleanUrl.endsWith(".gif")) return "image/gif";
|
||||
if (cleanUrl.endsWith(".bmp")) return "image/bmp";
|
||||
return "image/jpeg";
|
||||
}
|
||||
|
||||
function shouldRetryVolcanoWithInlineImage(errorText: string): boolean {
|
||||
if (!errorText) return false;
|
||||
return /timeout while downloading url|download multimodal file timed out|invalidparameter/i.test(
|
||||
errorText
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchImageAsDataUri(url: string): Promise<string | null> {
|
||||
if (!/^https?:\/\//i.test(url)) return null;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), VOLCANO_IMAGE_FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
console.warn("[Volcano Vision] Failed to fetch source image for inline retry:", response.status, url);
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
if (buffer.length === 0) return null;
|
||||
|
||||
const inputMime = response.headers.get("content-type") || guessImageMimeTypeFromUrl(url);
|
||||
|
||||
try {
|
||||
const sharp = await import("sharp");
|
||||
const optimized = await sharp.default(buffer)
|
||||
.rotate()
|
||||
.resize(VOLCANO_INLINE_MAX_SIDE, VOLCANO_INLINE_MAX_SIDE, {
|
||||
fit: "inside",
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.jpeg({ quality: 86, mozjpeg: true })
|
||||
.toBuffer();
|
||||
return `data:image/jpeg;base64,${optimized.toString("base64")}`;
|
||||
} catch {
|
||||
return `data:${inputMime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Volcano Vision] Inline image fetch failed:", error);
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function convertImageFieldToInlineDataUri(
|
||||
image: string | string[] | undefined
|
||||
): Promise<string | string[] | undefined> {
|
||||
if (!image) return image;
|
||||
const list = Array.isArray(image) ? image : [image];
|
||||
const converted = await Promise.all(
|
||||
list.map(async (item) => {
|
||||
if (typeof item !== "string") return null;
|
||||
if (item.startsWith("data:image")) return item;
|
||||
return fetchImageAsDataUri(item);
|
||||
})
|
||||
);
|
||||
|
||||
const ready = converted.filter((item): item is string => typeof item === "string" && item.length > 0);
|
||||
if (ready.length === 0) return undefined;
|
||||
return Array.isArray(image) ? ready : ready[0];
|
||||
}
|
||||
|
||||
async function getVolcanoUrlRewriteConfig(): Promise<VolcanoUrlRewriteConfig | null> {
|
||||
if (cachedRewriteConfig !== undefined) {
|
||||
@ -426,49 +503,66 @@ async function sendVolcanoRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 10 分钟超时
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 600000);
|
||||
const executeRequest = async (payload: any, tag: string): Promise<VolcanoVisionResponse> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 600000);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`[Volcano Vision] Request failed (${tag}):`, response.status, errorText);
|
||||
const err = new Error(
|
||||
`Volcano Vision API error: ${response.status} ${response.statusText} - ${errorText}`
|
||||
) as Error & { status?: number; body?: string };
|
||||
err.status = response.status;
|
||||
err.body = errorText;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(
|
||||
`[Volcano Vision] Response received (${tag}):`,
|
||||
JSON.stringify(data).substring(0, 200) + "..."
|
||||
);
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`API Error: ${data.error.message || data.error.code}`);
|
||||
}
|
||||
|
||||
return data as VolcanoVisionResponse;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
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("[Volcano Vision] Request failed:", response.status, errorText);
|
||||
throw new Error(
|
||||
`Volcano Vision API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(
|
||||
"[Volcano Vision] Response received:",
|
||||
JSON.stringify(data).substring(0, 200) + "..."
|
||||
);
|
||||
|
||||
// 检查是否有错误
|
||||
if (data.error) {
|
||||
throw new Error(`API Error: ${data.error.message || data.error.code}`);
|
||||
}
|
||||
|
||||
return data as VolcanoVisionResponse;
|
||||
return await executeRequest(requestBody, "primary");
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
console.error("[Volcano Vision] Request timeout after 10 minutes");
|
||||
throw new Error("请求超时,图片处理时间过长,请稍后重试");
|
||||
}
|
||||
|
||||
const errorBody = (error as any)?.body || String(error || "");
|
||||
if (requestBody.image && shouldRetryVolcanoWithInlineImage(errorBody)) {
|
||||
console.warn("[Volcano Vision] Retrying with inline base64 image payload...");
|
||||
const inlineImage = await convertImageFieldToInlineDataUri(requestBody.image);
|
||||
if (inlineImage) {
|
||||
const retryBody = { ...requestBody, image: inlineImage };
|
||||
return await executeRequest(retryBody, "inline-retry");
|
||||
}
|
||||
console.warn("[Volcano Vision] Inline retry skipped: failed to convert source image(s).");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user