feat(whatsapp): agrupar albumes de fotos en una sola publicacion y seleccionar la mejor foto con IA

This commit is contained in:
AI Bot 2026-08-24 16:22:08 -05:00
parent 1a56049a43
commit c8fd30e1c8
2 changed files with 48 additions and 14 deletions

View File

@ -184,10 +184,44 @@ export async function POST(req: Request) {
} }
} }
// LOGIC 2: PROCESAR NUEVO MENSAJE PARA GEMINI // Cache en memoria para evitar procesar mensajes duplicados que WhatsApp retransmite en ráfaga
// Evitámos tratar de procesar respuestas cortas o mensajes vacíos const processedMessagesCache = new Map<string, number>();
if (!text && !messageData.imageMessage && !messageData.videoMessage && !messageData.documentMessage) {
return NextResponse.json({ success: true, message: 'Ignored empty message' }, { status: 200 }); function isDuplicateMessage(chatId: string, text: string): boolean {
const key = `${chatId}:${text.trim().substring(0, 100)}`;
const now = Date.now();
const lastSeen = processedMessagesCache.get(key);
// Limpieza de claves viejas (> 2 minutos)
for (const [k, time] of processedMessagesCache.entries()) {
if (now - time > 120000) {
processedMessagesCache.delete(k);
}
}
if (lastSeen && (now - lastSeen) < 45000) {
return true;
}
processedMessagesCache.set(key, now);
return false;
}
// LOGIC 2: PROCESAR PUBLICACIÓN ÚNICA
// En WhatsApp, los álbumes de fotos (ej. 25 fotos + 1 texto) llegan como 26 eventos separados.
// Las fotos individuales sin texto sustancial NO deben generar publicaciones independientes;
// se recolectan como recursos para la publicación principal que contiene el texto de la noticia.
const hasSubstantialText = Boolean(text && text.trim().length >= 15);
if (!hasSubstantialText) {
console.log(`[WhatsApp Webhook] Mensaje multimedia sin texto editorial (${text?.length || 0} chars). Registrado para lote de fotos.`);
return NextResponse.json({ success: true, message: 'Media registered for album batch' }, { status: 200 });
}
// Evitar duplicación si WhatsApp envía el mismo texto repetido en la ráfaga
if (isDuplicateMessage(remoteJid, text)) {
console.log(`[WhatsApp Webhook] Mensaje de texto duplicado en ráfaga ignorado.`);
return NextResponse.json({ success: true, message: 'Ignored duplicate text in burst' }, { status: 200 });
} }
// Descubrir el grupo central de Notificaciones y Aprobaciones (Notificaciones Almeira) // Descubrir el grupo central de Notificaciones y Aprobaciones (Notificaciones Almeira)
@ -200,10 +234,10 @@ export async function POST(req: Request) {
`📥 *Nueva Información Recibida*\n` + `📥 *Nueva Información Recibida*\n` +
`📍 *Fuente:* ${filterResult.resolvedName}\n` + `📍 *Fuente:* ${filterResult.resolvedName}\n` +
`_Almeira Web • Asistente Editorial con IA_\n\n` + `_Almeira Web • Asistente Editorial con IA_\n\n` +
`🤖 Iniciando análisis con IA, estructuración y optimización de contenido...` `🤖 Recolectando lote de imágenes y analizando contenido...`
).catch(e => console.warn('[WhatsApp Webhook] No se pudo enviar notificación de inicio:', e.message)); ).catch(e => console.warn('[WhatsApp Webhook] No se pudo enviar notificación de inicio:', e.message));
// Recolectamos todas las imágenes/multimedia candidatas (mensaje actual + lote reciente del mismo chat) // Recolectamos todas las imágenes/multimedia candidatas (mensaje actual + álbum de fotos reciente del chat)
const candidateMediaList: Array<{ base64: string; mimeType: string }> = []; const candidateMediaList: Array<{ base64: string; mimeType: string }> = [];
if (messageData.imageMessage || messageData.videoMessage || messageData.documentMessage) { if (messageData.imageMessage || messageData.videoMessage || messageData.documentMessage) {
@ -213,9 +247,9 @@ export async function POST(req: Request) {
} }
} }
// Consultamos si llegaron otras imágenes en el mismo grupo o chat en los últimos 2 minutos (álbum / ráfaga) // Consultamos todas las fotos que llegaron en el mismo grupo en los últimos 4 minutos (álbum completo de hasta 30 fotos)
try { try {
const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 4, 120); const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 30, 240);
for (const rm of recentMedia) { for (const rm of recentMedia) {
// Evitar duplicar la misma imagen exacta // Evitar duplicar la misma imagen exacta
if (!candidateMediaList.some(c => c.base64.slice(0, 80) === rm.base64.slice(0, 80))) { if (!candidateMediaList.some(c => c.base64.slice(0, 80) === rm.base64.slice(0, 80))) {
@ -226,7 +260,7 @@ export async function POST(req: Request) {
console.warn('[WhatsApp Webhook] No se pudo consultar lote multimedia reciente:', e); console.warn('[WhatsApp Webhook] No se pudo consultar lote multimedia reciente:', e);
} }
console.log(`[WhatsApp Webhook] Procesando ${candidateMediaList.length} imágenes candidatas con Gemini...`); console.log(`[WhatsApp Webhook] Procesando ${candidateMediaList.length} imágenes del álbum con Gemini...`);
// Llamar a Gemini para estructurar la publicación y seleccionar la mejor imagen // Llamar a Gemini para estructurar la publicación y seleccionar la mejor imagen
const parsed = await processWhatsAppMessage(text, candidateMediaList); const parsed = await processWhatsAppMessage(text, candidateMediaList);

View File

@ -44,25 +44,25 @@ export async function getBase64FromMediaMessage(messagePayload: any): Promise<{
*/ */
export async function fetchRecentMediaFromSameChat( export async function fetchRecentMediaFromSameChat(
remoteJid: string, remoteJid: string,
maxItems = 3, maxItems: number = 25,
windowSeconds = 90 windowSeconds: number = 180
): Promise<Array<{ base64: string, mimeType: string, messageId: string }>> { ): Promise<Array<{ base64: string, mimeType: string, messageId: string }>> {
if (!EVOLUTION_API_URL || !EVOLUTION_INSTANCE_NAME || !remoteJid) { if (!EVOLUTION_API_URL || !EVOLUTION_INSTANCE_NAME || !remoteJid) {
return []; return [];
} }
try { try {
const url = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=5`; const url = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=35`;
const response = await axios.post( const response = await axios.post(
url, url,
{ where: { "key": { "remoteJid": remoteJid } } }, { where: { "key": { "remoteJid": remoteJid } } },
{ headers, timeout: 3000 } { headers, timeout: 6000 }
); );
const records = response.data?.messages?.records || []; const records = response.data?.messages?.records || [];
const nowSec = Math.floor(Date.now() / 1000); const nowSec = Math.floor(Date.now() / 1000);
// Strict check: only messages created within the last `windowSeconds` (90s) // Strict check: only messages created within the last `windowSeconds` (180s)
const mediaRecords = records.filter((r: any) => { const mediaRecords = records.filter((r: any) => {
const isMedia = Boolean(r.message?.imageMessage || r.message?.videoMessage); const isMedia = Boolean(r.message?.imageMessage || r.message?.videoMessage);
if (!isMedia || !r.messageTimestamp) return false; if (!isMedia || !r.messageTimestamp) return false;