342 lines
14 KiB
Markdown
342 lines
14 KiB
Markdown
# 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)
|
||
|
||
```json
|
||
{
|
||
"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_key` → `tenantId` — 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.ts` — `httpServer: 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.js` — `PluginManager` recebe `httpServer` como 3º argumento
|
||
- `server.js` — cria `http.createServer(app)` explicitamente antes de `loadAll()`
|
||
|
||
**Admin-app:**
|
||
- `services/api.ts` — `NwService` + 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.ts` — `POST /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.ts` — `syncExpiredAvatars()`
|
||
|
||
**Satélite:**
|
||
- `services/api.ts` — `markRead`, `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.ts` — `GET /media/:messageId` com 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.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.js` — `POST /api/webhook/newwhats`
|
||
- Valida `x-nw-signature` com `timingSafeEqual` (constant-time)
|
||
- Usa `req.rawBody` (capturado pelo `verify` callback do `express.json()`)
|
||
- Lê `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
|
||
|
||
```prisma
|
||
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.ts` — `buildWebhookDispatcher(prisma, hooks)`
|
||
- `backend/prisma/schema.prisma` — modelo `ExtWebhook`
|
||
|
||
**Satélite:**
|
||
- `plugins/newwhats/webhook-receiver.js` — novo
|
||
- `plugins/newwhats/index.js` — `createWebhookReceiver()` registrado
|
||
- `server.js` — `express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8') } })`
|
||
- `services/api.ts` — `getWebhooks`, `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
|
||
|
||
```sql
|
||
-- 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
|
||
|
||
```bash
|
||
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` — coluna `ext_chat_id` + índice em `sec_conversations`
|
||
- `plugins/ext-api/backend/routes.ts` — `POST /secretaria/ask`; imports `Knex`, `PluginConfigStore`, `ProtocolEngine`
|
||
- `plugins/ext-api/index.ts` — `db` e `config` passados para `buildExtRoutes`
|
||
|
||
**Satélite:**
|
||
- `services/api.ts` — `secretariaAsk()`; 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_configs` → `plugin_id='newwhats'`, `key='webhook_secret'` |
|