From 0d8e8bc0fb7ee331437cf313e4220cdf590b7176 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Sun, 28 Sep 2025 22:19:36 -0300 Subject: [PATCH 1/3] =?UTF-8?q?fix(chatwoot):=20corrige=20reabertura=20de?= =?UTF-8?q?=20conversas=20e=20loop=20de=20conex=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit aborda duas questões críticas na integração com o Chatwoot para melhorar a estabilidade e a experiência do agente. Primeiro, as conversas que já estavam marcadas como "resolvidas" no Chatwoot não eram reabertas automaticamente quando o cliente enviava uma nova mensagem. Isso foi corrigido para que o sistema verifique o status da conversa e a reabra, garantindo que nenhuma nova interação seja perdida. Segundo, um bug no tratamento do evento de conexão fazia com que a mensagem de status "Conexão estabelecida com sucesso" fosse enviada repetidamente, poluindo o histórico da conversa. A lógica foi ajustada para garantir que esta notificação seja enviada apenas uma vez por evento de conexão. --- .../chatwoot/services/chatwoot.service.ts | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index fd31da84b..5025da31e 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -747,34 +747,49 @@ export class ChatwootService { return null; } - let inboxConversation = contactConversations.payload.find( - (conversation) => conversation.inbox_id == filterInbox.id, - ); - if (inboxConversation) { - if (this.provider.reopenConversation) { - this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`); - if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') { - await client.conversations.toggleStatus({ - accountId: this.provider.accountId, - conversationId: inboxConversation.id, - data: { - status: 'pending', - }, - }); - } + let inboxConversation = null; + + if (this.provider.reopenConversation) { + inboxConversation = contactConversations.payload.find( + (conversation) => + conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + + if (inboxConversation) { + this.logger.verbose( + `Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`, + ); } else { inboxConversation = contactConversations.payload.find( - (conversation) => - conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + (conversation) => conversation.inbox_id == filterInbox.id, ); - this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); - } - if (inboxConversation) { - this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`); - this.cache.set(cacheKey, inboxConversation.id); - return inboxConversation.id; + if (inboxConversation) { + this.logger.verbose(`Found resolved conversation to reopen: ${JSON.stringify(inboxConversation)}`); + if (this.provider.conversationPending && inboxConversation.status !== 'open') { + await client.conversations.toggleStatus({ + accountId: this.provider.accountId, + conversationId: inboxConversation.id, + data: { + status: 'pending', + }, + }); + this.logger.verbose(`Reopened resolved conversation ID: ${inboxConversation.id}`); + } + } } + } else { + inboxConversation = contactConversations.payload.find( + (conversation) => + conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); + } + + if (inboxConversation) { + this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`); + this.cache.set(cacheKey, inboxConversation.id); + return inboxConversation.id; } const data = { @@ -2407,12 +2422,25 @@ export class ChatwootService { if (event === 'connection.update') { if (body.status === 'open') { + const waInstance = this.waMonitor.waInstances[instance.instanceName]; // if we have qrcode count then we understand that a new connection was established - if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { + if (waInstance && waInstance.qrCode.count > 0) { const msgConnection = i18next.t('cw.inbox.connected'); await this.createBotMessage(instance, msgConnection, 'incoming'); - this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0; + waInstance.qrCode.count = 0; + + waInstance.lastConnectionNotification = Date.now(); + chatwootImport.clearAll(instance); + } else if (waInstance) { + const timeSinceLastNotification = Date.now() - (waInstance.lastConnectionNotification || 0); + const minIntervalMs = 30000; // 30 seconds + + if (timeSinceLastNotification < minIntervalMs) { + this.logger.warn( + `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, + ); + } } } } From f7862637b164c1e625b8bdfa4bd1ac27cc6c4c6e Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Sun, 28 Sep 2025 22:38:45 -0300 Subject: [PATCH 2/3] =?UTF-8?q?fix(chatwoot):=20otimizar=20l=C3=B3gica=20d?= =?UTF-8?q?e=20reabertura=20de=20conversas=20e=20notifica=C3=A7=C3=A3o=20d?= =?UTF-8?q?e=20conex=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit introduz melhorias na integração com o Chatwoot, focando na reabertura de conversas e na notificação de conexão. A lógica foi refatorada para centralizar a busca por conversas abertas e a reabertura de conversas resolvidas, garantindo que interações não sejam perdidas. Além disso, foi implementado um intervalo mínimo para notificações de conexão, evitando mensagens excessivas e melhorando a experiência do usuário. --- .../chatwoot/services/chatwoot.service.ts | 128 ++++++++++-------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 5025da31e..5e53b2ee6 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -33,6 +33,8 @@ import mimeTypes from 'mime-types'; import path from 'path'; import { Readable } from 'stream'; +const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds + interface ChatwootMessage { messageId?: number; inboxId?: number; @@ -747,43 +749,14 @@ export class ChatwootService { return null; } - let inboxConversation = null; - - if (this.provider.reopenConversation) { - inboxConversation = contactConversations.payload.find( - (conversation) => - conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - - if (inboxConversation) { - this.logger.verbose( - `Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`, - ); - } else { - inboxConversation = contactConversations.payload.find( - (conversation) => conversation.inbox_id == filterInbox.id, - ); + let inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); - if (inboxConversation) { - this.logger.verbose(`Found resolved conversation to reopen: ${JSON.stringify(inboxConversation)}`); - if (this.provider.conversationPending && inboxConversation.status !== 'open') { - await client.conversations.toggleStatus({ - accountId: this.provider.accountId, - conversationId: inboxConversation.id, - data: { - status: 'pending', - }, - }); - this.logger.verbose(`Reopened resolved conversation ID: ${inboxConversation.id}`); - } - } - } - } else { - inboxConversation = contactConversations.payload.find( - (conversation) => - conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + if (!inboxConversation && this.provider.reopenConversation) { + inboxConversation = await this.findAndReopenResolvedConversation( + client, + contactConversations.payload, + filterInbox.id, ); - this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); } if (inboxConversation) { @@ -832,6 +805,45 @@ export class ChatwootService { } } + private findOpenConversation(conversations: any[], inboxId: number): any | null { + const openConversation = conversations.find( + (conversation) => conversation && conversation.status !== 'resolved' && conversation.inbox_id == inboxId, + ); + + if (openConversation) { + this.logger.verbose(`Found open conversation: ${JSON.stringify(openConversation)}`); + } + + return openConversation || null; + } + + private async findAndReopenResolvedConversation( + client: any, + conversations: any[], + inboxId: number, + ): Promise { + const resolvedConversation = conversations.find( + (conversation) => conversation && conversation.status === 'resolved' && conversation.inbox_id == inboxId, + ); + + if (resolvedConversation) { + this.logger.verbose(`Found resolved conversation to reopen: ${JSON.stringify(resolvedConversation)}`); + if (this.provider.conversationPending && resolvedConversation.status !== 'open') { + await client.conversations.toggleStatus({ + accountId: this.provider.accountId, + conversationId: resolvedConversation.id, + data: { + status: 'pending', + }, + }); + this.logger.verbose(`Reopened resolved conversation ID: ${resolvedConversation.id}`); + } + return resolvedConversation; + } + + return null; + } + public async getInbox(instance: InstanceDto): Promise { const cacheKey = `${instance.instanceName}:getInbox`; if (await this.cache.has(cacheKey)) { @@ -2420,28 +2432,30 @@ export class ChatwootService { await this.createBotMessage(instance, msgStatus, 'incoming'); } - if (event === 'connection.update') { - if (body.status === 'open') { - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - // if we have qrcode count then we understand that a new connection was established - if (waInstance && waInstance.qrCode.count > 0) { - const msgConnection = i18next.t('cw.inbox.connected'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - waInstance.qrCode.count = 0; - - waInstance.lastConnectionNotification = Date.now(); - - chatwootImport.clearAll(instance); - } else if (waInstance) { - const timeSinceLastNotification = Date.now() - (waInstance.lastConnectionNotification || 0); - const minIntervalMs = 30000; // 30 seconds - - if (timeSinceLastNotification < minIntervalMs) { - this.logger.warn( - `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, - ); - } - } + if (event === 'connection.update' && body.status === 'open') { + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + if (!waInstance) return; + + const now = Date.now(); + const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0); + + // Se a conexão foi estabelecida via QR code, notifica imediatamente. + if (waInstance.qrCode && waInstance.qrCode.count > 0) { + const msgConnection = i18next.t('cw.inbox.connected'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + waInstance.qrCode.count = 0; + waInstance.lastConnectionNotification = now; + chatwootImport.clearAll(instance); + } + // Se não foi via QR code, verifica o throttling. + else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) { + const msgConnection = i18next.t('cw.inbox.connected'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + waInstance.lastConnectionNotification = now; + } else { + this.logger.warn( + `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, + ); } } From c132379b3ab0cbd501f0b5881c61b0291d967a54 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Mon, 29 Sep 2025 15:26:24 -0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(chatwoot):=20ajustar=20l=C3=B3gica=20de?= =?UTF-8?q?=20verifica=C3=A7=C3=A3o=20de=20conversas=20e=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit modifica a lógica de verificação de conversas no serviço Chatwoot, garantindo que a busca por conversas ativas seja priorizada em relação ao uso de cache. A verificação de cache foi removida em pontos críticos para evitar que conversas desatualizadas sejam utilizadas, melhorando a precisão na recuperação de dados. Além disso, a lógica de reabertura de conversas foi refinada para garantir que as interações sejam tratadas corretamente, mantendo a experiência do usuário mais fluida. --- .../chatwoot/services/chatwoot.service.ts | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 5e53b2ee6..f439faed0 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -606,12 +606,7 @@ export class ChatwootService { this.logger.verbose(`--- Start createConversation ---`); this.logger.verbose(`Instance: ${JSON.stringify(instance)}`); - // If it already exists in the cache, return conversationId - if (await this.cache.has(cacheKey)) { - const conversationId = (await this.cache.get(cacheKey)) as number; - this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`); - return conversationId; - } + // Always check Chatwoot first, cache only as fallback // If lock already exists, wait until release or timeout if (await this.cache.has(lockKey)) { @@ -623,11 +618,7 @@ export class ChatwootService { break; } await new Promise((res) => setTimeout(res, 300)); - if (await this.cache.has(cacheKey)) { - const conversationId = (await this.cache.get(cacheKey)) as number; - this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`); - return conversationId; - } + // Removed cache check here to ensure we always check Chatwoot } } @@ -637,12 +628,9 @@ export class ChatwootService { try { /* - Double check after lock - Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock + Double check after lock - REMOVED + This was causing the system to use cached conversations instead of checking Chatwoot */ - if (await this.cache.has(cacheKey)) { - return (await this.cache.get(cacheKey)) as number; - } const client = await this.clientCw(instance); if (!client) return null; @@ -749,14 +737,25 @@ export class ChatwootService { return null; } - let inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); + let inboxConversation = null; - if (!inboxConversation && this.provider.reopenConversation) { - inboxConversation = await this.findAndReopenResolvedConversation( - client, - contactConversations.payload, - filterInbox.id, - ); + if (this.provider.reopenConversation) { + inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); + + if (inboxConversation) { + this.logger.verbose( + `Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`, + ); + } else { + inboxConversation = await this.findAndReopenResolvedConversation( + client, + contactConversations.payload, + filterInbox.id, + ); + } + } else { + inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); + this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); } if (inboxConversation) { @@ -765,6 +764,14 @@ export class ChatwootService { return inboxConversation.id; } + if (await this.cache.has(cacheKey)) { + const conversationId = (await this.cache.get(cacheKey)) as number; + this.logger.warn( + `No active conversations found in Chatwoot, using cached conversation ID: ${conversationId} as fallback`, + ); + return conversationId; + } + const data = { contact_id: contactId.toString(), inbox_id: filterInbox.id.toString(),