Configurcion de personalidad

This commit is contained in:
unknown
2026-06-02 23:07:34 -04:00
parent 7b0eb31f56
commit fc6a7044b0
8 changed files with 346 additions and 94 deletions

View File

@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class WhatsappDebounceService {
private pendingMessages: Map<
string,
{
timer: NodeJS.Timeout;
texts: string[];
}
> = new Map();
private readonly DEBOUNCE_MS = 3000;
/**
* Agrega un mensaje al buffer del usuario.
* @param userId Identificador único del usuario (ej. JID)
* @param messageText Texto del mensaje
* @param callback Función a ejecutar cuando se complete el debounce (recibe el texto combinado)
*/
async add(
userId: string,
messageText: string,
callback: (combinedMessage: string) => Promise<void>,
): Promise<void> {
if (this.pendingMessages.has(userId)) {
const pending = this.pendingMessages.get(userId)!;
clearTimeout(pending.timer);
pending.texts.push(messageText);
pending.timer = setTimeout(async () => {
await this.flush(userId, callback);
}, this.DEBOUNCE_MS);
} else {
this.pendingMessages.set(userId, {
timer: setTimeout(async () => {
await this.flush(userId, callback);
}, this.DEBOUNCE_MS),
texts: [messageText],
});
}
}
private async flush(
userId: string,
callback: (combinedMessage: string) => Promise<void>,
): Promise<void> {
const pending = this.pendingMessages.get(userId);
if (!pending) return;
this.pendingMessages.delete(userId);
const combinedMessage = pending.texts.join(' ');
await callback(combinedMessage);
}
}

View File

@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { WhatsappService } from './whatsapp.service';
import { WhatsappDebounceService } from './whatsapp-debounce.service';
import { LeadsModule } from '../leads/leads.module';
import { ConversacionModule } from '../conversacion/conversacion.module';
import { ClaudeModule } from '../claude/claude.module';
@@ -7,7 +8,7 @@ import { MediaModule } from '../media/media.module';
@Module({
imports: [LeadsModule, ConversacionModule, ClaudeModule, MediaModule],
providers: [WhatsappService],
providers: [WhatsappService, WhatsappDebounceService],
exports: [WhatsappService],
})
export class WhatsappModule {}

View File

