From ad5df4526a39e04e0be22750af4f469bf7bebfc3 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 6 May 2026 14:10:56 -0300 Subject: [PATCH 1/5] fix(docs): remove duplicate cover image from README Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d9ea137e14..0af3a614b7 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ Support

-
Evolution API
- --- ## About From b900770a38f52566a9d9c67693e7919e62fc5346 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 6 May 2026 14:13:30 -0300 Subject: [PATCH 2/5] docs(hosting): show only the project-specific HostGator partnership link Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0af3a614b7..98218c5f8a 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,14 @@ Local storage or S3/MinIO. Automatic media download from WhatsApp. Optional audi --- +## Hosting + +Deploy Evolution API with optimized infrastructure through our HostGator partnership: + +[**Evolution API VPS — HostGator**](https://evolution-api.com/vps-evolution-api) + +--- + ## Telemetry Evolution API collects anonymous telemetry data (routes used, most accessed routes, API version) to help improve the service. **No sensitive or personal data is collected.** This information helps us identify improvements and provide a better experience for users. From 4f3bf113eec2caa8846b757ac77aadff0a3e7050 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 6 May 2026 14:35:53 -0300 Subject: [PATCH 3/5] docs(org): update GitHub URLs from EvolutionAPI to evolution-foundation Co-Authored-By: Claude Opus 4.7 (1M context) --- CONTRIBUTING.md | 2 +- README.md | 6 +++--- evolution-manager-v2 | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 595600cc05..8aa5df1c7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Harassment, discrimination, or abusive behavior will not be tolerated. ### Reporting Bugs -1. Check existing [issues](https://github.com/EvolutionAPI/evolution-api/issues) +1. Check existing [issues](https://github.com/evolution-foundation/evolution-api/issues) to avoid duplicates 2. Open a new issue with: - Clear, descriptive title diff --git a/README.md b/README.md index 98218c5f8a..4586e4028a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

