Merge pull request #1508 from gomessguii/develop

refactor: improve chatbot integrations
This commit is contained in:
Davidson Gomes 2025-05-27 18:03:44 -03:00 committed by GitHub
commit 3500fbe27f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 887 additions and 1884 deletions

File diff suppressed because one or more lines are too long

381
manager/dist/assets/index-D-oOjDYe.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Evolution Manager</title>
<script type="module" crossorigin src="/assets/index-mxi8bQ4k.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css">
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
</head>
<body>
<div id="root"></div>

View File

@ -165,7 +165,7 @@ export class EvolutionStartupService extends ChannelStartupService {
openAiDefaultSettings.speechToText &&
received?.message?.audioMessage
) {
messageRaw.message.speechToText = await this.openaiService.speechToText(received);
messageRaw.message.speechToText = await this.openaiService.speechToText(received, this);
}
}

View File

@ -1298,7 +1298,7 @@ export class BaileysStartupService extends ChannelStartupService {
});
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
messageRaw.message.speechToText = await this.openaiService.speechToText(received);
messageRaw.message.speechToText = await this.openaiService.speechToText(received, this);
}
}
@ -2324,7 +2324,7 @@ export class BaileysStartupService extends ChannelStartupService {
});
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
messageRaw.message.speechToText = await this.openaiService.speechToText(messageRaw);
messageRaw.message.speechToText = await this.openaiService.speechToText(messageRaw, this);
}
}

View File

@ -353,20 +353,25 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
? pushName
: null;
session = (
await this.createNewSession(
{
instanceName: instance.instanceName,
instanceId: instance.instanceId,
},
{
remoteJid,
pushName: pushNameValue,
botId: (bot as any).id,
},
this.getBotType(),
)
)?.session;
const sessionResult = await this.createNewSession(
{
instanceName: instance.instanceName,
instanceId: instance.instanceId,
},
{
remoteJid,
pushName: pushNameValue,
botId: (bot as any).id,
},
this.getBotType(),
);
if (!sessionResult || !sessionResult.session) {
this.logger.error('Failed to create new session');
return;
}
session = sessionResult.session;
}
// Update session status to opened

View File

@ -80,7 +80,7 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
}
}
// Bots
// Override createBot to add Dify-specific validation
public async createBot(instance: InstanceDto, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
@ -92,7 +92,7 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
})
.then((instance) => instance.id);
// Check for Dify-specific duplicate
// Dify-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
@ -106,62 +106,10 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
throw new Error('Dify already exists');
}
// Let the base class handle the rest of the bot creation process
// Let the base class handle the rest
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
}
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return bot;
}
// Process Dify-specific bot logic
protected async processBot(
instance: any,
@ -173,6 +121,6 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
pushName?: string,
msg?: any,
) {
this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -3,12 +3,11 @@ import { $Enums } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class DifyDto extends BaseChatbotDto {
// Dify specific fields
botType?: $Enums.DifyBotType;
apiUrl?: string;
apiKey?: string;
}
export class DifySettingDto extends BaseChatbotSettingDto {
// Dify specific fields
difyIdFallback?: string;
}

View File

@ -1,10 +1,8 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService, HttpServer } from '@config/env.config';
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service';
@ -15,8 +13,8 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
constructor(
waMonitor: WAMonitoringService,
configService: ConfigService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'DifyService', configService);
@ -30,10 +28,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
return 'dify';
}
public async createNewSession(instance: InstanceDto, data: any) {
return super.createNewSession(instance, data, 'dify');
}
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
@ -43,10 +37,29 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
pushName: string,
content: string,
msg?: any,
) {
): Promise<void> {
try {
let endpoint: string = dify.apiUrl;
if (!endpoint) {
this.logger.error('No Dify endpoint defined');
return;
}
// Handle audio messages - transcribe using OpenAI Whisper
let processedContent = content;
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
processedContent = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
}
}
if (dify.botType === 'chatBot') {
endpoint += '/chat-messages';
const payload: any = {
@ -57,7 +70,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
query: content,
query: processedContent,
response_mode: 'blocking',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
@ -66,7 +79,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
@ -77,22 +89,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
payload.query = contentSplit[2] || content;
}
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
if (transcription) {
payload.query = transcription;
} else {
payload.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
payload.query = '[Audio message could not be transcribed]';
}
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
@ -110,7 +106,9 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
const message = response?.data?.answer;
const conversationId = response?.data?.conversation_id;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
@ -128,7 +126,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
endpoint += '/completion-messages';
const payload: any = {
inputs: {
query: content,
query: processedContent,
pushName: pushName,
remoteJid: remoteJid,
instanceName: instance.instanceName,
@ -143,7 +141,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
@ -154,22 +151,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
payload.inputs.query = contentSplit[2] || content;
}
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
if (transcription) {
payload.inputs.query = transcription;
} else {
payload.inputs.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
payload.inputs.query = '[Audio message could not be transcribed]';
}
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
@ -187,7 +168,9 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
const message = response?.data?.answer;
const conversationId = response?.data?.conversation_id;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
@ -211,7 +194,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
query: content,
query: processedContent,
response_mode: 'streaming',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
@ -220,7 +203,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
@ -231,22 +213,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
payload.query = contentSplit[2] || content;
}
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
if (transcription) {
payload.query = transcription;
} else {
payload.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
payload.query = '[Audio message could not be transcribed]';
}
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
@ -262,7 +228,6 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
let answer = '';
const data = response.data.replaceAll('data: ', '');
const events = data.split('\n').filter((line) => line.trim() !== '');
for (const eventString of events) {
@ -280,7 +245,9 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
if (answer) {
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
@ -298,288 +265,4 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
return;
}
}
protected async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclamation, altText, url] = match;
const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(textBuffer.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
textBuffer = '';
if (mediaType === 'image') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'image',
media: url,
});
} else if (mediaType === 'video') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'video',
media: url,
});
} else if (mediaType === 'audio') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'audio',
media: url,
});
} else if (mediaType === 'document') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'document',
media: url,
fileName: altText || 'file',
});
}
} else {
textBuffer += `[${altText}](${url})`;
}
lastIndex = match.index + fullMatch.length;
}
const remainingText = message.slice(lastIndex);
if (remainingText) {
textBuffer += remainingText;
}
if (textBuffer.trim()) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(textBuffer.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
}
protected async initNewSession(
instance: any,
remoteJid: string,
dify: Dify,
settings: DifySetting,
session: IntegrationSession,
content: string,
pushName?: string,
msg?: any,
) {
try {
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
return;
}
}
public async process(
instance: any,
remoteJid: string,
dify: Dify,
session: IntegrationSession,
settings: DifySetting,
content: string,
pushName?: string,
msg?: any,
) {
try {
// Handle keyword finish
if (settings?.keywordFinish?.includes(content.toLowerCase())) {
if (settings?.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.delete({
where: {
id: session.id,
},
});
}
await sendTelemetry('/dify/session/finish');
return;
}
// If session is new or doesn't exist
if (!session) {
const data = {
remoteJid,
pushName,
botId: dify.id,
};
const createSession = await this.createNewSession(
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
data,
);
await this.initNewSession(instance, remoteJid, dify, settings, createSession.session, content, pushName, msg);
await sendTelemetry('/dify/session/start');
return;
}
// If session exists but is paused
if (session.status === 'paused') {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
return;
}
// Regular message for ongoing session
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
return;
}
}
}

