import sharp from "sharp"; type InlineVisionOptions = { maxImages?: number; maxSide?: number; timeoutMs?: number; retries?: number; loggerPrefix?: string; jpegQuality?: number; }; type InlineVisionResult = { inlineDataUrls: string[]; skipped: Array<{ url: string; reason: string }>; }; const DEFAULT_MAX_IMAGES = 6; const DEFAULT_MAX_SIDE = 900; const DEFAULT_TIMEOUT_MS = 12000; const DEFAULT_RETRIES = 2; const DEFAULT_JPEG_QUALITY = 82; async function fetchImageWithRetry(url: string, timeoutMs: number, retries: number) { let lastError: unknown; for (let attempt = 1; attempt <= retries; attempt++) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { method: "GET", signal: controller.signal, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); } catch (error) { lastError = error; if (attempt < retries) { await new Promise((resolve) => setTimeout(resolve, 250 * attempt)); } } finally { clearTimeout(timeout); } } throw lastError instanceof Error ? lastError : new Error("图片拉取失败"); } async function downscaleForVision( inputBuffer: Buffer, maxSide: number, jpegQuality: number, ) { const image = sharp(inputBuffer, { failOn: "none" }); const metadata = await image.metadata(); const hasAlpha = Boolean(metadata.hasAlpha); const resized = image .rotate() .resize({ width: maxSide, height: maxSide, fit: "inside", withoutEnlargement: true, }); if (hasAlpha) { const output = await resized.png({ compressionLevel: 9 }).toBuffer(); return { mime: "image/png", buffer: output }; } const output = await resized.jpeg({ quality: jpegQuality, mozjpeg: true }).toBuffer(); return { mime: "image/jpeg", buffer: output }; } export async function inlineVisionImages( urls: string[], options: InlineVisionOptions = {}, ): Promise { const maxImages = Math.max(1, options.maxImages ?? DEFAULT_MAX_IMAGES); const maxSide = Math.max(320, options.maxSide ?? DEFAULT_MAX_SIDE); const timeoutMs = Math.max(3000, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); const retries = Math.max(1, options.retries ?? DEFAULT_RETRIES); const jpegQuality = Math.max(55, Math.min(95, options.jpegQuality ?? DEFAULT_JPEG_QUALITY)); const loggerPrefix = options.loggerPrefix ?? "[Vision Inline]"; const inlineDataUrls: string[] = []; const skipped: Array<{ url: string; reason: string }> = []; const candidates = urls .map((item) => String(item || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) .slice(0, maxImages); for (const url of candidates) { try { const buffer = await fetchImageWithRetry(url, timeoutMs, retries); const processed = await downscaleForVision(buffer, maxSide, jpegQuality); const dataUrl = `data:${processed.mime};base64,${processed.buffer.toString("base64")}`; inlineDataUrls.push(dataUrl); } catch (error) { const reason = error instanceof Error ? error.message : "unknown_error"; skipped.push({ url, reason }); console.warn(`${loggerPrefix} skip reference image:`, { url, reason, }); } } return { inlineDataUrls, skipped }; }