Configurcion de personalidad
This commit is contained in:
55
mvp/Whatsapp-bot/src/whatsapp/whatsapp-debounce.service.ts
Normal file
55
mvp/Whatsapp-bot/src/whatsapp/whatsapp-debounce.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user