View File

@ -77,7 +77,7 @@ export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto>
}
}
// Bots
// Override createBot to add EvoAI-specific validation
public async createBot(instance: InstanceDto, data: EvoaiDto) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
@ -89,6 +89,7 @@ export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto>
})
.then((instance) => instance.id);
// EvoAI-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
@ -101,61 +102,10 @@ export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto>
throw new Error('Evoai already exists');
}
// Let the base class handle the rest
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
}
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Evoai not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Evoai not found');
}
return bot;
}
// Process Evoai-specific bot logic
protected async processBot(
instance: any,

View File

@ -1,11 +1,10 @@
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class EvoaiDto extends BaseChatbotDto {
// Evoai specific fields
agentUrl?: string;
apiKey?: string;
}
export class EvoaiSettingDto extends BaseChatbotSettingDto {
// Evoai specific fields
evoaiIdFallback?: string;
}

View File

@ -1,8 +1,7 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService } from '@config/env.config';
import { ConfigService, HttpServer } from '@config/env.config';
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
@ -10,12 +9,18 @@ import { v4 as uuidv4 } from 'uuid';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
private openaiService: OpenaiService;
constructor(waMonitor: WAMonitoringService, prismaRepository: PrismaRepository, configService: ConfigService) {
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'EvoaiService', configService);
this.openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
this.openaiService = openaiService;
}
/**
@ -25,52 +30,10 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
return 'evoai';
}
public async createNewSession(instance: InstanceDto, data: any) {
return super.createNewSession(instance, data, 'evoai');
}
/**
* Override the process method to directly handle audio messages
* Implement the abstract method to send message to EvoAI API
* Handles audio transcription, image processing, and complex JSON-RPC payload
*/
public async process(
instance: any,
remoteJid: string,
bot: Evoai,
session: IntegrationSession,
settings: EvoaiSetting,
content: string,
pushName?: string,
msg?: any,
): Promise<void> {
try {
this.logger.debug(`[EvoAI] Processing message with custom process method`);
let contentProcessed = content;
// Check if this is an audio message that we should try to transcribe
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
if (transcription) {
contentProcessed = transcription;
} else {
contentProcessed = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`);
contentProcessed = '[Audio message could not be transcribed]';
}
}
// For non-audio messages or if transcription failed, proceed normally
return super.process(instance, remoteJid, bot, session, settings, contentProcessed, pushName, msg);
} catch (error) {
this.logger.error(`[EvoAI] Error in process: ${error}`);
return;
}
}
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
@ -80,19 +43,40 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
pushName: string,
content: string,
msg?: any,
) {
): Promise<void> {
try {
this.logger.debug(`[EvoAI] Sending message to bot with content: ${content}`);
let processedContent = content;
// Handle audio messages - transcribe using OpenAI Whisper
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
processedContent = transcription;
}
} catch (err) {
this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`);
}
}
const endpoint: string = evoai.agentUrl;
if (!endpoint) {
this.logger.error('No EvoAI endpoint defined');
return;
}
const callId = `req-${uuidv4().substring(0, 8)}`;
const messageId = uuidv4();
const messageId = msg?.key?.id || uuidv4();
// Prepare message parts
const parts = [
{
type: 'text',
text: content,
text: processedContent,
},
];
@ -130,6 +114,17 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
role: 'user',
parts,
messageId: messageId,
metadata: {
messageKey: msg?.key,
},
},
metadata: {
remoteJid: remoteJid,
pushName: pushName,
fromMe: msg?.key?.fromMe,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
},
};
@ -177,22 +172,10 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
}
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
const conversationId = session.sessionId;
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
sessionId: conversationId,
},
});
} catch (error) {
this.logger.error(
`[EvoAI] Error sending message: ${error?.response?.data ? JSON.stringify(error.response.data) : error}`,

View File

@ -71,7 +71,7 @@ export const evoaiSettingSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
evoaiIdFallback: { type: 'string' },
botIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},

View File

@ -1,38 +1,10 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class EvolutionBotDto extends BaseChatbotDto {
apiUrl: string;
apiKey: string;
enabled?: boolean;
expire?: number;
keywordFinish?: string | null;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}
export class EvolutionBotSettingDto extends BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string | null;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
botIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}

