From 50aee19c851ea5f0dc22f90ea0d7b5df2d0e421f Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 2 May 2025 20:23:50 -0600 Subject: [PATCH] separacion de respuesta para mejor handeleo --- nucleo-bot/config.js | 2 +- nucleo-bot/handlers.js | 84 +++++--------------- nucleo-bot/respuestas/respuestaNormal.js | 99 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 66 deletions(-) create mode 100644 nucleo-bot/respuestas/respuestaNormal.js diff --git a/nucleo-bot/config.js b/nucleo-bot/config.js index 7790e26..5d409b0 100644 --- a/nucleo-bot/config.js +++ b/nucleo-bot/config.js @@ -3,7 +3,7 @@ /*───────────────────────────────────────────────────────────────*/ export const config = { - VERSION : '0.6.02', + VERSION : '0.6.12', API_URL : process.env.BOT_API_URL ?? 'http://whatsapp-bot:8002', GROUP_ID : process.env.GROUP_ID ?? '120363203056794284@g.us', REPLY_MSG : process.env.REPLY_MSG ?? 'que pedos', diff --git a/nucleo-bot/handlers.js b/nucleo-bot/handlers.js index 907ef4c..dcb3d1e 100644 --- a/nucleo-bot/handlers.js +++ b/nucleo-bot/handlers.js @@ -1,83 +1,37 @@ // handlers.js import fs from 'fs/promises'; import { log } from './logger.js'; -import { - sendText, - fetchChatMessages, - setTypingStatus -} from './whatsapp.js'; -import { askGemini } from './gemini.js'; +// Ya no se necesitan: sendText, fetchChatMessages, setTypingStatus, askGemini aquí import { processMessage } from './utils/processMessage.js'; -import { saveMedia } from './utils/saveMedia.js'; // ← NUEVO +import { saveMedia } from './utils/saveMedia.js'; +import { respuestaNormal } from './respuesta/respuestaNormal.js'; // <- NUEVA IMPORTACIÓN /* carpeta raíz donde saveMedia deja todo */ const MEDIA_DIR = '/media'; await fs.mkdir(MEDIA_DIR, { recursive: true }); -/* Quita campos pesados antes de mandar a Gemini */ -const cleanForGemini = ({ media, reactions, preview, ...rest }) => rest; +// La función cleanForGemini se movió a respuestaNormal.js export async function processIncoming(raw) { - if (raw.type !== 'chat') await saveMedia(raw); + // Guarda media si no es un mensaje de texto plano + if (raw.type !== 'chat') { + try { + // Nota: saveMedia podría necesitar acceso a MEDIA_DIR si no está codificado internamente + await saveMedia(raw /*, MEDIA_DIR */); // Podrías necesitar pasar MEDIA_DIR si saveMedia lo requiere + } catch (error) { + log('error', 'Error guardando media:', error); + } + } const msg = processMessage(raw); const text = msg.text || ''; /* ----- comando @nucleo ----- */ if (/^@nucleo(\s|$)/i.test(text)) { - setTypingStatus(msg.chatId, true); - log('info', '🧠 Generando respuesta…'); - - /* 1) historial completo del chat */ - const allraw = await fetchChatMessages(msg.chatId); - const allMsgs = allraw.map(processMessage); - - /* 2) recorta contexto a ≤100 kB */ - const context = allMsgs.map(cleanForGemini); - let json = JSON.stringify(context); - while (json.length > 100_000 && context.length) { - context.shift(); - json = JSON.stringify(context); - } - - /* 3) prompt */ - const prompt = [ - `Eres el asistente del grupo "${context[0]?.chatName ?? 'chat'}".`, - `Pregunta del usuario: ${text}` - , ...context]; - - /* 4) procesa medias y respeta el límite de 20 MB */ - const medias = allraw.filter(m => m.type !== 'chat'); - const settled = await Promise.allSettled(medias.map(saveMedia)); - - const MAX = 20 * 1024 * 1024; // 20 MB - let total = 0; - const files = {}; - - for (const r of settled) { - if (r.status !== 'fulfilled' || !r.value) continue; - - const [msgId, obj] = Object.entries(r.value)[0]; - let size = Number(obj.sizeBytes || 0); - - if (!size && obj.filePath) { - try { size = (await fs.stat(obj.filePath)).size; } - catch { size = 0; } - } - - if (total + size > MAX && total > 0) break; - - total += size; - files[msgId] = { uri: obj.uri || obj.filePath, mimeType: obj.mimeType }; - } - - // log('info', '🧠 Enviando a Gemini...', { files }); - - /* 5) llama a Gemini y responde */ - const respuesta = await askGemini(prompt, files ); - await sendText(msg.chatId, respuesta); - - setTypingStatus(msg.chatId, false); - log('info', '🧠 Respuesta enviada'); + // Llama a la función importada + await respuestaNormal(msg); // <- LLAMADA A LA FUNCIÓN EXTERNA + } else { + // Lógica para otros mensajes (si aplica) + // log('debug', 'Mensaje recibido no es comando @nucleo:', text); } -} +} \ No newline at end of file diff --git a/nucleo-bot/respuestas/respuestaNormal.js b/nucleo-bot/respuestas/respuestaNormal.js new file mode 100644 index 0000000..edfe86d --- /dev/null +++ b/nucleo-bot/respuestas/respuestaNormal.js @@ -0,0 +1,99 @@ +// /respuesta/respuestaNormal.js +import fs from 'fs/promises'; +import { log } from '../logger.js'; // <- Nota el '../' +import { + sendText, + fetchChatMessages, + setTypingStatus +} from '../whatsapp.js'; // <- Nota el '../' +import { askGemini } from '../gemini.js'; // <- Nota el '../' +import { processMessage } from '../utils/processMessage.js'; // <- Nota el '../' +import { saveMedia } from '../utils/saveMedia.js'; // <- Nota el '../' + +/* Quita campos pesados antes de mandar a Gemini */ +const cleanForGemini = ({ media, reactions, preview, ...rest }) => rest; + +/** + * Procesa la lógica específica para el comando @nucleo. + * Obtiene el historial, prepara el contexto, procesa media, + * llama a Gemini y envía la respuesta. + * @param {object} msg - El objeto de mensaje procesado. + */ +export async function respuestaNormal(msg) { + const text = msg.text || ''; // Necesitamos el texto original aquí también + + setTypingStatus(msg.chatId, true); + log('info', '🧠 Generando respuesta para @nucleo…'); + + try { + /* 1) historial completo del chat */ + const allraw = await fetchChatMessages(msg.chatId); + const allMsgs = allraw.map(processMessage); + + /* 2) recorta contexto a ≤100 kB */ + const context = allMsgs.map(cleanForGemini); + let json = JSON.stringify(context); + while (json.length > 100_000 && context.length) { + context.shift(); + json = JSON.stringify(context); + } + + // Asegurarse de que el contexto no esté vacío antes de acceder a context[0] + const chatName = context.length > 0 ? context[0]?.chatName : 'chat'; + + /* 3) prompt */ + const prompt = [ + `Eres el asistente del grupo "${chatName}".`, + `Pregunta del usuario: ${text}` // Usamos el 'text' del mensaje original que activó el comando + , ...context]; + + /* 4) procesa medias y respeta el límite de 20 MB */ + // Filtrar solo los mensajes que no son de tipo 'chat' del historial obtenido + const medias = allraw.filter(m => m.type !== 'chat'); + const settled = await Promise.allSettled(medias.map(saveMedia)); + + const MAX = 20 * 1024 * 1024; // 20 MB + let total = 0; + const files = {}; + + for (const r of settled) { + if (r.status !== 'fulfilled' || !r.value) continue; + + if (typeof r.value !== 'object' || r.value === null) continue; + const entries = Object.entries(r.value); + if (entries.length === 0) continue; + const [msgId, obj] = entries[0]; + if (typeof obj !== 'object' || obj === null) continue; + + let size = Number(obj.sizeBytes || 0); + + if (!size && obj.filePath) { + try { size = (await fs.stat(obj.filePath)).size; } + catch { size = 0; } + } + + // Comprobar si añadir este archivo excede el límite MÁXIMO TOTAL + // y si ya tenemos *algo* (total > 0) para evitar empezar con un archivo demasiado grande + if (total + size > MAX && total > 0) break; + // Si este archivo *por sí solo* excede el límite, saltarlo + if (size > MAX) continue; + + total += size; + files[msgId] = { uri: obj.uri || obj.filePath, mimeType: obj.mimeType }; + } + + // log('info', '🧠 Enviando a Gemini...', { files }); + + /* 5) llama a Gemini y responde */ + const respuesta = await askGemini(prompt, files); + await sendText(msg.chatId, respuesta); + + log('info', '🧠 Respuesta enviada'); + + } catch (error) { + log('error', 'Error en respuestaNormal:', error); + await sendText(msg.chatId, 'Hubo un error al procesar tu solicitud.'); + } finally { + setTypingStatus(msg.chatId, false); + } +} \ No newline at end of file