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
// Evitámos tratar de procesar respuestas cortas o mensajes vacíos
if (!text && !messageData.imageMessage && !messageData.videoMessage && !messageData.documentMessage) {
return NextResponse.json({ success: true, message: 'Ignored empty message' }, { status: 200 });
// Cache en memoria para evitar procesar mensajes duplicados que WhatsApp retransmite en ráfaga
const processedMessagesCache = new Map<string, number>();
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)
@ -200,10 +234,10 @@ export async function POST(req: Request) {
`📥 *Nueva Información Recibida*\n` +
`📍 *Fuente:* ${filterResult.resolvedName}\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));
// 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 }> = [];
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 {
const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 4, 120);
const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 30, 240);
for (const rm of recentMedia) {
// Evitar duplicar la misma imagen exacta
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.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
const parsed = await processWhatsAppMessage(text, candidateMediaList);

View File

@ -44,25 +44,25 @@ export async function getBase64FromMediaMessage(messagePayload: any): Promise<{
*/
export async function fetchRecentMediaFromSameChat(
remoteJid: string,
maxItems = 3,
windowSeconds = 90
maxItems: number = 25,
windowSeconds: number = 180
): Promise<Array<{ base64: string, mimeType: string, messageId: string }>> {
if (!EVOLUTION_API_URL || !EVOLUTION_INSTANCE_NAME || !remoteJid) {
return [];
}
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(
url,
{ where: { "key": { "remoteJid": remoteJid } } },
{ headers, timeout: 3000 }
{ headers, timeout: 6000 }
);
const records = response.data?.messages?.records || [];
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 isMedia = Boolean(r.message?.imageMessage || r.message?.videoMessage);
if (!isMedia || !r.messageTimestamp) return false;