View File

@ -2,7 +2,7 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { ConfigService, HttpServer } from '@config/env.config';
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
@ -15,8 +15,8 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
constructor(
waMonitor: WAMonitoringService,
configService: ConfigService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'EvolutionBotService', configService);
@ -62,15 +62,12 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.query = transcription;
} else {
payload.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
payload.query = '[Audio message could not be transcribed]';
}
}
@ -91,6 +88,13 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
const endpoint = bot.apiUrl;
if (!endpoint) {
this.logger.error('No Evolution Bot endpoint defined');
return;
}
let headers: any = {
'Content-Type': 'application/json',
};
@ -102,7 +106,7 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
};
}
const response = await axios.post(bot.apiUrl, payload, {
const response = await axios.post(endpoint, payload, {
headers,
});

View File

@ -1,13 +1,16 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Flowise } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Flowise, IntegrationSession } from '@prisma/client';
import { BadRequestException } from '@exceptions';
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
import { BaseChatbotController } from '../../base-chatbot.controller';
import { FlowiseDto } from '../dto/flowise.dto';
import { FlowiseService } from '../services/flowise.service';
export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto> {
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
constructor(
private readonly flowiseService: FlowiseService,
prismaRepository: PrismaRepository,
@ -23,14 +26,12 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
public readonly logger = new Logger('FlowiseController');
protected readonly integrationName = 'Flowise';
integrationEnabled = true; // Set to true by default or use config value if available
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Implementation of abstract methods required by BaseChatbotController
protected getFallbackBotId(settings: any): string | undefined {
return settings?.flowiseIdFallback;
}
@ -50,7 +51,6 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
};
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
@ -58,13 +58,10 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
id: { not: botId },
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
@ -76,16 +73,46 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
}
}
// Process bot-specific logic
// Process Flowise-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: Flowise,
bot: FlowiseModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.flowiseService.process(instance, remoteJid, bot, session, settings, content, pushName);
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
// Override createBot to add module availability check and Flowise-specific validation
public async createBot(instance: InstanceDto, data: FlowiseDto) {
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
// Flowise-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Flowise already exists');
}
// Let the base class handle the rest
return super.createBot(instance, data);
}
}

View File

@ -1,39 +1,10 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class FlowiseDto extends BaseChatbotDto {
apiUrl: string;
apiKey: string;
description: string;
keywordFinish?: string | null;
triggerType: TriggerType;
enabled?: boolean;
expire?: number;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
apiKey?: string;
}
export class FlowiseSettingDto extends BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string | null;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
flowiseIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}

View File

