fix(agent): downscale vision images to 500px base64
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-19 01:39:18 +08:00
parent cd9cadbb61
commit 5694fefca0

View File

@ -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<Response> {
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<string | null> {
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: [