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;
}
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
// 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);
const hasSubstantialText = Boolean(text && text.trim().length >= 10);
if (!hasSubstantialText) {
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 });
}
// 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)
const adminNotificationJid = await getNotificationGroupJid() || '120363428012905103@g.us';
const destinationJid = adminNotificationJid;
// Notificación de Progreso 1: Enviada EXCLUSIVAMENTE a Notificaciones Almeira
const typeInfoText = explicitType ? ` (${explicitType.toUpperCase()})` : '';
sendWhatsAppMessage(
destinationJid,
`📥 *Nueva Información Recibida*\n` +
`📥 *Nueva Información Recibida${typeInfoText}*\n` +
`📍 *Fuente:* ${filterResult.resolvedName}\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));
// 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.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
const parsed = await processWhatsAppMessage(text, candidateMediaList);
// 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, undefined, explicitType);
// Determinar dimensiones según el tipo de publicación
// 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
*/
function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPublication {
function fallbackRuleBasedParser(
text: string,
mediaCount: number,
explicitType?: 'noticia' | 'evento' | 'aviso' | null
): ProcessedPublication {
const clean = (text || '').trim();
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();
let tipo: 'noticia' | 'evento' | 'aviso' = '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')) {
tipo = 'evento';
} else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio')) {
tipo = 'aviso';
let tipo: 'noticia' | 'evento' | 'aviso' = explicitType || 'noticia';
if (!explicitType) {
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')) {
tipo = 'evento';
} 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;
@ -63,7 +69,8 @@ function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPub
export async function processWhatsAppMessage(
text: string,
mediaParam?: string | MediaInput[] | MediaInput,
mimeTypeParam?: string
mimeTypeParam?: string,
explicitType?: 'noticia' | 'evento' | 'aviso' | null
): Promise<ProcessedPublication> {
// Normalize mediaParam into an array of MediaInput
let mediaList: MediaInput[] = [];
@ -77,18 +84,23 @@ export async function processWhatsAppMessage(
if (!process.env.GEMINI_API_KEY) {
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 = `
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.
${explicitTypeDirective}
Debes estructurar el mensaje en base a lo siguiente:
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).
- "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.
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.
@ -152,6 +164,9 @@ export async function processWhatsAppMessage(
if (response.text) {
const result = JSON.parse(response.text) as ProcessedPublication;
if (explicitType) {
result.tipo = explicitType;
}
return result;
}
} 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
console.warn('[Gemini] Modelos de IA agotados por cuota, activando fallback editorial inteligente');
return fallbackRuleBasedParser(text, mediaList.length);
return fallbackRuleBasedParser(text, mediaList.length, explicitType);
}