@ -2,133 +2,135 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import { ConfigService, HttpServer } from '@config/env.config';
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class FlowiseService extends BaseChatbotService<Flowise, FlowiseSetting> {
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
private openaiService: OpenaiService;
constructor(
waMonitor: WAMonitoringService,
configService: ConfigService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'FlowiseService', configService);
this.openaiService = openaiService;
}
/**
* Get the bot type identifier
*/
// Return the bot type for Flowise
protected getBotType(): string {
return 'flowise';
}
/**
* Send a message to the Flowise API
*/
// Process Flowise-specific bot logic
public async processBot(
instance: any,
remoteJid: string,
bot: FlowiseModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
// Implement the abstract method to send message to Flowise API
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: FlowiseSetting,
bot: Flowise,
settings: any,
bot: FlowiseModel,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void> {
try {
const payload: any = {
question: content,
overrideConfig: {
sessionId: remoteJid,
vars: {
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
const payload: any = {
question: content,
overrideConfig: {
sessionId: remoteJid,
vars: {
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
};
},
};
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
if (transcription) {
payload.query = transcription;
} else {
payload.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
payload.query = '[Audio message could not be transcribed]';
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.question = transcription;
}
} catch (err) {
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
}
}
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.uploads = [
{
data: contentSplit[1].split('?')[0],
type: 'url',
name: 'Flowise.png',
mime: 'image/png',
},
];
payload.question = contentSplit[2] || content;
}
payload.uploads = [
{
data: contentSplit[1].split('?')[0],
type: 'url',
name: 'Flowise.png',
mime: 'image/png',
},
];
payload.question = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
let headers: any = {
'Content-Type': 'application/json',
let headers: any = {
'Content-Type': 'application/json',
};
if (bot.apiKey) {
headers = {
...headers,
Authorization: `Bearer ${bot.apiKey}`,
};
}
if (bot.apiKey) {
headers = {
...headers,
Authorization: `Bearer ${bot.apiKey}`,
};
}
const endpoint = bot.apiUrl;
const endpoint = bot.apiUrl;
if (!endpoint) {
this.logger.error('No Flowise endpoint defined');
return;
}
const response = await axios.post(endpoint, payload, {
headers,
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
const message = response?.data?.text;
if (message) {
// Use the base class method to send the message to WhatsApp
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
// Send telemetry
sendTelemetry('/message/sendText');
} catch (error) {
this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`);
if (!endpoint) {
this.logger.error('No Flowise endpoint defined');
return;
}
const response = await axios.post(endpoint, payload, {
headers,
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
const message = response?.data?.text;
if (message) {
// Use the base class method to send the message to WhatsApp
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
}
// The service is now complete with just the abstract method implementations
}

View File

@ -40,6 +40,8 @@ export const flowiseSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: ['enabled', 'apiUrl', 'triggerType'],
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
@ -69,7 +71,9 @@ export const flowiseSettingSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' },
flowiseIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: [
'expire',

View File

@ -110,58 +110,6 @@ export class N8nController extends BaseChatbotController<N8nModel, N8nDto> {
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('N8n is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
}
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('N8n is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('N8n not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('N8n not found');
}
return bot;
}
// Process N8n-specific bot logic
protected async processBot(
instance: any,
@ -173,6 +121,7 @@ export class N8nController extends BaseChatbotController<N8nModel, N8nDto> {
pushName?: string,
msg?: any,
) {
this.n8nService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
// Use the base class pattern instead of calling n8nService.process directly
await this.n8nService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -1,5 +1,3 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class N8nDto extends BaseChatbotDto {
@ -7,26 +5,10 @@ export class N8nDto extends BaseChatbotDto {
webhookUrl?: string;
basicAuthUser?: string;
basicAuthPass?: string;
// Advanced bot properties (copied from DifyDto style)
triggerType: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
ignoreJids?: string[];
splitMessages?: boolean;
timePerChar?: number;
}
export class N8nSettingDto extends BaseChatbotSettingDto {
// N8n specific fields
// N8n has no specific fields
}
export class N8nMessageDto {

View File

@ -1,14 +1,12 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { ConfigService, HttpServer } from '@config/env.config';
import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
import { N8nDto } from '../dto/n8n.dto';
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
private openaiService: OpenaiService;
@ -29,92 +27,6 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
return 'n8n';
}
/**
* Create a new N8n bot for the given instance.
*/
public async createBot(instanceId: string, data: N8nDto) {
try {
return await this.prismaRepository.n8n.create({
data: {
enabled: data.enabled ?? true,
description: data.description,
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
instanceId,
},
});
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Find all N8n bots for the given instance.
*/
public async findBots(instanceId: string) {
try {
return await this.prismaRepository.n8n.findMany({ where: { instanceId } });
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Fetch a specific N8n bot by ID and instance.
*/
public async fetchBot(instanceId: string, n8nId: string) {
try {
const bot = await this.prismaRepository.n8n.findFirst({ where: { id: n8nId } });
if (!bot || bot.instanceId !== instanceId) throw new Error('N8n bot not found');
return bot;
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Update a specific N8n bot.
*/
public async updateBot(instanceId: string, n8nId: string, data: N8nDto) {
try {
await this.fetchBot(instanceId, n8nId);
return await this.prismaRepository.n8n.update({
where: { id: n8nId },
data: {
enabled: data.enabled,
description: data.description,
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
},
});
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Delete a specific N8n bot.
*/
public async deleteBot(instanceId: string, n8nId: string) {
try {
await this.fetchBot(instanceId, n8nId);
return await this.prismaRepository.n8n.delete({ where: { id: n8nId } });
} catch (error) {
this.logger.error(error);
throw error;
}
}
public async createNewSession(instance: InstanceDto, data: any) {
return super.createNewSession(instance, data, 'n8n');
}
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
@ -126,6 +38,11 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
msg?: any,
) {
try {
if (!session) {
this.logger.error('Session is null in sendMessageToBot');
return;
}
const endpoint: string = n8n.webhookUrl;
const payload: any = {
chatInput: content,
@ -142,15 +59,12 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.chatInput = transcription;
} else {
payload.chatInput = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[N8n] Failed to transcribe audio: ${err}`);
payload.chatInput = '[Audio message could not be transcribed]';
}
}
@ -161,7 +75,10 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
}
const response = await axios.post(endpoint, payload, { headers });
const message = response?.data?.output || response?.data?.answer;
// Use base class method instead of custom implementation
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
@ -176,277 +93,4 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
return;
}
}
protected async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: N8nSetting) {
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclamation, altText, url] = match;
const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index).trim();
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(textBuffer.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
textBuffer = '';
if (mediaType === 'image') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'image',
media: url,
});
} else if (mediaType === 'video') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'video',
media: url,
});
} else if (mediaType === 'audio') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'audio',
media: url,
});
} else if (mediaType === 'document') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'document',
media: url,
fileName: altText || 'file',
});
}
} else {
textBuffer += `[${altText}](${url})`;
}
lastIndex = match.index + fullMatch.length;
}
const remainingText = message.slice(lastIndex).trim();
if (remainingText) {
textBuffer += remainingText;
}
if (textBuffer.trim()) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(textBuffer.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
}
protected async initNewSession(
instance: any,
remoteJid: string,
n8n: N8n,
settings: N8nSetting,
session: IntegrationSession,
content: string,
pushName?: string,
msg?: any,
) {
try {
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
return;
}
}
public async process(
instance: any,
remoteJid: string,
n8n: N8n,
session: IntegrationSession,
settings: N8nSetting,
content: string,
pushName?: string,
msg?: any,
) {
try {
// Handle keyword finish
if (settings?.keywordFinish?.includes(content.toLowerCase())) {
if (settings?.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.delete({
where: {
id: session.id,
},
});
}
return;
}
// If session is new or doesn't exist
if (!session) {
const data = {
remoteJid,
pushName,
botId: n8n.id,
};
const createSession = await this.createNewSession(
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
data,
);
await this.initNewSession(instance, remoteJid, n8n, settings, createSession.session, content, pushName, msg);
await sendTelemetry('/n8n/session/start');
return;
}
// If session exists but is paused
if (session.status === 'paused') {
return;
}
// Regular message for ongoing session
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
return;
}
}
}

