From 5694fefca0130c2d51e85bed3354c6b164d6dd1a Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Feb 2026 01:39:18 +0800 Subject: [PATCH] fix(agent): downscale vision images to 500px base64 --- lib/ai/auto-agent-graph.ts | 84 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/lib/ai/auto-agent-graph.ts b/lib/ai/auto-agent-graph.ts index 2fae37353..06308904a 100644 --- a/lib/ai/auto-agent-graph.ts +++ b/lib/ai/auto-agent-graph.ts @@ -1,5 +1,6 @@ import { Annotation, StateGraph, START, END } from "@langchain/langgraph"; import { generateText } from "ai"; +import sharp from "sharp"; import type { AnalysisResult } from "@/lib/ai/smart-analyzer"; import type { RouteResult } from "@/lib/ai/model-router"; import { isAiModelMultimodal } from "@/lib/db/ai-model-flags"; @@ -84,6 +85,83 @@ function compactText(text: string, max = 48) { return `${normalized.slice(0, max)}...`; } +const AGENT_VISION_MAX_IMAGES = 4; +const AGENT_VISION_MAX_SIDE = 500; +const AGENT_VISION_FETCH_TIMEOUT_MS = 10_000; + +function isHttpUrl(value: string) { + return /^https?:\/\//i.test(value); +} + +async function fetchWithTimeout(url: string, timeoutMs: number): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { + signal: controller.signal, + cache: "no-store", + headers: { + "user-agent": "ai-chatbot-auto-agent/1.0", + }, + }); + } finally { + clearTimeout(timeoutId); + } +} + +async function toAgentVisionDataUrl(imageUrl: string): Promise { + try { + const response = await fetchWithTimeout(imageUrl, AGENT_VISION_FETCH_TIMEOUT_MS); + if (!response.ok) { + console.warn( + `[Auto Agent] Failed to fetch image for inline vision payload (${response.status}): ${imageUrl}`, + ); + return null; + } + + const contentType = response.headers.get("content-type") || ""; + if (!contentType.startsWith("image/")) { + console.warn(`[Auto Agent] Skip non-image content-type for vision payload: ${contentType}`); + return null; + } + + const arrayBuffer = await response.arrayBuffer(); + const optimized = await sharp(Buffer.from(arrayBuffer)) + .rotate() + .resize({ + width: AGENT_VISION_MAX_SIDE, + height: AGENT_VISION_MAX_SIDE, + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ + quality: 80, + mozjpeg: true, + }) + .toBuffer(); + + return `data:image/jpeg;base64,${optimized.toString("base64")}`; + } catch (error) { + console.warn("[Auto Agent] Failed to convert image URL to inline base64:", error); + return null; + } +} + +async function buildAgentVisionInputs(imageUrls: string[]) { + if (imageUrls.length === 0) return [] as Array<{ type: "image"; image: string }>; + + const convertedResults = await Promise.all(imageUrls.map((url) => toAgentVisionDataUrl(url))); + const inlineCount = convertedResults.filter(Boolean).length; + console.log( + `[Auto Agent] Prepared vision images: total=${imageUrls.length}, inline=${inlineCount}, remote=${imageUrls.length - inlineCount}, maxSide=${AGENT_VISION_MAX_SIDE}`, + ); + + return imageUrls.map((originalUrl, index) => ({ + type: "image" as const, + image: convertedResults[index] || originalUrl, + })); +} + function normalizeAspectRatio(value?: string): string | undefined { if (!value) return undefined; const normalized = value.trim().toLowerCase(); @@ -517,8 +595,8 @@ async function generateAgentConversation( .join("\n"); const validImageUrls = state.imageUrls - .filter((url) => /^https?:\/\//i.test(url)) - .slice(0, 4); + .filter((url) => isHttpUrl(url)) + .slice(0, AGENT_VISION_MAX_IMAGES); const imageInfo = state.hasImage ? `用户本轮可用图片数量:${state.imageUrls.length || 1}。图片来源:${state.imageFromHistory ? "历史消息复用" : "本轮上传"}。` @@ -542,7 +620,7 @@ async function generateAgentConversation( 2) 只写客观可见信息,不要编造品牌、尺寸、镜头参数。 3) 全部输出控制在 6 行内。`; - const visionInputs = validImageUrls.map((url) => ({ type: "image" as const, image: url })); + const visionInputs = await buildAgentVisionInputs(validImageUrls); const { text: visionText } = await generateText({ model: visionModel, messages: [