14 KiB
Fases — Plugin Inbox (NewWhats como Motor Central)
Data de início: 2026-04-17
Última atualização: 2026-04-17
Estratégia: Contrato mínimo (A) → PoC validação (B) → expansão incremental
✅ Fase 1 — Contrato Mínimo — CONCLUÍDA
Objetivo: Definir o núcleo da API e do protocolo WS antes de escrever qualquer código.
Concluída em: 2026-04-17
REST — motor /api/ext/v1/
| Endpoint | Status |
|---|---|
GET /sessions |
✅ |
GET /sessions/:id/qr |
✅ |
GET /inbox?session=&search=&limit= |
✅ |
GET /inbox/:chatId/messages?limit=&before= |
✅ |
POST /inbox/:chatId/send |
✅ |
WebSocket — WS /api/ext/v1/stream
| Evento | Emissor |
|---|---|
connected |
bridge (handshake) |
session.qr |
WhatsAppConnectionManager via hookBus |
session.status |
WhatsAppConnectionManager via hookBus |
message.new |
MessageHandler via hookBus |
Envelope padrão (contrato imutável v1)
{
"event": "session.qr",
"data": { "instanceId": "uuid", "qrBase64": "data:image/png;base64,..." },
"timestamp": 1776403129107
}
Decisões técnicas fixadas
- WebSocket nativo (
wslib,noServer: true) — sem dependência de Socket.IO client - Versionamento:
/api/ext/v1/— quebra de contrato →/v2/, nunca silenciosamente - Auth: header
x-nw-key: <integration_key>— validado contra tabelaapi_keys(Prisma) - Rate-limit: 120 req/min por chave (in-memory, janela deslizante)
- Multi-tenant:
integration_key→tenantId— eventos filtrados antes de enviar ao WS - hookBus:
WhatsAppConnectionManagereMessageHandleremitemext:*; pluginext-apisubscreve
Arquivos criados/modificados
Motor:
backend/src/core/types.ts—httpServer: HttpServernoPluginContextbackend/src/server.ts— expõehttpServernopluginCtx+globalThis.__whatsAppManagerbackend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts— emiteext:session.qr,ext:session.statusbackend/src/modules/whatsapp/handlers/MessageHandler.ts— emiteext:message.newplugins/ext-api/manifest.json,index.ts,backend/apikey-auth.ts,backend/routes.ts,backend/ws-bridge.ts
✅ Fase 2 — PoC QR + WS + Inbox — CONCLUÍDA
Objetivo: Validar fluxo end-to-end + inbox completo.
Concluída em: 2026-04-17
Entregue (absorveu escopo da Fase 3 original)
- Tela de 3 painéis no admin-app (
/wa-inbox) - Painel Sessões: lista com status badge, QR inline em tempo real
- Painel Chats: lista com preview, timestamp relativo, badge unread, busca
- Painel Mensagens: histórico, bubbles por lado, status (✓ / ✓✓ / ✓✓ ciano), envio
- Append em tempo real via WS (
message.new) - Update optimista no envio (rollback em erro)
- Reconexão automática do WS (retry a cada 3s com badge de status)
Proxy no satélite
Satélite:
plugins/newwhats/proxy.js— proxy REST (HTTP nativo) + proxy WS (TCP tunnel vianet)plugins/newwhats/index.js— proxy REST em/api/nw/v1/*, WS em/api/nw/v1/streamplugins/index.js—PluginManagerrecebehttpServercomo 3º argumentoserver.js— criahttp.createServer(app)explicitamente antes deloadAll()
Admin-app:
services/api.ts—NwService+ interfacesNwSession,NwChat,NwMessagepages/WhatsAppInbox.tsx— página de inboxApp.tsx— rota/wa-inbox+ item de menu
Fluxo validado
Browser → /api/nw/v1/sessions → satélite proxy → motor → retorna instâncias ✅
Browser → WS /api/nw/v1/stream → TCP tunnel → motor WS → handshake OK ✅
Baileys gera QR → hookBus → ws-bridge → browser QR inline ✅
QR escaneado → CONNECTED → browser recarrega chats ✅
Mensagem recebida → hookBus → browser appenda ✅
✅ Fase 3 — Inbox Refinado — CONCLUÍDA
Concluída em: 2026-04-17
Motor — novos endpoints
| Endpoint | Função |
|---|---|
POST /inbox/:chatId/read |
Zera unreadCount |
PATCH /inbox/:chatId/archive |
Arquiva/desarquiva chat |
Motor — melhorias
message.updatevia hookBus — emitido emWhatsAppConnectionManagerapós mudança de status de mensagem (delivered/read)- Filtro
NOT @lid / NOT @broadcastno SELECT de inbox — elimina 3k+ chats "fantasma" gerados pelo Baileys - Campos
pushNameesenderJidadicionados ao SELECT de mensagens syncExpiredAvatars()noContactHandler— parse do parâmetrooe=<hex>das URLs do CDN WhatsApp; limpa e re-enfileira avatares que expiram em < 48h
UI — refinamentos
markReadao selecionar chat,handleArchivecom atualização otimista da lista- Handler WS para
message.update— atualiza status da mensagem em tempo real - Paginação infinita de mensagens (scroll no topo, cursor
before=<ISO>,IntersectionObserver) - Grupos: exibição do nome do remetente (
pushName) nas mensagens - Smart scroll-to-bottom: só força descida se o chat mudou; preserva posição ao carregar mais
Arquivos modificados
Motor:
plugins/ext-api/backend/routes.ts—POST /inbox/:chatId/read,PATCH /inbox/:chatId/archive, filtro@lid/@broadcast, campospushName/senderJid,message.updateno hookBusbackend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts— emiteext:message.update; chamasyncExpiredAvatars()30s após conectarbackend/src/modules/whatsapp/handlers/ContactHandler.ts—syncExpiredAvatars()
Satélite:
services/api.ts—markRead,archive; campospushName/senderJidemNwMessagepages/WhatsAppInbox.tsx— paginação infinita, grupos, smart scroll, WSmessage.update
✅ Fase 4 — Mídia e Arquivos — CONCLUÍDA
Concluída em: 2026-04-17
Motor — endpoint de mídia
| Endpoint | Comportamento |
|---|---|
GET /media/:messageId |
Redirect 302 se URL externa (CDN/Wasabi); stream com Range se arquivo local |
Content-Disposition: inlinepara imagens/áudio/vídeo;attachmentpara documentosAccept-Ranges: bytes+ suporte aRange:— essencial para<audio>e<video>no browserCache-Control: private, max-age=3600
UI — suporte completo a tipos de mídia
| Tipo | Renderização |
|---|---|
IMAGE |
<ImageBubble> com lazy loading + skeleton |
AUDIO / PTT |
<audio controls> nativo |
VIDEO |
<video controls> com aspect-ratio |
DOCUMENT / STICKER |
link de download |
- Helper
nwMediaUrl(msg)— constrói URL via proxy/api/nw/v1/media/:id
Arquivos modificados
Motor:
plugins/ext-api/backend/routes.ts—GET /media/:messageIdcom stream/Range/redirect
Satélite:
pages/WhatsAppInbox.tsx—nwMediaUrl(),ImageBubble, players nativos por tipo
✅ Fase 5 — Webhooks — CONCLUÍDA
Objetivo: Satélite reage a eventos sem conexão WS aberta (automações de negócio).
Concluída em: 2026-04-17
Motor — CRUD de webhooks
| Endpoint | Função |
|---|---|
GET /webhooks |
Lista webhooks do tenant |
POST /webhooks |
Registra novo webhook |
DELETE /webhooks/:id |
Remove webhook |
PATCH /webhooks/:id |
Ativa/desativa webhook |
Motor — dispatcher
plugins/ext-api/backend/webhook-dispatcher.ts- Subscreve
ext:message.neweext:session.statusno hookBus - HMAC-SHA256:
x-nw-signature: sha256=<hmac>calculado sobreJSON.stringify(body) - Timeout de 10s por request (
AbortController), dispatch paralelo (Promise.allSettled) tenantIdremovido do payload antes de enviar
Satélite — receptor
plugins/newwhats/webhook-receiver.js—POST /api/webhook/newwhats- Valida
x-nw-signaturecomtimingSafeEqual(constant-time) - Usa
req.rawBody(capturado peloverifycallback doexpress.json()) - Lê
webhook_secretdeplugin_configspor chavewebhook_secret - Handlers:
message.new→ insere emwhatsapp_logs;session.status→ insere emwhatsapp_logs
Motor — modelo de dados
model ExtWebhook {
id String @id @default(uuid())
tenantId String
url String
events String[] // ["message.new", "session.status"]
secret String
active Boolean @default(true)
createdAt DateTime @default(now())
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@map("ext_webhooks")
}
Tabela criada via SQL direto (
CREATE TABLE IF NOT EXISTS) para não acionarprisma db push— evita drop das tabelassec_*com dados.
Configuração ativa (produção)
- Secret armazenado em
plugin_configs:webhook_secret = e75c28beaf1fe5f68330364c90eb13f9... - Webhook registrado no motor:
POST /api/webhook/newwhats(127.0.0.1:4001), eventosmessage.new+session.status, ativo - Teste end-to-end confirmado:
{"ok":true}✅
UI — página de gerenciamento
pages/WhatsAppWebhooks.tsx— listagem, criação, toggle ativo/inativo, exclusão com dupla confirmaçãoApp.tsx— rota/wa-webhooks+ item de menu
Arquivos criados/modificados
Motor:
plugins/ext-api/backend/routes.ts— CRUD/webhooksplugins/ext-api/backend/webhook-dispatcher.ts— novoplugins/ext-api/index.ts—buildWebhookDispatcher(prisma, hooks)backend/prisma/schema.prisma— modeloExtWebhook
Satélite:
plugins/newwhats/webhook-receiver.js— novoplugins/newwhats/index.js—createWebhookReceiver()registradoserver.js—express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8') } })services/api.ts—getWebhooks,createWebhook,deleteWebhook,toggleWebhook; interfaceNwWebhookpages/WhatsAppWebhooks.tsx— novoApp.tsx— rota + menu
✅ Fase 6 — Secretária Virtual — CONCLUÍDA
Objetivo: IA atende mensagens WhatsApp com contexto persistido por conversa.
Concluída em: 2026-04-17
Motor — endpoint
| Endpoint | Função |
|---|---|
POST /secretaria/ask |
Envia mensagem ao ProtocolEngine; retorna resposta da IA |
Body: { chatId: string, message: string, contactName?: string, agentId?: string }
Response: { reply: string, conversationId: string, protocolNumber: string }
Como funciona
- A chave de contexto é
"<tenantId>:<chatId>"— garante isolamento multi-tenant - Motor busca
sec_conversationscomext_chat_id = chaveestatus = 'active' - Se não existir: cria conversa nova ligada ao primeiro agente ativo (ou ao
agentIdinformado) - Delega ao
ProtocolEngine(pluginsecretaria) — suporta OpenAI, Gemini, Anthropic, Ollama em cadeia de fallback - Contexto é sumarizado automaticamente a cada 10 trocas (economia de tokens)
- Retorna
reply,conversationIdeprotocolNumberpara rastreabilidade
Migração de schema
-- Adicionado a sec_conversations via alterTable idempotente em secretaria/migrate.ts
ALTER TABLE sec_conversations ADD COLUMN IF NOT EXISTS ext_chat_id VARCHAR(300);
CREATE INDEX IF NOT EXISTS idx_sec_conv_ext_chat ON sec_conversations (ext_chat_id);
UI — botão "Secretária IA" no inbox
- Botão roxo ✨ (Sparkles) aparece na barra de composição quando há texto digitado
- Ao clicar: envia o texto atual como pergunta à IA, preenche o campo com a resposta
- O agente humano revisa/edita antes de enviar via WhatsApp
- Estado de loading com spinner animado durante a chamada
Teste end-to-end confirmado
POST /api/ext/v1/secretaria/ask
x-nw-key: nw_test_alemao_integration_key_001
{ "chatId": "5511999999999@s.whatsapp.net", "message": "Quero agendar uma consulta" }
→ { "reply": "Oi! Temos horários disponíveis na sexta-feira, 17 de abril: 09:00–10:00...",
"conversationId": "b4fc2124-...", "protocolNumber": "170426025928" }
# Segunda chamada reutiliza mesmo conversationId — contexto persistido ✅
Arquivos criados/modificados
Motor:
plugins/secretaria/migrate.ts— colunaext_chat_id+ índice emsec_conversationsplugins/ext-api/backend/routes.ts—POST /secretaria/ask; importsKnex,PluginConfigStore,ProtocolEngineplugins/ext-api/index.ts—dbeconfigpassados parabuildExtRoutes
Satélite:
services/api.ts—secretariaAsk(); interfaceNwSecretariaReplypages/WhatsAppInbox.tsx— estadoaskingAi, handlerhandleAskAi, botão Sparkles noInputBar
Regras transversais
- Nunca quebrar contrato silenciosamente — mudança de payload → nova versão
/v2/ - Rate limit por
integration_keyobrigatório — ✅ implementado desde Fase 1 (120 req/min) - Isolamento multi-tenant:
integration_keyresolvetenantId— ✅ implementado - Toda funcionalidade nova como plugin no motor (
/plugins/ext-api/) - Compilar TypeScript após cada mudança:
cd /home/deploy/projetos/newwhats.local/plugins && npx tsc -p tsconfig.json noEmitOnError: falseno tsconfig — erros em plugins não relacionados não bloqueiam a compilação
Referências rápidas
| Item | Valor |
|---|---|
| Motor | /home/deploy/projetos/newwhats.local |
| Satélite | /home/deploy/projetos/alemaoconveniencias.local |
| Plugin ext-api | newwhats.local/plugins/ext-api/ |
| Plugin newwhats (satélite) | sistema-nuvem/plugins/newwhats/ |
| Plugin secretaria | newwhats.local/plugins/secretaria/ |
| Serviço motor | newwhats-backend.service (porta 8008) |
| Serviço satélite | alemaoconveniencias.service (porta 4001) |
| Admin panel | http://alemaoconveniencias.local:4001/admin-v2/ |
| API key de teste | nw_test_alemao_integration_key_001 (tenant: ruibto@gmail.com) |
| Webhook secret | plugin_configs → plugin_id='newwhats', key='webhook_secret' |