View File

@ -1,59 +1,116 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};
export const n8nSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
webhookUrl: { type: 'string', minLength: 1 },
webhookUrl: { type: 'string' },
basicAuthUser: { type: 'string' },
basicAuthPass: { type: 'string' },
},
required: ['enabled', 'webhookUrl'],
};
export const n8nMessageSchema: JSONSchema7 = {
type: 'object',
properties: {
chatInput: { type: 'string', minLength: 1 },
sessionId: { type: 'string', minLength: 1 },
},
required: ['chatInput', 'sessionId'],
};
export const n8nSettingSchema: JSONSchema7 = {
type: 'object',
properties: {
expire: { type: 'number' },
basicAuthPassword: { type: 'string' },
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
triggerValue: { type: 'string' },
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'number' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'number' },
n8nIdFallback: { type: 'string' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'number' },
timePerChar: { type: 'integer' },
},
required: [],
required: ['enabled', 'webhookUrl', 'triggerType'],
...isNotEmpty('enabled', 'webhookUrl', 'triggerType'),
};
export const n8nStatusSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
status: { type: 'string', enum: ['opened', 'closed', 'delete', 'paused'] },
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
},
required: ['remoteJid', 'status'],
...isNotEmpty('remoteJid', 'status'),
};
export const n8nSettingSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
'splitMessages',
'timePerChar',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
'splitMessages',
'timePerChar',
),
};
export const n8nIgnoreJidSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
...isNotEmpty('remoteJid', 'action'),
};

View File

