fix(agent): downscale vision images to 500px base64
This commit is contained in:
parent
cd9cadbb61
commit
5694fefca0
@ -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: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user