feat(whatsapp): respetar tipo de publicacion explicito (noticia, evento o publicidad) indicado en el mensaje
This commit is contained in:
parent
1f8498e5a7
commit
bbce2a7f9a
@ -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)
|
||||||
|
|||||||
@ -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,12 +38,14 @@ 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 (!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')) {
|
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';
|
tipo = 'evento';
|
||||||
} else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio')) {
|
} 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';
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user