@ -117,7 +117,7 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
}
}
// Bots
// Override createBot to handle OpenAI-specific credential logic
public async createBot(instance: InstanceDto, data: OpenaiDto) {
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
@ -206,58 +206,6 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
}
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Openai Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Openai Bot not found');
}
return bot;
}
// Process OpenAI-specific bot logic
protected async processBot(
instance: any,
@ -284,8 +232,31 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
})
.then((instance) => instance.id);
if (!data.apiKey) throw new Error('API Key is required');
if (!data.name) throw new Error('Name is required');
if (!data.apiKey) throw new BadRequestException('API Key is required');
if (!data.name) throw new BadRequestException('Name is required');
// Check if API key already exists
const existingApiKey = await this.credsRepository.findFirst({
where: {
apiKey: data.apiKey,
},
});
if (existingApiKey) {
throw new BadRequestException('This API key is already registered. Please use a different API key.');
}
// Check if name already exists for this instance
const existingName = await this.credsRepository.findFirst({
where: {
name: data.name,
instanceId: instanceId,
},
});
if (existingName) {
throw new BadRequestException('This credential name is already in use. Please choose a different name.');
}
try {
const creds = await this.credsRepository.create({
@ -449,7 +420,7 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
}
// Models - OpenAI specific functionality
public async getModels(instance: InstanceDto) {
public async getModels(instance: InstanceDto, openaiCredsId?: string) {
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
const instanceId = await this.prismaRepository.instance
@ -462,21 +433,40 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
if (!instanceId) throw new Error('Instance not found');
const defaultSettings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
OpenaiCreds: true,
},
});
let apiKey: string;
if (!defaultSettings) throw new Error('Settings not found');
if (openaiCredsId) {
// Use specific credential ID if provided
const creds = await this.credsRepository.findFirst({
where: {
id: openaiCredsId,
instanceId: instanceId, // Ensure the credential belongs to this instance
},
});
if (!defaultSettings.OpenaiCreds)
throw new Error('OpenAI credentials not found. Please create credentials and associate them with the settings.');
if (!creds) throw new Error('OpenAI credentials not found for the provided ID');
const { apiKey } = defaultSettings.OpenaiCreds;
apiKey = creds.apiKey;
} else {
// Use default credentials from settings if no ID provided
const defaultSettings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (!defaultSettings) throw new Error('Settings not found');
if (!defaultSettings.OpenaiCreds)
throw new Error(
'OpenAI credentials not found. Please create credentials and associate them with the settings.',
);
apiKey = defaultSettings.OpenaiCreds.apiKey;
}
try {
this.client = new OpenAI({ apiKey });

View File

@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => openaiController.getModels(instance),
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
});
res.status(HttpStatus.OK).json(response);

View File

