The staging area is empty — there are no staged changes to summarize. Stage some files first with git add and I can help craft the message.
This commit is contained in:
@@ -1094,7 +1094,14 @@ async function _runSingleMediaAnalysis(
|
|||||||
|
|
||||||
const maxDimension = config.AI_LLM_IMAGE_MAX_DIMENSION ?? 1024;
|
const maxDimension = config.AI_LLM_IMAGE_MAX_DIMENSION ?? 1024;
|
||||||
|
|
||||||
// ── 1. Download attachments for this message (with resize — R5) ──
|
// ── 1-3. Parallel download of ALL media sources ──
|
||||||
|
// Build all download promises upfront and execute them in one Promise.all.
|
||||||
|
// Attachment, URL, sticker/emoji downloads are fully independent of each other.
|
||||||
|
// An 8-image cap is enforced across all sources combined.
|
||||||
|
|
||||||
|
const downloadPromises: Array<Promise<void>> = [];
|
||||||
|
|
||||||
|
// ── Attachment downloads ──
|
||||||
const msgAttachments = (allAttachments ?? [])
|
const msgAttachments = (allAttachments ?? [])
|
||||||
.filter(
|
.filter(
|
||||||
(att) =>
|
(att) =>
|
||||||
@@ -1104,99 +1111,100 @@ async function _runSingleMediaAnalysis(
|
|||||||
)
|
)
|
||||||
.slice(0, 8);
|
.slice(0, 8);
|
||||||
|
|
||||||
await Promise.all(
|
for (const att of msgAttachments) {
|
||||||
msgAttachments.map(async (att) => {
|
downloadPromises.push(
|
||||||
const urlToUse = getAttachmentImageUrl(att);
|
(async () => {
|
||||||
if (!urlToUse) return;
|
const urlToUse = getAttachmentImageUrl(att);
|
||||||
|
if (!urlToUse) return;
|
||||||
|
|
||||||
// Check vision cache BEFORE downloading
|
// Check vision cache BEFORE downloading
|
||||||
const attVisionKey = makeImageCacheKey(urlToUse);
|
const attVisionKey = makeImageCacheKey(urlToUse);
|
||||||
const cachedVision = await getCachedMediaAnalysis(attVisionKey);
|
const cachedVision = await getCachedMediaAnalysis(attVisionKey);
|
||||||
if (cachedVision) {
|
if (cachedVision) {
|
||||||
log.debug(
|
log.debug(
|
||||||
{ attachmentId: att.id, cacheKey: attVisionKey },
|
{ attachmentId: att.id, cacheKey: attVisionKey },
|
||||||
"Vision cache HIT for attachment — skipped download",
|
"Vision cache HIT for attachment — skipped download",
|
||||||
);
|
|
||||||
const sourceLabel = `[gambar di atas adalah attachment ${att.filename} dari pesan id=${att.message_id}]`;
|
|
||||||
const analysisText = `[Media analysis for message ${att.message_id}] ${sourceLabel}: ${cachedVision}`;
|
|
||||||
const existing = mediaAnalysisMap.get(targetId) ?? [];
|
|
||||||
existing.push(analysisText);
|
|
||||||
mediaAnalysisMap.set(targetId, existing);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(urlToUse, { signal: controller.signal });
|
|
||||||
if (!res.ok || !res.body) return;
|
|
||||||
|
|
||||||
let totalBytes = 0;
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
const reader = res.body.getReader();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
if (value) {
|
|
||||||
totalBytes += value.length;
|
|
||||||
if (totalBytes > 10 * 1024 * 1024) {
|
|
||||||
reader.cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chunks.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageBytes = Buffer.concat(chunks);
|
|
||||||
const sniffedMime = sniffImageMimeType(imageBytes);
|
|
||||||
if (!sniffedMime) {
|
|
||||||
log.warn(
|
|
||||||
{ attachmentId: att.id },
|
|
||||||
"Skipping attachment: not a recognised image format",
|
|
||||||
);
|
);
|
||||||
|
const sourceLabel = `[gambar di atas adalah attachment ${att.filename} dari pesan id=${att.message_id}]`;
|
||||||
|
const analysisText = `[Media analysis for message ${att.message_id}] ${sourceLabel}: ${cachedVision}`;
|
||||||
|
const existing = mediaAnalysisMap.get(targetId) ?? [];
|
||||||
|
existing.push(analysisText);
|
||||||
|
mediaAnalysisMap.set(targetId, existing);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize before base64 encoding (R5)
|
const controller = new AbortController();
|
||||||
const { data: resizedBuffer, mimeType: resizedMime } =
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||||
await resizeImageForVision(imageBytes, maxDimension);
|
|
||||||
|
|
||||||
const dataUrl = `data:${resizedMime};base64,${resizedBuffer.toString("base64")}`;
|
try {
|
||||||
const part: MessageImagePart = {
|
const res = await fetch(urlToUse, { signal: controller.signal });
|
||||||
type: "image_url",
|
if (!res.ok || !res.body) return;
|
||||||
image_url: { url: dataUrl },
|
|
||||||
sourceLabel: `[gambar di atas adalah attachment ${att.filename} dari pesan id=${att.message_id}]`,
|
|
||||||
};
|
|
||||||
const existing = imageMap.get(targetId) ?? [];
|
|
||||||
existing.push(part);
|
|
||||||
imageMap.set(targetId, existing);
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(
|
|
||||||
{
|
|
||||||
attachmentId: att.id,
|
|
||||||
error: err instanceof Error ? err.message : String(err),
|
|
||||||
},
|
|
||||||
"Error downloading attachment",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── 2. Fetch URLs found in message text ──
|
let totalBytes = 0;
|
||||||
const content = target.edited_content ?? target.content;
|
const chunks: Uint8Array[] = [];
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
if (value) {
|
||||||
|
totalBytes += value.length;
|
||||||
|
if (totalBytes > 10 * 1024 * 1024) {
|
||||||
|
reader.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageBytes = Buffer.concat(chunks);
|
||||||
|
const sniffedMime = sniffImageMimeType(imageBytes);
|
||||||
|
if (!sniffedMime) {
|
||||||
|
log.warn(
|
||||||
|
{ attachmentId: att.id },
|
||||||
|
"Skipping attachment: not a recognised image format",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: resizedBuffer, mimeType: resizedMime } =
|
||||||
|
await resizeImageForVision(imageBytes, maxDimension);
|
||||||
|
|
||||||
|
const dataUrl = `data:${resizedMime};base64,${resizedBuffer.toString("base64")}`;
|
||||||
|
const part: MessageImagePart = {
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: dataUrl },
|
||||||
|
sourceLabel: `[gambar di atas adalah attachment ${att.filename} dari pesan id=${att.message_id}]`,
|
||||||
|
};
|
||||||
|
const existing = imageMap.get(targetId) ?? [];
|
||||||
|
if (existing.length < 8) {
|
||||||
|
existing.push(part);
|
||||||
|
imageMap.set(targetId, existing);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(
|
||||||
|
{
|
||||||
|
attachmentId: att.id,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
},
|
||||||
|
"Error downloading attachment",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── URL fetch promises ──
|
||||||
const urls = extractUrlsFromText(content).slice(0, 3);
|
const urls = extractUrlsFromText(content).slice(0, 3);
|
||||||
|
const urlWebTexts: string[] = [];
|
||||||
|
|
||||||
if (urls.length > 0) {
|
for (const url of urls) {
|
||||||
const webTexts: string[] = [];
|
downloadPromises.push(
|
||||||
await Promise.all(
|
(async () => {
|
||||||
urls.map(async (url) => {
|
|
||||||
const result = await fetchUrlSafely(url);
|
const result = await fetchUrlSafely(url);
|
||||||
if (result.type === "image" && result.data && result.mimeType) {
|
if (result.type === "image" && result.data && result.mimeType) {
|
||||||
// Resize fetched images too (R5)
|
|
||||||
const { data: resizedBuffer, mimeType: resizedMime } =
|
const { data: resizedBuffer, mimeType: resizedMime } =
|
||||||
await resizeImageForVision(result.data, maxDimension);
|
await resizeImageForVision(result.data, maxDimension);
|
||||||
|
|
||||||
@@ -1207,17 +1215,18 @@ async function _runSingleMediaAnalysis(
|
|||||||
sourceLabel: `[gambar di atas berasal dari link ${url} pada pesan id=${targetId}]`,
|
sourceLabel: `[gambar di atas berasal dari link ${url} pada pesan id=${targetId}]`,
|
||||||
};
|
};
|
||||||
const existing = imageMap.get(targetId) ?? [];
|
const existing = imageMap.get(targetId) ?? [];
|
||||||
existing.push(part);
|
if (existing.length < 8) {
|
||||||
imageMap.set(targetId, existing);
|
existing.push(part);
|
||||||
|
imageMap.set(targetId, existing);
|
||||||
|
}
|
||||||
} else if (result.type === "text" && result.textContent) {
|
} else if (result.type === "text" && result.textContent) {
|
||||||
webTexts.push(`[Isi Web dari ${url}]: ${result.textContent}`);
|
urlWebTexts.push(`[Isi Web dari ${url}]: ${result.textContent}`);
|
||||||
}
|
}
|
||||||
}),
|
})(),
|
||||||
);
|
);
|
||||||
if (webTexts.length > 0) webTextMap.set(targetId, webTexts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Sticker / embed / custom emoji images ──
|
// ── Sticker / embed / custom emoji download promises ──
|
||||||
const mediaEvidence = extractMessageMediaEvidence(target.metadata);
|
const mediaEvidence = extractMessageMediaEvidence(target.metadata);
|
||||||
const mediaCandidates: Array<{
|
const mediaCandidates: Array<{
|
||||||
messageId: string;
|
messageId: string;
|
||||||
@@ -1273,81 +1282,91 @@ async function _runSingleMediaAnalysis(
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
const remainingSlots = Math.max(0, 8 - (imageMap.get(targetId)?.length ?? 0));
|
for (const candidate of mediaCandidates) {
|
||||||
|
downloadPromises.push(
|
||||||
|
(async () => {
|
||||||
|
// Skip if we already have 8 images
|
||||||
|
if ((imageMap.get(targetId)?.length ?? 0) >= 8) return;
|
||||||
|
|
||||||
await Promise.all(
|
// Vision cache check before download
|
||||||
mediaCandidates.slice(0, remainingSlots).map(async (candidate) => {
|
const visionCacheKey = candidate.customEmojiId
|
||||||
// Vision cache check before download
|
? makeCustomEmojiCacheKey(candidate.customEmojiId)
|
||||||
const visionCacheKey = candidate.customEmojiId
|
: candidate.stickerName
|
||||||
? makeCustomEmojiCacheKey(candidate.customEmojiId)
|
? makeStickerCacheKey(candidate.stickerName)
|
||||||
: candidate.stickerName
|
: makeImageCacheKey(candidate.url);
|
||||||
? makeStickerCacheKey(candidate.stickerName)
|
const cachedVision = await getCachedMediaAnalysis(visionCacheKey);
|
||||||
: makeImageCacheKey(candidate.url);
|
if (cachedVision) {
|
||||||
const cachedVision = await getCachedMediaAnalysis(visionCacheKey);
|
log.debug(
|
||||||
if (cachedVision) {
|
{ cacheKey: visionCacheKey },
|
||||||
log.debug(
|
"Vision cache HIT for media candidate — skipped download",
|
||||||
{ cacheKey: visionCacheKey },
|
);
|
||||||
"Vision cache HIT for media candidate — skipped download",
|
const analysisText = `[Media analysis for message ${candidate.messageId}] ${candidate.label}: ${cachedVision}`;
|
||||||
);
|
const existing = mediaAnalysisMap.get(targetId) ?? [];
|
||||||
const analysisText = `[Media analysis for message ${candidate.messageId}] ${candidate.label}: ${cachedVision}`;
|
existing.push(analysisText);
|
||||||
const existing = mediaAnalysisMap.get(targetId) ?? [];
|
mediaAnalysisMap.set(targetId, existing);
|
||||||
existing.push(analysisText);
|
return;
|
||||||
mediaAnalysisMap.set(targetId, existing);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sticker download cache
|
|
||||||
if (candidate.stickerName && isStickerCacheReady()) {
|
|
||||||
try {
|
|
||||||
const cached = await getStickerFromCache(candidate.stickerName);
|
|
||||||
if (cached) {
|
|
||||||
const part: MessageImagePart = {
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url: `data:${cached.mimeType};base64,${cached.base64}`,
|
|
||||||
},
|
|
||||||
sourceLabel: candidate.label,
|
|
||||||
stickerName: candidate.stickerName,
|
|
||||||
};
|
|
||||||
const existing = imageMap.get(targetId) ?? [];
|
|
||||||
existing.push(part);
|
|
||||||
imageMap.set(targetId, existing);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Fall through to fetch
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const result = await fetchUrlSafely(candidate.url);
|
// Sticker download cache
|
||||||
if (result.type !== "image" || !result.data || !result.mimeType) return;
|
if (candidate.stickerName && isStickerCacheReady()) {
|
||||||
|
try {
|
||||||
|
const cached = await getStickerFromCache(candidate.stickerName);
|
||||||
|
if (cached) {
|
||||||
|
const part: MessageImagePart = {
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: `data:${cached.mimeType};base64,${cached.base64}`,
|
||||||
|
},
|
||||||
|
sourceLabel: candidate.label,
|
||||||
|
stickerName: candidate.stickerName,
|
||||||
|
};
|
||||||
|
const existing = imageMap.get(targetId) ?? [];
|
||||||
|
if (existing.length < 8) {
|
||||||
|
existing.push(part);
|
||||||
|
imageMap.set(targetId, existing);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to fetch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resize sticker/emoji images too (R5)
|
const result = await fetchUrlSafely(candidate.url);
|
||||||
const { data: resizedBuffer, mimeType: resizedMime } =
|
if (result.type !== "image" || !result.data || !result.mimeType) return;
|
||||||
await resizeImageForVision(result.data, maxDimension);
|
|
||||||
|
|
||||||
const base64 = resizedBuffer.toString("base64");
|
const { data: resizedBuffer, mimeType: resizedMime } =
|
||||||
if (candidate.stickerName) {
|
await resizeImageForVision(result.data, maxDimension);
|
||||||
setStickerInCache(candidate.stickerName, base64, resizedMime).catch(
|
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const part: MessageImagePart = {
|
const base64 = resizedBuffer.toString("base64");
|
||||||
type: "image_url",
|
if (candidate.stickerName) {
|
||||||
image_url: {
|
setStickerInCache(candidate.stickerName, base64, resizedMime).catch(
|
||||||
url: `data:${resizedMime};base64,${base64}`,
|
() => {},
|
||||||
},
|
);
|
||||||
sourceLabel: candidate.label,
|
}
|
||||||
stickerName: candidate.stickerName,
|
|
||||||
customEmojiId: candidate.customEmojiId,
|
const part: MessageImagePart = {
|
||||||
customEmojiName: candidate.customEmojiName,
|
type: "image_url",
|
||||||
};
|
image_url: { url: `data:${resizedMime};base64,${base64}` },
|
||||||
const existing = imageMap.get(targetId) ?? [];
|
sourceLabel: candidate.label,
|
||||||
existing.push(part);
|
stickerName: candidate.stickerName,
|
||||||
imageMap.set(targetId, existing);
|
customEmojiId: candidate.customEmojiId,
|
||||||
}),
|
customEmojiName: candidate.customEmojiName,
|
||||||
);
|
};
|
||||||
|
const existing = imageMap.get(targetId) ?? [];
|
||||||
|
if (existing.length < 8) {
|
||||||
|
existing.push(part);
|
||||||
|
imageMap.set(targetId, existing);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute ALL media downloads in parallel
|
||||||
|
await Promise.all(downloadPromises);
|
||||||
|
|
||||||
|
// Collect web text results from URL fetches
|
||||||
|
if (urlWebTexts.length > 0) webTextMap.set(targetId, urlWebTexts);
|
||||||
|
|
||||||
// ── 4. Vision analysis for every image ──
|
// ── 4. Vision analysis for every image ──
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|||||||
Reference in New Issue
Block a user