feat(whatsapp): respetar tipo de publicacion explicito (noticia, evento o publicidad) indicado en el mensaje

This commit is contained in:
AI Bot 2026-08-24 16:36:19 -05:00
parent 1f8498e5a7
commit bbce2a7f9a
2 changed files with 69 additions and 17 deletions

View File

@ -207,11 +207,41 @@ function isDuplicateMessage(chatId: string, text: string): boolean {
return false; return false;
} }
function detectExplicitPublicationType(inputText: string): 'noticia' | 'evento' | 'aviso' | null {
if (!inputText) return null;
const lower = inputText.toLowerCase();
// Detección de palabras clave explícitas enviadas por el usuario:
// Ejemplos: "Noticia: ...", "Evento: ...", "Publicidad: ...", "#noticia", "publicar como evento", etc.
if (
/^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*noticia\b/i.test(lower) ||
/\b(?:publicar\s+como\s+noticia|es\s+una\s+noticia)\b/i.test(lower)
) {
return 'noticia';
}
if (
/^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*evento\b/i.test(lower) ||
/\b(?:publicar\s+como\s+evento|es\s+un\s+evento|agenda\s+cultural|agenda\s+de\s+eventos)\b/i.test(lower)
) {
return 'evento';
}
if (
/^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*(?:publicidad|aviso|clasificado|anuncio)\b/i.test(lower) ||
/\b(?:publicar\s+como\s+publicidad|publicar\s+como\s+aviso|publicar\s+como\s+clasificado|es\s+publicidad|es\s+un\s+aviso)\b/i.test(lower)
) {
return 'aviso';
}
return null;
}
// LOGIC 2: PROCESAR PUBLICACIÓN ÚNICA // LOGIC 2: PROCESAR PUBLICACIÓN ÚNICA
// En WhatsApp, los álbumes de fotos (ej. 25 fotos + 1 texto) llegan como 26 eventos separados. // 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; // 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. // se recolectan como recursos para la publicación principal que contiene el texto de la noticia.
const hasSubstantialText = Boolean(text && text.trim().length >= 15); const hasSubstantialText = Boolean(text && text.trim().length >= 10);
if (!hasSubstantialText) { if (!hasSubstantialText) {
console.log(`[WhatsApp Webhook] Mensaje multimedia sin texto editorial (${text?.length || 0} chars). Registrado para lote de fotos.`); console.log(`[WhatsApp Webhook] Mensaje multimedia sin texto editorial (${text?.length || 0} chars). Registrado para lote de fotos.`);
@ -224,17 +254,24 @@ function isDuplicateMessage(chatId: string, text: string): boolean {
return NextResponse.json({ success: true, message: 'Ignored duplicate text in burst' }, { status: 200 }); return NextResponse.json({ success: true, message: 'Ignored duplicate text in burst' }, { status: 200 });
} }
// Detectar si el usuario definió explícitamente el tipo en el texto (noticia, evento o publicidad/aviso)
const explicitType = detectExplicitPublicationType(text);
if (explicitType) {
console.log(`[WhatsApp Webhook] Tipo de publicación explícito detectado: "${explicitType.toUpperCase()}"`);
}
// Descubrir el grupo central de Notificaciones y Aprobaciones (Notificaciones Almeira) // Descubrir el grupo central de Notificaciones y Aprobaciones (Notificaciones Almeira)
const adminNotificationJid = await getNotificationGroupJid() || '120363428012905103@g.us'; const adminNotificationJid = await getNotificationGroupJid() || '120363428012905103@g.us';
const destinationJid = adminNotificationJid; const destinationJid = adminNotificationJid;
// Notificación de Progreso 1: Enviada EXCLUSIVAMENTE a Notificaciones Almeira // Notificación de Progreso 1: Enviada EXCLUSIVAMENTE a Notificaciones Almeira
const typeInfoText = explicitType ? ` (${explicitType.toUpperCase()})` : '';
sendWhatsAppMessage( sendWhatsAppMessage(
destinationJid, destinationJid,
`📥 *Nueva Información Recibida*\n` + `📥 *Nueva Información Recibida${typeInfoText}*\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` +
`🤖 Recolectando lote de imágenes y analizando contenido...` `🤖 Recolectando lote de imágenes y estructurando 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 + álbum de fotos reciente del chat) // Recolectamos todas las imágenes/multimedia candidatas (mensaje actual + álbum de fotos reciente del chat)
@ -260,10 +297,10 @@ function isDuplicateMessage(chatId: string, text: string): boolean {
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 del álbum con Gemini...`); console.log(`[WhatsApp Webhook] Procesando ${candidateMediaList.length} imágenes del álbum con Gemini (Tipo forzado: ${explicitType || 'Auto'})...`);
// 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 respetando el tipo explícito si existe
const parsed = await processWhatsAppMessage(text, candidateMediaList); const parsed = await processWhatsAppMessage(text, candidateMediaList, undefined, explicitType);
// Determinar dimensiones según el tipo de publicación // Determinar dimensiones según el tipo de publicación
// Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1) // Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1)

View File

@ -23,7 +23,11 @@ export interface ProcessedPublication {
/** /**
* Fallback parser in case Gemini API is rate-limited (429) or unavailable * Fallback parser in case Gemini API is rate-limited (429) or unavailable
*/ */
function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPublication { function fallbackRuleBasedParser(
text: string,
mediaCount: number,
explicitType?: 'noticia' | 'evento' | 'aviso' | null
): ProcessedPublication {
const clean = (text || '').trim(); const clean = (text || '').trim();
const lines = clean.split('\n').map(l => l.trim()).filter(Boolean); const lines = clean.split('\n').map(l => l.trim()).filter(Boolean);
@ -34,11 +38,13 @@ function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPub
} }
const lower = clean.toLowerCase(); const lower = clean.toLowerCase();
let tipo: 'noticia' | 'evento' | 'aviso' = 'noticia'; let tipo: 'noticia' | 'evento' | 'aviso' = explicitType || 'noticia';
if (lower.includes('evento') || lower.includes('invitación') || lower.includes('invitacion') || lower.includes('jornada') || lower.includes('taller') || lower.includes('festival') || lower.includes('feria') || lower.includes('convocatoria')) { if (!explicitType) {
tipo = 'evento'; if (lower.includes('evento') || lower.includes('invitación') || lower.includes('invitacion') || lower.includes('jornada') || lower.includes('taller') || lower.includes('festival') || lower.includes('feria') || lower.includes('convocatoria')) {
} else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio')) { tipo = 'evento';
tipo = 'aviso'; } else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio') || lower.includes('publicidad')) {
tipo = 'aviso';
}
} }
const seo = clean.replace(/\n+/g, ' ').substring(0, 150) || rawTitle; const seo = clean.replace(/\n+/g, ' ').substring(0, 150) || rawTitle;
@ -63,7 +69,8 @@ function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPub
export async function processWhatsAppMessage( export async function processWhatsAppMessage(
text: string, text: string,
mediaParam?: string | MediaInput[] | MediaInput, mediaParam?: string | MediaInput[] | MediaInput,
mimeTypeParam?: string mimeTypeParam?: string,
explicitType?: 'noticia' | 'evento' | 'aviso' | null
): Promise<ProcessedPublication> { ): Promise<ProcessedPublication> {
// Normalize mediaParam into an array of MediaInput // Normalize mediaParam into an array of MediaInput
let mediaList: MediaInput[] = []; let mediaList: MediaInput[] = [];
@ -77,18 +84,23 @@ export async function processWhatsAppMessage(
if (!process.env.GEMINI_API_KEY) { if (!process.env.GEMINI_API_KEY) {
console.warn('[Gemini] Sin GEMINI_API_KEY configurada, usando parser de respaldo'); console.warn('[Gemini] Sin GEMINI_API_KEY configurada, usando parser de respaldo');
return fallbackRuleBasedParser(text, mediaList.length); return fallbackRuleBasedParser(text, mediaList.length, explicitType);
} }
const explicitTypeDirective = explicitType
? `\n IMPORTANTE: El usuario ha definido explícitamente que la categoría DEBE SER "${explicitType}". Debes asignar tipo: "${explicitType}" obligatoriamente.\n`
: '';
const systemInstruction = ` const systemInstruction = `
Eres un asistente editorial experto y editor gráfico de Almeira Web. Eres un asistente editorial experto y editor gráfico de Almeira Web.
Analiza los mensajes y recursos multimedia recibidos desde WhatsApp para estructurarlos como publicaciones del portal. Analiza los mensajes y recursos multimedia recibidos desde WhatsApp para estructurarlos como publicaciones del portal.
${explicitTypeDirective}
Debes estructurar el mensaje en base a lo siguiente: Debes estructurar el mensaje en base a lo siguiente:
1. titulo: Asigna un título claro, profesional y conciso (máx. 70 caracteres). 1. titulo: Asigna un título claro, profesional y conciso (máx. 70 caracteres).
2. tipo: Determina la categoría exacta: 2. tipo: Determina la categoría exacta ${explicitType ? `(Usa obligatoriamente "${explicitType}")` : ''}:
- "noticia": Noticias generales, política, actualidad, comunicados y reportajes (dimensiones recomendadas de imagen: 640x360 px, relación 16:9). - "noticia": Noticias generales, política, actualidad, comunicados y reportajes (dimensiones recomendadas de imagen: 640x360 px, relación 16:9).
- "evento": Invitaciones a actividades, jornadas, ferias, foros, congresos, festivales o convocatorias con fecha (dimensiones de imagen: 640x640 px, relación 1:1). - "evento": Invitaciones a actividades, jornadas, ferias, foros, congresos, festivales o convocatorias con fecha (dimensiones de imagen: 640x640 px, relación 1:1).
- "aviso": Clasificados, ofertas de empleo, compra/venta, servicios profesionales, comunicados comunitarios (dimensiones de imagen: 640x640 px, relación 1:1). - "aviso": Publicidad comercial, clasificados, ofertas de empleo, compra/venta, servicios profesionales, comunicados comunitarios (dimensiones de imagen: 640x640 px, relación 1:1).
3. seo_descripcion: Un resumen conciso de máximo 150 caracteres optimizado para redes y motores de búsqueda. 3. seo_descripcion: Un resumen conciso de máximo 150 caracteres optimizado para redes y motores de búsqueda.
4. contenido: El texto detallado de la publicación en HTML válido y limpio (usa <p>, <strong>, <ul>/<li> si es apropiado), preservando nombres, fechas, contactos y enlaces originales. 4. contenido: El texto detallado de la publicación en HTML válido y limpio (usa <p>, <strong>, <ul>/<li> si es apropiado), preservando nombres, fechas, contactos y enlaces originales.
5. fecha_vencimiento: Si el texto indica fecha límite del evento o del aviso (Ej. "hasta el 30 de noviembre", o "el próximo sábado"), devuélvela en formato YYYY-MM-DD. Si no aplica o es una noticia general, pasa null. 5. fecha_vencimiento: Si el texto indica fecha límite del evento o del aviso (Ej. "hasta el 30 de noviembre", o "el próximo sábado"), devuélvela en formato YYYY-MM-DD. Si no aplica o es una noticia general, pasa null.
@ -152,6 +164,9 @@ export async function processWhatsAppMessage(
if (response.text) { if (response.text) {
const result = JSON.parse(response.text) as ProcessedPublication; const result = JSON.parse(response.text) as ProcessedPublication;
if (explicitType) {
result.tipo = explicitType;
}
return result; return result;
} }
} catch (err: any) { } catch (err: any) {
@ -161,5 +176,5 @@ export async function processWhatsAppMessage(
// Si todos los modelos de Gemini superaron cuota (429), usamos el fallback seguro para no fallar el webhook // Si todos los modelos de Gemini superaron cuota (429), usamos el fallback seguro para no fallar el webhook
console.warn('[Gemini] Modelos de IA agotados por cuota, activando fallback editorial inteligente'); console.warn('[Gemini] Modelos de IA agotados por cuota, activando fallback editorial inteligente');
return fallbackRuleBasedParser(text, mediaList.length); return fallbackRuleBasedParser(text, mediaList.length, explicitType);
} }