Files
mercado.clube67.com/docs/fasesPluginInbox.md
T

14 KiB
Raw Blame History

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 (ws lib, 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 tabela api_keys (Prisma)
  • Rate-limit: 120 req/min por chave (in-memory, janela deslizante)
  • Multi-tenant: integration_keytenantId — eventos filtrados antes de enviar ao WS
  • hookBus: WhatsAppConnectionManager e MessageHandler emitem ext:*; plugin ext-api subscreve

Arquivos criados/modificados

Motor:

  • backend/src/core/types.tshttpServer: HttpServer no PluginContext
  • backend/src/server.ts — expõe httpServer no pluginCtx + globalThis.__whatsAppManager
  • backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts — emite ext:session.qr, ext:session.status
  • backend/src/modules/whatsapp/handlers/MessageHandler.ts — emite ext:message.new
  • plugins/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 via net)
  • plugins/newwhats/index.js — proxy REST em /api/nw/v1/*, WS em /api/nw/v1/stream
  • plugins/index.jsPluginManager recebe httpServer como 3º argumento
  • server.js — cria http.createServer(app) explicitamente antes de loadAll()

Admin-app:

  • services/api.tsNwService + interfaces NwSession, NwChat, NwMessage
  • pages/WhatsAppInbox.tsx — página de inbox
  • App.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.update via hookBus — emitido em WhatsAppConnectionManager após mudança de status de mensagem (delivered/read)
  • Filtro NOT @lid / NOT @broadcast no SELECT de inbox — elimina 3k+ chats "fantasma" gerados pelo Baileys
  • Campos pushName e senderJid adicionados ao SELECT de mensagens
  • syncExpiredAvatars() no ContactHandler — parse do parâmetro oe=<hex> das URLs do CDN WhatsApp; limpa e re-enfileira avatares que expiram em < 48h

UI — refinamentos

  • markRead ao selecionar chat, handleArchive com 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.tsPOST /inbox/:chatId/read, PATCH /inbox/:chatId/archive, filtro @lid/@broadcast, campos pushName/senderJid, message.update no hookBus
  • backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts — emite ext:message.update; chama syncExpiredAvatars() 30s após conectar
  • backend/src/modules/whatsapp/handlers/ContactHandler.tssyncExpiredAvatars()

Satélite:

  • services/api.tsmarkRead, archive; campos pushName/senderJid em NwMessage
  • pages/WhatsAppInbox.tsx — paginação infinita, grupos, smart scroll, WS message.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: inline para imagens/áudio/vídeo; attachment para documentos
  • Accept-Ranges: bytes + suporte a Range: — essencial para <audio> e <video> no browser
  • Cache-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.tsGET /media/:messageId com stream/Range/redirect

Satélite:

  • pages/WhatsAppInbox.tsxnwMediaUrl(), 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.new e ext:session.status no hookBus
  • HMAC-SHA256: x-nw-signature: sha256=<hmac> calculado sobre JSON.stringify(body)
  • Timeout de 10s por request (AbortController), dispatch paralelo (Promise.allSettled)
  • tenantId removido do payload antes de enviar

Satélite — receptor

  • plugins/newwhats/webhook-receiver.jsPOST /api/webhook/newwhats
  • Valida x-nw-signature com timingSafeEqual (constant-time)
  • Usa req.rawBody (capturado pelo verify callback do express.json())
  • webhook_secret de plugin_configs por chave webhook_secret
  • Handlers: message.new → insere em whatsapp_logs; session.status → insere em whatsapp_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 acionar prisma db push — evita drop das tabelas sec_* 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), eventos message.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ção
  • App.tsx — rota /wa-webhooks + item de menu

Arquivos criados/modificados

Motor:

  • plugins/ext-api/backend/routes.ts — CRUD /webhooks
  • plugins/ext-api/backend/webhook-dispatcher.ts — novo
  • plugins/ext-api/index.tsbuildWebhookDispatcher(prisma, hooks)
  • backend/prisma/schema.prisma — modelo ExtWebhook

Satélite:

  • plugins/newwhats/webhook-receiver.js — novo
  • plugins/newwhats/index.jscreateWebhookReceiver() registrado
  • server.jsexpress.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8') } })
  • services/api.tsgetWebhooks, createWebhook, deleteWebhook, toggleWebhook; interface NwWebhook
  • pages/WhatsAppWebhooks.tsx — novo
  • App.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

  1. A chave de contexto é "<tenantId>:<chatId>" — garante isolamento multi-tenant
  2. Motor busca sec_conversations com ext_chat_id = chave e status = 'active'
  3. Se não existir: cria conversa nova ligada ao primeiro agente ativo (ou ao agentId informado)
  4. Delega ao ProtocolEngine (plugin secretaria) — suporta OpenAI, Gemini, Anthropic, Ollama em cadeia de fallback
  5. Contexto é sumarizado automaticamente a cada 10 trocas (economia de tokens)
  6. Retorna reply, conversationId e protocolNumber para 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:0010:00...",
    "conversationId": "b4fc2124-...", "protocolNumber": "170426025928" }
# Segunda chamada reutiliza mesmo conversationId — contexto persistido ✅

Arquivos criados/modificados

Motor:

  • plugins/secretaria/migrate.ts — coluna ext_chat_id + índice em sec_conversations
  • plugins/ext-api/backend/routes.tsPOST /secretaria/ask; imports Knex, PluginConfigStore, ProtocolEngine
  • plugins/ext-api/index.tsdb e config passados para buildExtRoutes

Satélite:

  • services/api.tssecretariaAsk(); interface NwSecretariaReply
  • pages/WhatsAppInbox.tsx — estado askingAi, handler handleAskAi, botão Sparkles no InputBar

Regras transversais

  • Nunca quebrar contrato silenciosamente — mudança de payload → nova versão /v2/
  • Rate limit por integration_key obrigatório — implementado desde Fase 1 (120 req/min)
  • Isolamento multi-tenant: integration_key resolve tenantId 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: false no 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_configsplugin_id='newwhats', key='webhook_secret'