- Latest version + Latest version License: Apache 2.0 Documentation Community @@ -37,7 +37,7 @@ Evolution API began as a WhatsApp controller API based on [CodeChat](https://git ## Part of the Evolution Foundation ecosystem -Evolution API is one of the messaging engines maintained by Evolution Foundation. It is used as a WhatsApp provider by the [Evo CRM Community](https://github.com/EvolutionAPI/evo-crm-community) and other projects in the ecosystem. +Evolution API is one of the messaging engines maintained by Evolution Foundation. It is used as a WhatsApp provider by the [Evo CRM Community](https://github.com/evolution-foundation/evo-crm-community) and other projects in the ecosystem. --- @@ -80,7 +80,7 @@ Evolution API integrates natively with many platforms: ### Installation ```bash -git clone git@github.com:EvolutionAPI/evolution-api.git +git clone git@github.com:evolution-foundation/evolution-api.git cd evolution-api # Install dependencies diff --git a/evolution-manager-v2 b/evolution-manager-v2 index f054b9bc28..3137df4695 160000 --- a/evolution-manager-v2 +++ b/evolution-manager-v2 @@ -1 +1 @@ -Subproject commit f054b9bc28083152d4948f835e3346fd0add39db +Subproject commit 3137df469504ce211c68e7b35f0706497ac1b95f From fa09d37892cdbb1d65a250155d293d92230c5b30 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 6 May 2026 14:38:27 -0300 Subject: [PATCH 4/5] docs(org): update nested submodule URLs from EvolutionAPI to evolution-foundation Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index ef5b3e63cf..54a177465b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "evolution-manager-v2"] path = evolution-manager-v2 - url = https://github.com/EvolutionAPI/evolution-manager-v2.git + url = https://github.com/evolution-foundation/evolution-manager-v2.git From c043dd0f8d4f06936f573696a00bff53c4fd8697 Mon Sep 17 00:00:00 2001 From: Bruno Eduardo Santos Date: Fri, 24 Jul 2026 08:58:12 -0300 Subject: [PATCH 5/5] fix: notifica falhas silenciosas de QR no Chatwoot e reconcilia estado zumbi Dois problemas distintos, mas relacionados: 1) QR solicitado pelo Chatwoot as vezes simplesmente nao aparecia na conversa - mesmo com a instancia genuinamente desconectada, nao so em cenario de estado "zumbi". Causa: falhas engolidas em silencio. receiveWebhook (comando 'init') e o handler de qrcode.updated estavam em try/catch que so logava no servidor, sem nenhum feedback ao operador no Chatwoot. Havia tambem um bug de optional chaining (body?.qrcode.base64) que podia lancar TypeError silencioso se body.qrcode viesse undefined. 2) Instancia aparecia "conectada" na Evolution mesmo com o WhatsApp real caido. Causa: o status de conexao e 100% orientado a evento (connection.update); se esse evento nunca dispara (ex. socket morrendo sem RST/FIN, comum sob CPU steal alto), o valor cacheado em memoria/banco fica preso em 'open' para sempre. Nao havia nenhuma sonda ativa (confirmado: zero ocorrencias de setInterval em todo o src/). Mudancas: - receiveWebhook: erro ao conectar agora notifica a conversa (cw.inbox.qrError); catch geral tambem notifica erro generico (cw.inbox.requestError) alem de logar. - Corrigido o optional chaining (body?.qrcode?.base64) com guarda explicita e notificacao de erro. - Handler de qrcode.updated: catch notifica cw.inbox.qrError, mas so quando o QR ainda nao tinha sido postado com sucesso (flag qrPosted) - evita notificar erro depois que a imagem do QR ja chegou na conversa. - monitor.service.ts: health check periodico (30s) que confirma via client.ws.isOpen se instancias marcadas 'open' realmente tem socket ativo; se nao, reconcilia o estado para 'close' (banco + webhook CONNECTION_UPDATE), reaproveitando os mesmos campos ja usados no fechamento real da conexao. - instance.controller.ts: connectionState reconcilia sob demanda antes de responder, alem do health check periodico. - Novas chaves de traducao cw.inbox.qrError/cw.inbox.requestError em pt-BR/en/es. --- src/api/controllers/instance.controller.ts | 9 ++ .../chatwoot/services/chatwoot.service.ts | 40 ++++++++- src/api/services/monitor.service.ts | 90 ++++++++++++++++++- src/utils/translations/en.json | 2 + src/utils/translations/es.json | 2 + src/utils/translations/pt-BR.json | 2 + 6 files changed, 142 insertions(+), 3 deletions(-) diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index 6a69106881..a79ae21e61 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -391,6 +391,15 @@ export class InstanceController { } public async connectionState({ instanceName }: InstanceDto) { + // Cross-check the cached state against the real WebSocket before answering, so a + // "zombie open" instance (state cached as 'open' after the socket already dropped) + // is reconciled here too, not only on the next periodic health check. + try { + await this.waMonitor.reconcileInstanceConnection(instanceName); + } catch (error) { + this.logger.error(error); + } + return { instance: { instanceName: instanceName, diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 906fff1881..408785724f 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1381,7 +1381,12 @@ export class ChatwootService { if (state !== 'open') { const number = command.split(':')[1]; - await waInstance.connectToWhatsapp(number); + try { + await waInstance.connectToWhatsapp(number); + } catch (connectError) { + this.logger.error(connectError); + await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming'); + } } else { await this.createBotMessage( instance, @@ -1601,6 +1606,13 @@ export class ChatwootService { return { message: 'bot' }; } catch (error) { this.logger.error(error); + this.logger.error(error?.stack ?? error); + + try { + await this.createBotMessage(instance, i18next.t('cw.inbox.requestError'), 'incoming'); + } catch (notifyError) { + this.logger.error(notifyError); + } return { message: 'bot' }; } @@ -1948,6 +1960,10 @@ export class ChatwootService { } public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + // Tracks whether createBotQr already succeeded, so a later failure in this same + // handler (e.g. posting the accompanying text message) doesn't also send a + // confusing "QR failed" notification into a conversation that just received the QR. + let qrPosted = false; try { const waInstance = this.waMonitor.waInstances[instance.instanceName]; @@ -2493,7 +2509,17 @@ export class ChatwootService { const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`; return await this.createBotMessage(instance, erroQRcode, 'incoming'); } else { - const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); + const qrBase64 = body?.qrcode?.base64; + + if (!qrBase64) { + this.logger.error( + `qrcode.updated event received without a valid qrcode.base64 payload for instance ${instance?.instanceName}`, + ); + await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming'); + return; + } + + const fileData = Buffer.from(qrBase64.replace('data:image/png;base64,', ''), 'base64'); const fileStream = new Readable(); fileStream._read = () => {}; @@ -2507,6 +2533,7 @@ export class ChatwootService { fileStream, `${instance.instanceName}.png`, ); + qrPosted = true; let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`; @@ -2524,6 +2551,15 @@ export class ChatwootService { } } catch (error) { this.logger.error(error); + this.logger.error(error?.stack ?? error); + + if (event === 'qrcode.updated' && !qrPosted) { + try { + await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming'); + } catch (notifyError) { + this.logger.error(notifyError); + } + } } } diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index 438530b57e..23b92c1448 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -2,7 +2,7 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { ProviderFiles } from '@api/provider/sessions'; import { PrismaRepository } from '@api/repository/repository.service'; import { channelController } from '@api/server.module'; -import { Events, Integration } from '@api/types/wa.types'; +import { Events, Integration, wa } from '@api/types/wa.types'; import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { INSTANCE_DIR, STORE_DIR } from '@config/path.config'; @@ -26,6 +26,7 @@ export class WAMonitoringService { ) { this.removeInstance(); this.noConnection(); + this.startConnectionHealthCheck(); Object.assign(this.db, configService.get('DATABASE')); Object.assign(this.redis, configService.get('CACHE')); @@ -42,6 +43,12 @@ export class WAMonitoringService { private readonly providerSession: ProviderSession; + // Health check: how often (ms) we probe instances cached as 'open' to confirm the + // underlying WebSocket is really still connected (mitigates the "zombie open" state, + // since connectionStatus is otherwise 100% event-driven from connection.update). + private readonly HEALTH_CHECK_INTERVAL_MS = 30_000; + private healthCheckInterval: NodeJS.Timeout; + public delInstanceTime(instance: string) { const time = this.configService.get('DEL_INSTANCE'); if (typeof time === 'number' && time > 0) { @@ -83,6 +90,87 @@ export class WAMonitoringService { } } + /** + * Starts the periodic reconciliation of cached connection state ("zombie open" mitigation). + * connectionStatus.state is otherwise only ever written from inside the Baileys + * 'connection.update' handler, so if that event is ever missed (process hiccup, socket + * torn down outside Baileys' own reconnection flow, etc.) an instance can stay marked + * 'open' in memory/DB forever even though the WebSocket is gone. + */ + private startConnectionHealthCheck() { + this.healthCheckInterval = setInterval(() => { + this.checkInstancesConnection(); + }, this.HEALTH_CHECK_INTERVAL_MS); + } + + private async checkInstancesConnection() { + for (const instanceName of Object.keys(this.waInstances)) { + try { + await this.reconcileInstanceConnection(instanceName); + } catch (error) { + // A failure checking one instance must never abort the loop or affect the others. + this.logger.error(`Health check failed for instance "${instanceName}": ${error}`); + } + } + } + + /** + * Confirms that an instance cached as 'open' still has a real, open WebSocket + * (client.ws.isOpen, per Baileys' AbstractSocketClient). If it does not, forces + * stateConnection to 'close', persists it via the same Prisma fields used by + * BaileysStartupService.connectionUpdate() on a genuine close, and re-emits + * CONNECTION_UPDATE through the instance's own sendDataWebhook (no duplicated logic). + * + * Used both by the periodic health check above and on-demand by + * InstanceController.connectionState() before answering a status request. + */ + public async reconcileInstanceConnection(instanceName: string): Promise { + const waInstance = this.waInstances[instanceName]; + + if (!waInstance || waInstance.connectionStatus?.state !== 'open') { + return waInstance?.connectionStatus; + } + + const client = waInstance.client; + + // Only instances with an initialized Baileys client expose ws.isOpen; instances + // without one (still connecting, non-Baileys channel, etc.) are left untouched. + if (!client?.ws || client.ws.isOpen) { + return waInstance.connectionStatus; + } + + this.logger.warn( + `Instance "${instanceName}" is cached as 'open' but its WebSocket is not open (isOpen=false). Reconciling status to 'close'.`, + ); + + waInstance.stateConnection = { state: 'close', statusReason: 428 }; + + try { + await this.prismaRepository.instance.update({ + where: { id: waInstance.instanceId }, + data: { + connectionStatus: 'close', + disconnectionAt: new Date(), + disconnectionReasonCode: 428, + disconnectionObject: JSON.stringify({ reason: 'health-check: websocket not open' }), + }, + }); + } catch (error) { + this.logger.error(`Failed to persist reconciled connection state for "${instanceName}": ${error}`); + } + + try { + await waInstance.sendDataWebhook?.(Events.CONNECTION_UPDATE, { + instance: instanceName, + ...waInstance.stateConnection, + }); + } catch (error) { + this.logger.error(`Failed to emit CONNECTION_UPDATE for "${instanceName}": ${error}`); + } + + return waInstance.connectionStatus; + } + public async instanceInfo(instanceNames?: string[]): Promise { if (instanceNames && instanceNames.length > 0) { const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : []; diff --git a/src/utils/translations/en.json b/src/utils/translations/en.json index 43dfd8d213..0a31dc1b34 100644 --- a/src/utils/translations/en.json +++ b/src/utils/translations/en.json @@ -2,6 +2,8 @@ "qrgeneratedsuccesfully": "QRCode successfully generated!", "scanqr": "Scan this QR code within the next 40 seconds.", "qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.", + "cw.inbox.qrError": "🚨 Failed to generate the QR Code. Please try again by sending 'init' in this conversation.", + "cw.inbox.requestError": "⚠️ An error occurred while processing your request. If you were trying to connect via QR Code, please try again by sending 'init'.", "cw.inbox.connected": "🚀 Connection successfully established!", "cw.inbox.disconnect": "🚨 Disconnecting WhatsApp from inbox *{{inboxName}}*.", "cw.inbox.alreadyConnected": "🚨 {{inboxName}} instance is connected.", diff --git a/src/utils/translations/es.json b/src/utils/translations/es.json index be830885ed..1bf0f5a824 100644 --- a/src/utils/translations/es.json +++ b/src/utils/translations/es.json @@ -2,6 +2,8 @@ "qrgeneratedsuccesfully": "Código QR generado exitosamente!", "scanqr": "Escanea este código QR en los próximos 40 segundos.", "qrlimitreached": "🚨 Se alcanzó el límite de generación de QRCode. Para generar un nuevo QRCode, envíe el mensaje 'init' nuevamente.", + "cw.inbox.qrError": "🚨 No se pudo generar el código QR. Por favor, intente nuevamente enviando 'init' en esta conversación.", + "cw.inbox.requestError": "⚠️ Ocurrió un error al procesar su solicitud. Si estaba intentando conectar mediante código QR, intente nuevamente enviando 'init'.", "cw.inbox.connected": "🚀 ¡Conexión establecida exitosamente!", "cw.inbox.disconnect": "🚨 Instancia *{{inboxName}}* desconectado de Whatsapp.", "cw.inbox.alreadyConnected": "🚨 La instancia {{inboxName}} está conectada.", diff --git a/src/utils/translations/pt-BR.json b/src/utils/translations/pt-BR.json index 31bf468a9b..f55557cf4d 100644 --- a/src/utils/translations/pt-BR.json +++ b/src/utils/translations/pt-BR.json @@ -2,6 +2,8 @@ "qrgeneratedsuccesfully": "QRCode gerado com sucesso!", "scanqr": "Escaneie o QRCode com o WhatsApp nos próximos 40 segundos.", "qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa.", + "cw.inbox.qrError": "🚨 Falha ao gerar o QR Code. Por favor, tente novamente enviando 'init' nesta conversa.", + "cw.inbox.requestError": "⚠️ Ocorreu um erro ao processar sua solicitação. Se você estava tentando conectar via QR Code, tente novamente enviando 'init'.", "cw.inbox.connected": "🚀 Conectado com sucesso!", "cw.inbox.disconnect": "🚨 Instância *{{inboxName}}* desconectada do WhatsApp.", "cw.inbox.alreadyConnected": "🚨 Instância *{{inboxName}}* já está conectada.",