From 43f519e7aaaed7d4ff53dc24d36a1b7337a4f762 Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Sun, 28 Jun 2026 05:45:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(settings):=20selo=20Conta=20Business,=20av?= =?UTF-8?q?iso=20de=20re-scan=20ao=20trocar=20API,=20log=20de=20erro=20de?= =?UTF-8?q?=20bot=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detecta WhatsApp Business (app) via getBusinessProfile no pós-conexão e persiste em instances.isBusiness; painel mostra "Conta Business: sim/não" (com nota Business App ≠ Business API). - Painel avisa que trocar de API desconecta e exige escanear o QR novamente, com confirmação antes de aplicar a troca. - sendRichMessage agora monitora e loga erros especificamente quando a mensagem tem botões/lista/carrossel (diagnóstico de rejeição de interativos por engine). Co-Authored-By: Claude Opus 4.8 --- .../backend/prisma/schema.prisma | 1 + .../whatsapp/handlers/ContactHandler.ts | 27 +++++++++++++- .../src/modules/whatsapp/rich-message.ts | 26 +++++++++++++ .../frontend/components/SettingsPanel.tsx | 37 ++++++++++++++++++- 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/newwhats.clube67.com/newwhats.local/backend/prisma/schema.prisma b/newwhats.clube67.com/newwhats.local/backend/prisma/schema.prisma index f9a5c5e..e5e4a63 100644 --- a/newwhats.clube67.com/newwhats.local/backend/prisma/schema.prisma +++ b/newwhats.clube67.com/newwhats.local/backend/prisma/schema.prisma @@ -89,6 +89,7 @@ model Instance { avatar String? status InstanceStatus @default(DISCONNECTED) engine String @default("infinite") + isBusiness Boolean @default(false) // conta WhatsApp Business (app) — detectada via getBusinessProfile sessionPath String? webhookUrl String? createdAt DateTime @default(now()) diff --git a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/ContactHandler.ts b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/ContactHandler.ts index 3778846..199cfff 100644 --- a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/ContactHandler.ts +++ b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/ContactHandler.ts @@ -260,6 +260,9 @@ export class ContactHandler { */ async syncInstanceAvatar(instanceId: string): Promise { if (!this.sock?.user?.id) return null + // Detecta se a conta é WhatsApp Business (app) — informativo no painel. + // Nota: é o Business APP, não a Business API; botões só são confiáveis na API. + this.syncBusinessStatus(instanceId).catch(() => {}) try { const url = await this.sock.profilePictureUrl(this.sock.user.id, 'image') if (url) { @@ -273,6 +276,25 @@ export class ContactHandler { return null } + /** + * Verifica se a conta conectada é WhatsApp Business (app) via getBusinessProfile + * e persiste em instances.isBusiness. Falha silenciosa = conta pessoal. + */ + async syncBusinessStatus(instanceId: string): Promise { + if (!this.sock?.user?.id) return + let isBusiness = false + try { + const profile = await (this.sock as any).getBusinessProfile?.(this.sock.user.id) + isBusiness = !!profile && (profile.description != null || profile.category != null || profile.email != null || profile.website != null) + } catch { + isBusiness = false + } + try { + await prisma.instance.update({ where: { id: instanceId }, data: { isBusiness } }) + logger.info({ instanceId, isBusiness }, '[ContactHandler] Status Business da instância atualizado') + } catch { /* não-fatal */ } + } + // ═══════════════════════════════════════════════════════════════════════════ // RECONCILIAÇÃO DE NOMES VIA PUSHNAME // ═══════════════════════════════════════════════════════════════════════════ @@ -403,7 +425,10 @@ export class ContactHandler { let ok = 0 for (const collection of collections) { try { - await this.sock.resyncAppState([collection] as any, false) + // isInitial=true → full snapshot da collection (baixa o estado ATUAL, + // incluindo pins/arquivos já existentes). Com false (incremental) o WA + // não reenvia o que já está no hash, então fixados existentes não vêm. + await this.sock.resyncAppState([collection] as any, true) ok++ logger.info({ instanceId: this.instanceId, collection }, '[ContactHandler] app-state sincronizado (pin/arquivo/mute)') diff --git a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/rich-message.ts b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/rich-message.ts index 44e076c..ce2e2b4 100644 --- a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/rich-message.ts +++ b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/rich-message.ts @@ -29,6 +29,7 @@ * mas testamos que a forma direta com nó biz > list também funciona. */ import { generateMessageIDV2, generateWAMessageFromContent, isJidGroup, isJidStatusBroadcast } from './engine' +import { logger } from '../../config/logger' import { proto } from './engine' import type { WASocket, BinaryNode } from './engine' @@ -212,6 +213,31 @@ export async function sendRichMessage( payload: RichPayload, quoted?: { key: any; message: any }, engineType?: string, +): Promise { + // Monitora erros ESPECIFICAMENTE quando a mensagem tem botões/lista/carrossel, + // para diagnosticar quando o WhatsApp rejeita a renderização de mensagens + // interativas (varia por engine e por tipo de conta). + const hasInteractive = !!(payload.nativeButtons || payload.nativeList || payload.nativeCarousel) + const eng = engineType ?? process.env.BAILEYS_ENGINE ?? 'infinite' + try { + return await sendRichMessageInner(sock, jid, payload, quoted, engineType) + } catch (err) { + if (hasInteractive) { + const tipo = payload.nativeButtons ? 'botões' : payload.nativeList ? 'lista' : 'carrossel' + logger.error({ err, jid, engine: eng, tipo }, + `[rich-message] FALHA ao enviar mensagem interativa (${tipo}) — engine="${eng}". ` + + `Botões podem não ser suportados nesta conta/engine.`) + } + throw err + } +} + +async function sendRichMessageInner( + sock: WASocket, + jid: string, + payload: RichPayload, + quoted?: { key: any; message: any }, + engineType?: string, ): Promise { const { text, footer, nativeButtons, nativeList, nativeCarousel, poll } = payload const engine = engineType ?? process.env.BAILEYS_ENGINE ?? 'infinite' diff --git a/newwhats.clube67.com/newwhats.local/frontend/components/SettingsPanel.tsx b/newwhats.clube67.com/newwhats.local/frontend/components/SettingsPanel.tsx index bc9d101..0c9e87f 100644 --- a/newwhats.clube67.com/newwhats.local/frontend/components/SettingsPanel.tsx +++ b/newwhats.clube67.com/newwhats.local/frontend/components/SettingsPanel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { ShieldCheck, LayoutGrid, Check, Loader2, Tag } from 'lucide-react'; +import { ShieldCheck, LayoutGrid, Check, Loader2, Tag, AlertTriangle, Briefcase } from 'lucide-react'; interface SettingsPanelProps { instanceId: string | null; @@ -28,6 +28,7 @@ const ENGINES: { export default function SettingsPanel({ instanceId }: SettingsPanelProps) { const [engine, setEngine] = useState(null); + const [isBusiness, setIsBusiness] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(null); const [error, setError] = useState(null); @@ -56,6 +57,7 @@ export default function SettingsPanel({ instanceId }: SettingsPanelProps) { .then((list) => { const inst = Array.isArray(list) ? list.find((i: any) => i.id === instanceId) : null; setEngine(inst?.engine ?? null); + setIsBusiness(inst ? !!inst.isBusiness : null); }) .catch(() => {}) .finally(() => setLoading(false)); @@ -63,6 +65,12 @@ export default function SettingsPanel({ instanceId }: SettingsPanelProps) { const selectEngine = useCallback(async (value: string) => { if (!instanceId || saving || value === engine) return; + // Trocar de engine invalida a sessão (credenciais não migram entre APIs): + // confirma porque exige escanear o QR Code novamente. + if (typeof window !== 'undefined' && !window.confirm( + 'Trocar de API vai DESCONECTAR a sessão atual.\n\n' + + 'Você precisará escanear o QR Code novamente para reconectar. Continuar?' + )) return; setSaving(value); setError(null); try { @@ -88,7 +96,14 @@ export default function SettingsPanel({ instanceId }: SettingsPanelProps) {

Configurações

-

Escolha a API de conexão do WhatsApp para esta instância.

+

Escolha a API de conexão do WhatsApp para esta instância.

+ +
+ +

+ Trocar de API desconecta a sessão — será necessário escanear o QR Code novamente para reconectar. +

+
{!instanceId && (

Selecione uma instância primeiro.

@@ -136,6 +151,24 @@ export default function SettingsPanel({ instanceId }: SettingsPanelProps) { {loading &&

Carregando configuração atual…

} {error &&

{error}

} + {/* ── Conta Business ──────────────────────────────────────── */} +
+
+
+ +
+
+

Conta Business

+

+ {isBusiness === null ? '—' : isBusiness ? 'Sim (WhatsApp Business)' : 'Não (conta pessoal)'} +

+

+ Business App ≠ Business API — botões só são garantidos na Business API oficial. +

+
+
+
+ {/* ── Versão ──────────────────────────────────────────────── */}