Files

342 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()`)
-`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: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.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'` |