@ -1,11 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService, Language } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { IntegrationSession, OpenaiBot, OpenaiCreds, OpenaiSetting } from '@prisma/client';
import { ConfigService, Language, Openai as OpenaiConfig } from '@config/env.config';
import { IntegrationSession, OpenaiBot, OpenaiSetting } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
@ -33,13 +30,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
return 'openai';
}
/**
* Create a new session specific to OpenAI
*/
public async createNewSession(instance: InstanceDto, data: any) {
return super.createNewSession(instance, data, 'openai');
}
/**
* Initialize the OpenAI client with the provided API key
*/
@ -82,7 +72,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
this.initClient(creds.apiKey);
// Transcribe the audio
const transcription = await this.speechToText(msg);
const transcription = await this.speechToText(msg, instance);
if (transcription) {
this.logger.log(`Audio transcribed: ${transcription}`);
@ -149,6 +139,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
const createSession = await this.createNewSession(
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
data,
this.getBotType(),
);
await this.initNewSession(
@ -182,7 +173,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
}
// Process with the appropriate API based on bot type
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content, msg);
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content);
} catch (error) {
this.logger.error(`Error in process: ${error.message || JSON.stringify(error)}`);
return;
@ -200,7 +191,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void> {
this.logger.log(`Sending message to bot for remoteJid: ${remoteJid}, bot type: ${openaiBot.botType}`);
@ -232,11 +222,10 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
pushName,
false, // Not fromMe
content,
msg,
);
} else {
this.logger.log('Processing with ChatCompletion API');
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content, msg);
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content);
}
this.logger.log(`Got response from OpenAI: ${message?.substring(0, 50)}${message?.length > 50 ? '...' : ''}`);
@ -279,7 +268,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
pushName: string,
fromMe: boolean,
content: string,
msg?: any,
): Promise<string> {
const messageData: any = {
role: fromMe ? 'assistant' : 'user',
@ -388,7 +376,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
openaiBot: OpenaiBot,
remoteJid: string,
content: string,
msg?: any,
): Promise<string> {
this.logger.log('Starting processChatCompletionMessage');
@ -650,159 +637,67 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
/**
* Implementation of speech-to-text transcription for audio messages
* This overrides the base class implementation with extra functionality
* Can be called directly with a message object or with an audio buffer
*/
public async speechToText(msgOrBuffer: any, updateMediaMessage?: any): Promise<string | null> {
try {
this.logger.log('Starting speechToText transcription');
public async speechToText(msg: any, instance: any): Promise<string | null> {
const settings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: instance.instanceId,
},
});
// Handle direct calls with message object
if (msgOrBuffer && (msgOrBuffer.key || msgOrBuffer.message)) {
this.logger.log('Processing message object for audio transcription');
const audioBuffer = await this.getAudioBufferFromMsg(msgOrBuffer, updateMediaMessage);
if (!audioBuffer) {
this.logger.error('Failed to get audio buffer from message');
return null;
}
this.logger.log(`Got audio buffer of size: ${audioBuffer.length} bytes`);
// Process the audio buffer with the base implementation
return this.processAudioTranscription(audioBuffer);
}
// Handle calls with a buffer directly (base implementation)
this.logger.log('Processing buffer directly for audio transcription');
return this.processAudioTranscription(msgOrBuffer);
} catch (err) {
this.logger.error(`Error in speechToText: ${err}`);
return null;
}
}
/**
* Helper method to process audio buffer for transcription
*/
private async processAudioTranscription(audioBuffer: Buffer): Promise<string | null> {
if (!this.configService) {
this.logger.error('ConfigService not available for speech-to-text transcription');
if (!settings) {
this.logger.error(`OpenAI settings not found. InstanceId: ${instance.instanceId}`);
return null;
}
try {
// Use the initialized client's API key if available
let apiKey;
const creds = await this.prismaRepository.openaiCreds.findUnique({
where: { id: settings.openaiCredsId },
});
if (this.client) {
// Extract the API key from the initialized client if possible
// OpenAI client doesn't expose the API key directly, so we need to use environment or config
apiKey = this.configService.get<any>('OPENAI')?.API_KEY || process.env.OPENAI_API_KEY;
} else {
this.logger.log('No OpenAI client initialized, using config API key');
apiKey = this.configService.get<any>('OPENAI')?.API_KEY || process.env.OPENAI_API_KEY;
}
if (!creds) {
this.logger.error(`OpenAI credentials not found. CredsId: ${settings.openaiCredsId}`);
return null;
}
if (!apiKey) {
this.logger.error('No OpenAI API key set for Whisper transcription');
return null;
}
let audio: Buffer;
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
? 'pt'
: this.configService.get<Language>('LANGUAGE');
this.logger.log(`Sending audio for transcription with language: ${lang}`);
const formData = new FormData();
formData.append('file', audioBuffer, 'audio.ogg');
formData.append('model', 'whisper-1');
formData.append('language', lang);
this.logger.log('Making API request to OpenAI Whisper transcription');
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${apiKey}`,
},
if (msg.message.mediaUrl) {
audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => {
return Buffer.from(response.data, 'binary');
});
this.logger.log(`Transcription completed: ${response?.data?.text || 'No text returned'}`);
return response?.data?.text || null;
} catch (err) {
this.logger.error(`Whisper transcription failed: ${JSON.stringify(err.response?.data || err.message || err)}`);
return null;
} else if (msg.message.base64) {
audio = Buffer.from(msg.message.base64, 'base64');
} else {
// Fallback for raw WhatsApp audio messages that need downloadMediaMessage
audio = await downloadMediaMessage(
{ key: msg.key, message: msg?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: instance,
},
);
}
}
/**
* Helper method to convert message to audio buffer
*/
private async getAudioBufferFromMsg(msg: any, updateMediaMessage: any): Promise<Buffer | null> {
try {
this.logger.log('Getting audio buffer from message');
this.logger.log(`Message type: ${msg.messageType}, has media URL: ${!!msg?.message?.mediaUrl}`);
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
? 'pt'
: this.configService.get<Language>('LANGUAGE');
let audio;
const formData = new FormData();
formData.append('file', audio, 'audio.ogg');
formData.append('model', 'whisper-1');
formData.append('language', lang);
if (msg?.message?.mediaUrl) {
this.logger.log(`Getting audio from media URL: ${msg.message.mediaUrl}`);
audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => {
return Buffer.from(response.data, 'binary');
});
} else if (msg?.message?.audioMessage) {
// Handle WhatsApp audio messages
this.logger.log('Getting audio from audioMessage');
audio = await downloadMediaMessage(
{ key: msg.key, message: msg?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: updateMediaMessage,
},
);
} else if (msg?.message?.pttMessage) {
// Handle PTT voice messages
this.logger.log('Getting audio from pttMessage');
audio = await downloadMediaMessage(
{ key: msg.key, message: msg?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: updateMediaMessage,
},
);
} else {
this.logger.log('No recognized audio format found');
audio = await downloadMediaMessage(
{ key: msg.key, message: msg?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: updateMediaMessage,
},
);
}
const apiKey = creds?.apiKey || this.configService.get<OpenaiConfig>('OPENAI').API_KEY_GLOBAL;
if (audio) {
this.logger.log(`Successfully obtained audio buffer of size: ${audio.length} bytes`);
} else {
this.logger.error('Failed to obtain audio buffer');
}
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${apiKey}`,
},
});
return audio;
} catch (error) {
this.logger.error(`Error getting audio buffer: ${error.message || JSON.stringify(error)}`);
if (error.response) {
this.logger.error(`API response status: ${error.response.status}`);
this.logger.error(`API response data: ${JSON.stringify(error.response.data || {})}`);
}
return null;
}
return response?.data?.text;
}
}

View File

