diff --git a/nucleo-bot/config.js b/nucleo-bot/config.js index 5d409b0..7790e26 100644 --- a/nucleo-bot/config.js +++ b/nucleo-bot/config.js @@ -3,7 +3,7 @@ /*───────────────────────────────────────────────────────────────*/ export const config = { - VERSION : '0.6.12', + VERSION : '0.6.02', 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 dcb3d1e..907ef4c 100644 --- a/nucleo-bot/handlers.js +++ b/nucleo-bot/handlers.js @@ -1,37 +1,83 @@ // handlers.js import fs from 'fs/promises'; import { log } from './logger.js'; -// Ya no se necesitan: sendText, fetchChatMessages, setTypingStatus, askGemini aquí +import { + sendText, + fetchChatMessages, + setTypingStatus +} from './whatsapp.js'; +import { askGemini } from './gemini.js'; import { processMessage } from './utils/processMessage.js'; -import { saveMedia } from './utils/saveMedia.js'; -import { respuestaNormal } from './respuesta/respuestaNormal.js'; // <- NUEVA IMPORTACIÓN +import { saveMedia } from './utils/saveMedia.js'; // ← NUEVO /* carpeta raíz donde saveMedia deja todo */ const MEDIA_DIR = '/media'; await fs.mkdir(MEDIA_DIR, { recursive: true }); -// La función cleanForGemini se movió a respuestaNormal.js +/* Quita campos pesados antes de mandar a Gemini */ +const cleanForGemini = ({ media, reactions, preview, ...rest }) => rest; export async function processIncoming(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); - } - } + if (raw.type !== 'chat') await saveMedia(raw); const msg = processMessage(raw); const text = msg.text || ''; /* ----- comando @nucleo ----- */ if (/^@nucleo(\s|$)/i.test(text)) { - // 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); + 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'); } -} \ No newline at end of file +} diff --git a/nucleo-bot/respuestas/respuestaNormal.js b/nucleo-bot/respuestas/respuestaNormal.js deleted file mode 100644 index edfe86d..0000000 --- a/nucleo-bot/respuestas/respuestaNormal.js +++ /dev/null @@ -1,99 +0,0 @@ -// /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