SEC-06: ChatbotService reescrito com abstração aiCall() que despacha para
GEMINI ou OPENAI baseado em bot.credential.provider. Fallback automático:
se o provider primário falha (quota, rate limit), tenta a próxima credencial
da instância com provider diferente.
SEC-07: Divergência entre LLMProvider Prisma (GEMINI/OPENAI) e Secretária
(gemini/openai/anthropic/ollama) documentada com comentário explícito no
topo do service. Chatbot Rápido é intencionalmente mais simples.
SEC-13 (bônus): logger.info no ChatbotService agora inclui inputTokens e
outputTokens por chamada para observabilidade de consumo.
SEC-14: sendPresenceUpdate('composing', jid) antes de chamar a IA e
sendPresenceUpdate('paused', jid) após enviar a resposta — tanto no
ChatbotService quanto no listener ext:message.new da Secretária.
O usuário no WhatsApp vê "digitando..." enquanto a IA processa.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SEC-01+SEC-11: /secretaria/ask agora envia reply via WhatsApp (sock.sendMessage)
e persiste a mensagem no banco Prisma após brain.chat(), com notify Socket.IO.
SEC-02+SEC-03: MessageHandler emite hookBus 'ext:message.new' para todo msg
recebido. Listener no ext-api auto-dispara a Secretária apenas se NÃO
houver AIBot ativo na instância (exclusão mútua — chatbot tem prioridade).
SEC-04: ChatListItem exibe ícone BotOff (âmbar) quando chat.bot_paused = true,
com tooltip "Bot pausado — aguardando atendente".
SEC-05: chatToLegacy() mapeia c.botPaused → bot_paused na bridge legada.
NewWhatsChat recebe campo bot_paused opcional.
SEC-08: POST /secretaria/ask valida que existe instância CONNECTED antes
de criar conversa. Retorna 503 se nenhuma instância disponível.
SEC-10: POST /api/chatbot/bots/:instanceId valida que credentialId pertence
ao mesmo tenantId antes de criar o bot. Retorna 403 se não pertencer.
SEC-12: chatBotState.resumeBot() limpa botSummary (= null) ao retomar,
evitando contexto defasado após escalação humana.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolve erros "Cannot find module" no language server do VS Code ao abrir
arquivos de plugins que importam tipos via caminho relativo do backend.
O build de produção não era afetado (tsc seguia os imports normalmente),
mas o extHost2 do editor não resolvia os arquivos fora do include.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stickers: captura automática por fileSha256 (Baileys) no MessageHandler,
tabela Prisma 'stickers' com deduplicação por [tenantId, sha256],
API REST /api/stickers (list + favorite toggle), cache IndexedDB permanente
(sem TTL — conteúdo imutável), StickerPanel.tsx com abas Recentes/Favoritos
- Emoji picker: migrado de emoji-picker-react para @emoji-mart/react com
locale="pt" (tradução completa: categorias, busca, mensagem de sem resultado)
- Preloader: aparece apenas em hard reload (F5/URL direta) via
performance.getEntriesByType('navigation').type + sessionStorage flag,
navegação client-side Next.js pula o overlay instantaneamente
- Avatar cache: Wasabi como storage persistente + IndexedDB client-side,
avatar_version via MD5(avatarUrl) sem coluna extra no banco,
rota /api/storage/view/* para servir arquivos Wasabi ao frontend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Solução 3 — Wasabi:
- Após download do CDN do WhatsApp, faz upload fire-and-forget para
Wasabi com path determinístico (avatars/{tenant}/{instance}/{jid}.jpg)
- Armazena o path relativo em contact.avatarUrl no banco
- Na próxima requisição, serve bytes direto do Wasabi sem tocar no CDN do WA
- Adiciona customPath em StorageUploadPayload e getBuffer() na interface
StorageProviderInterface/StorageProviderRegistry e plugin
Solução 4 — IndexedDB:
- Backend expõe avatarVersion (MD5 curto do avatarUrl) no response /api/chats
- Campo propagado por chatStore → chatToLegacy → NewWhatsChat
- Hook useAvatarCache (IndexedDB nativo, sem deps) armazena dataUrl do avatar
com chave jid:version — invalida automaticamente quando o avatar muda no banco
- Avatar.tsx serve do IndexedDB antes de disparar qualquer request HTTP;
converte via canvas e persiste no IndexedDB na primeira carga bem-sucedida
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static require('backend/src/...') with NODE_ENV-conditional
requires in compiled plugin JS and TypeScript sources.
In production Docker /app/backend/src/ does not exist — only /app/dist/.
Fixes MODULE_NOT_FOUND errors on UnifiedStorageProvider, ext-api,
leads, nanobana, and uploads plugins.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>