@ -90,23 +90,8 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
pushName?: string,
msg?: any,
) {
await this.typebotService.processTypebot(
instance,
remoteJid,
msg,
session,
bot,
bot.url,
settings.expire,
bot.typebot,
settings.keywordFinish,
settings.delayMessage,
settings.unknownMessage,
settings.listeningFromMe,
settings.stopBotFromMe,
settings.keepOpen,
content,
);
// Use the simplified service method that follows the base class pattern
await this.typebotService.processTypebot(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
// TypeBot specific method for starting a bot from API
@ -117,7 +102,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
const instanceData = await this.prismaRepository.instance.findFirst({
where: {
name: instance.instanceName,
id: instance.instanceId,
},
});
@ -226,22 +211,25 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
},
});
await this.typebotService.processTypebot(
instanceData,
remoteJid,
null,
null,
findBot,
url,
// Use the simplified service method instead of the complex one
const settings = {
expire,
typebot,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
};
await this.typebotService.processTypebot(
instanceData,
remoteJid,
findBot,
null, // session
settings,
'init',
null, // pushName
prefilledVariables,
);
} else {
@ -287,7 +275,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
request.data.clientSideActions,
);
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, {
remoteJid: remoteJid,
url: url,
typebot: typebot,

View File

@ -1,5 +1,3 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class PrefilledVariables {
@ -12,30 +10,8 @@ export class PrefilledVariables {
export class TypebotDto extends BaseChatbotDto {
url: string;
typebot: string;
description: string;
expire?: number;
keywordFinish?: string | null;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
}
export class TypebotSettingDto extends BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string | null;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
typebotIdFallback?: string;
ignoreJids?: any;
}

View File

@ -1,8 +1,7 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Auth, ConfigService, HttpServer, Typebot } from '@config/env.config';
import { Instance, IntegrationSession, Message, Typebot as TypebotModel } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service';
@ -83,15 +82,12 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
reqData.message = transcription;
} else {
reqData.message = '[Audio message could not be transcribed]';
reqData.message = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
reqData.message = '[Audio message could not be transcribed]';
}
}
@ -107,9 +103,6 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
response?.data?.input,
response?.data?.clientSideActions,
);
// Send telemetry data
sendTelemetry('/message/sendText');
} catch (error) {
this.logger.error(`Error in sendMessageToBot for Typebot: ${error.message || JSON.stringify(error)}`);
}
@ -526,44 +519,20 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
return { text, buttons: [] };
}
}
/**
* Main process method for handling Typebot messages
* This is called directly from the controller
* Simplified method that matches the base class pattern
* This should be the preferred way for the controller to call
*/
public async processTypebot(
instance: Instance,
instance: any,
remoteJid: string,
msg: Message,
session: IntegrationSession,
bot: TypebotModel,
url: string,
expire: number,
typebot: string,
keywordFinish: string,
delayMessage: number,
unknownMessage: string,
listeningFromMe: boolean,
stopBotFromMe: boolean,
keepOpen: boolean,
session: IntegrationSession,
settings: any,
content: string,
prefilledVariables?: any,
) {
try {
const settings = {
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
};
// Use the base class process method to handle the message
await this.process(instance, remoteJid, bot, session, settings, content, msg.pushName, prefilledVariables || msg);
} catch (error) {
this.logger.error(`Error in processTypebot: ${error}`);
}
pushName?: string,
msg?: any,
): Promise<void> {
return this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -123,19 +123,19 @@ export const openaiController = new OpenaiController(openaiService, prismaReposi
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
const difyService = new DifyService(waMonitor, configService, prismaRepository, openaiService);
const difyService = new DifyService(waMonitor, prismaRepository, configService, openaiService);
export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
const evolutionBotService = new EvolutionBotService(waMonitor, configService, prismaRepository, openaiService);
const evolutionBotService = new EvolutionBotService(waMonitor, prismaRepository, configService, openaiService);
export const evolutionBotController = new EvolutionBotController(evolutionBotService, prismaRepository, waMonitor);
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository, openaiService);
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService);
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService, openaiService);
export const evoaiController = new EvoaiController(evoaiService, prismaRepository, waMonitor);
logger.info('Module - ON');

View File

@ -49,7 +49,7 @@ export class ChannelStartupService {
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
public difyService = new DifyService(waMonitor, this.prismaRepository, this.configService, this.openaiService);
public setInstance(instance: InstanceDto) {
this.logger.setInstance(instance.instanceName);

View File

@ -270,6 +270,7 @@ export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
export type Dify = { ENABLED: boolean };
export type N8n = { ENABLED: boolean };
export type Evoai = { ENABLED: boolean };
export type Flowise = { ENABLED: boolean };
export type S3 = {
ACCESS_KEY: string;
@ -311,6 +312,7 @@ export interface Env {
DIFY: Dify;
N8N: N8n;
EVOAI: Evoai;
FLOWISE: Flowise;
CACHE: CacheConf;
S3?: S3;
AUTHENTICATION: Auth;
@ -626,6 +628,9 @@ export class ConfigService {
EVOAI: {
ENABLED: process.env?.EVOAI_ENABLED === 'true',
},
FLOWISE: {
ENABLED: process.env?.FLOWISE_ENABLED === 'true',
},
CACHE: {
REDIS: {
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',

View File

@ -15,5 +15,6 @@ export default defineConfig({
},
loader: {
'.json': 'file',
'.yml': 'file',
},
});