@@ -10,6 +10,7 @@ import makeWASocket, {
fetchLatestBaileysVersion,
WASocket,
downloadMediaMessage,
normalizeMessageContent,
} from "@whiskeysockets/baileys";
import { Boom } from "@hapi/boom";
import * as path from "path";
@@ -19,6 +20,7 @@ import { LeadsService } from "../leads/leads.service";
import { ConversacionService } from "../conversacion/conversacion.service";
import { ClaudeService } from "../claude/claude.service";
import { MediaService } from "../media/media.service";
import { WhatsappDebounceService } from "./whatsapp-debounce.service";
import { wrapSocket } from "baileys-antiban";
const ESTADOS_TERMINALES = [
@@ -34,12 +36,15 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(WhatsappService.name);
private sock: WASocket | null = null;
private authDir = path.join(process.cwd(), "auth_info_baileys");
private readonly ultimoMsgPorJid = new Map<string, any>();
private baileysLogger = pino({ level: "info" });
constructor(
private readonly leadsService: LeadsService,
private readonly conversacionService: ConversacionService,
private readonly claudeService: ClaudeService,
private readonly mediaService: MediaService,
private readonly debounceService: WhatsappDebounceService,
) {}
async onModuleInit() {
@@ -71,11 +76,13 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
const { version } = await fetchLatestBaileysVersion();
this.baileysLogger = pino({ level: "info" }) as any;
this.sock = makeWASocket({
version,
auth: state,
printQRInTerminal: false,
logger: pino({ level: "info" }) as any,
logger: this.baileysLogger,
markOnlineOnConnect: false,
generateHighQualityLinkPreview: false,
syncFullHistory: false,
@@ -132,11 +139,49 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
continue;
}
await this.procesarMensaje(msg);
await this.encolarMensaje(msg);
}
});
}
private extraerTextoPlano(msg: any): string | null {
const msgContent = msg.message;
if (!msgContent) return null;
if (msgContent.conversation || msgContent.extendedTextMessage) {
const texto =
msgContent.conversation || msgContent.extendedTextMessage?.text || "";
return texto.trim() ? texto : null;
}
return null;
}
private crearMsgConTexto(msg: any, texto: string): any {
return {
...msg,
message: { conversation: texto },
};
}
private async encolarMensaje(msg: any): Promise<void> {
const jid = msg.key.remoteJid!;
const textoPlano = this.extraerTextoPlano(msg);
if (textoPlano === null) {
await this.procesarMensaje(msg);
return;
}
this.ultimoMsgPorJid.set(jid, msg);
await this.debounceService.add(jid, textoPlano, async (combinedMessage) => {
const baseMsg = this.ultimoMsgPorJid.get(jid) ?? msg;
this.ultimoMsgPorJid.delete(jid);
await this.procesarMensaje(this.crearMsgConTexto(baseMsg, combinedMessage));
});
}
private async procesarMensaje(msg: any): Promise<void> {
const jid = msg.key.remoteJid!;
@@ -155,7 +200,7 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
}
let textoNormalizado = "";
const msgContent = msg.message;
const msgContent = normalizeMessageContent(msg.message);
if (!msgContent) return;
@@ -163,24 +208,62 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
textoNormalizado =
msgContent.conversation || msgContent.extendedTextMessage?.text || "";
} else if (msgContent.audioMessage) {
const audioMeta = msgContent.audioMessage;
const mimeType = audioMeta.mimetype || "audio/ogg; codecs=opus";
this.logger.log(
`Audio recibido de lead id=${lead.id}. Transcribiendo...`,
`[AUDIO 1/4] Recibido lead=${lead.id}, ptt=${audioMeta.ptt ?? false}, seconds=${audioMeta.seconds ?? "?"}, mimetype=${mimeType}, fileLength=${audioMeta.fileLength ?? "?"}, url=${audioMeta.url ? "si" : "no"}`,
);
const buffer = await downloadMediaMessage(msg as any, "buffer", {});
const mimeType =
msgContent.audioMessage.mimetype || "audio/ogg; codecs=opus";
if (!this.sock) {
this.logger.error("[AUDIO 1/4] Socket no disponible para descargar audio");
return;
}
const buffer = await downloadMediaMessage(
msg as any,
"buffer",
{},
{
logger: this.baileysLogger,
reuploadRequest: this.sock.updateMediaMessage,
},
);
const audioBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
const magicHex = audioBuffer.subarray(0, 4).toString("hex");
this.logger.log(
`[AUDIO 1/4] Buffer descargado — size=${audioBuffer.length} bytes, magic_hex=${magicHex}, esperado_ogg=4f676753`,
);
textoNormalizado = await this.mediaService.transcribirAudio(
buffer as Buffer,
audioBuffer,
mimeType,
);
this.logger.log(
`[AUDIO 1/4] Transcripcion recibida en procesarMensaje — "${textoNormalizado.slice(0, 200).replace(/\n/g, "\\n")}"`,
);
} else if (msgContent.imageMessage) {
this.logger.log(
`Imagen recibida de lead id=${lead.id}. Analizando con Vision...`,
);
const buffer = await downloadMediaMessage(msg as any, "buffer", {});
if (!this.sock) return;
const buffer = await downloadMediaMessage(
msg as any,
"buffer",
{},
{
logger: this.baileysLogger,
reuploadRequest: this.sock.updateMediaMessage,
},
);
const mimeType = msgContent.imageMessage.mimetype || "image/jpeg";
textoNormalizado = await this.mediaService.inferirImagen(
buffer as Buffer,
Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer),
mimeType,
lead.estado_actual,
);