feat: sincronização de desenvolvimento local da VPS 4 para o Gitea
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
# Baileys Engine Swap — Plano de Implementação
|
||||
|
||||
**Data:** 2026-04-29
|
||||
**Contexto:** Mensagens ricas (botões, lista, carrossel) não renderizam no WhatsApp Web com `@whiskeysockets/baileys` padrão. A solução é usar o fork InfiniteAPI que implementa esses tipos nativamente dentro do `sendMessage()`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problema Raiz
|
||||
|
||||
O `@whiskeysockets/baileys` v6.7.18 (oficial) **não suporta** os campos:
|
||||
- `nativeButtons`
|
||||
- `nativeList`
|
||||
- `nativeCarousel`
|
||||
|
||||
O WhatsApp Web exige nós binários adicionais (`<biz><interactive>`, `<bot biz_bot="1"/>`) injetados no stanza via `relayMessage(additionalNodes)`. O `rich-message.ts` atual tenta replicar isso manualmente, mas **ainda falha no WhatsApp Web**.
|
||||
|
||||
**Erro observado:** "Não foi possível carregar a mensagem. Use seu celular para acessá-la."
|
||||
|
||||
### Por que o InfiniteAPI funciona
|
||||
|
||||
O fork `github:rsalcara/InfiniteAPI` (v7.0.0-rc.9, package name: `baileys`) implementa toda a lógica de `additionalNodes` internamente no `generateWAMessageContent()` / `relayMessage()`. O dev só chama `sock.sendMessage(jid, { nativeButtons: [...] })` e o fork faz o resto.
|
||||
|
||||
### Por que whatsmeow (Go) NÃO resolve
|
||||
|
||||
Pesquisa confirmada (abril 2026):
|
||||
- `InteractiveMessage` + `NativeFlowMessage` → struct existe, botões **não aparecem** (issue #711, sem solução)
|
||||
- `ListMessage` → **não exibe** na conversa do destinatário (issue #669)
|
||||
- `CarouselMessage` → **não existe** (exclusivo da Cloud API)
|
||||
- Migração seria ~2–3 meses de reescrita para o mesmo resultado quebrado
|
||||
|
||||
---
|
||||
|
||||
## 2. Solução Escolhida: Engine Swap com Fallback
|
||||
|
||||
Instalar **os dois** packages (nomes diferentes, convivem no mesmo `node_modules`) e criar uma camada de abstração `engine/` que permite trocar via variável de ambiente + restart automático.
|
||||
|
||||
**Downtime por troca:** ~5–10 segundos (restart + reconexão automática, sem novo QR — auth state no disco é compatível entre os dois, InfiniteAPI é fork direto).
|
||||
|
||||
---
|
||||
|
||||
## 3. Estrutura de Arquivos
|
||||
|
||||
```
|
||||
backend/
|
||||
src/
|
||||
modules/whatsapp/
|
||||
engine/
|
||||
official.ts ← re-exporta @whiskeysockets/baileys
|
||||
infinite.ts ← re-exporta baileys (InfiniteAPI)
|
||||
index.ts ← exporta conforme process.env.BAILEYS_ENGINE
|
||||
connection/
|
||||
WhatsAppConnectionManager.ts ← muda import para '../engine'
|
||||
handlers/
|
||||
MessageHandler.ts ← muda import para '../../engine' (ou relativo)
|
||||
ContactHandler.ts ← idem
|
||||
rich-message.ts ← simplificado + fallback comentado
|
||||
modules/chatbot/
|
||||
chatbot.service.ts ← muda import
|
||||
shared/utils/
|
||||
whatsapp.ts ← muda import
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementação Passo a Passo
|
||||
|
||||
### 4.1 Instalar os dois packages
|
||||
|
||||
```bash
|
||||
cd /home/deploy/projetos/newwhats.local/backend
|
||||
|
||||
# Manter o oficial
|
||||
# @whiskeysockets/baileys já está em package.json
|
||||
|
||||
# Adicionar InfiniteAPI com alias para não conflitar
|
||||
npm install baileys@github:rsalcara/InfiniteAPI
|
||||
```
|
||||
|
||||
Ambos convivem no `node_modules` porque têm nomes diferentes:
|
||||
- `node_modules/@whiskeysockets/baileys/`
|
||||
- `node_modules/baileys/`
|
||||
|
||||
### 4.2 Criar `engine/official.ts`
|
||||
|
||||
```typescript
|
||||
// Re-exporta tudo do Baileys oficial
|
||||
export * from '@whiskeysockets/baileys'
|
||||
export { proto } from '@whiskeysockets/baileys'
|
||||
```
|
||||
|
||||
### 4.3 Criar `engine/infinite.ts`
|
||||
|
||||
```typescript
|
||||
// Re-exporta tudo do InfiniteAPI
|
||||
export * from 'baileys'
|
||||
export { proto } from 'baileys'
|
||||
```
|
||||
|
||||
### 4.4 Criar `engine/index.ts`
|
||||
|
||||
```typescript
|
||||
// Seleciona engine conforme BAILEYS_ENGINE (padrão: infinite)
|
||||
// IMPORTANTE: process.env lido em tempo de inicialização do módulo.
|
||||
// Para trocar a engine: alterar BAILEYS_ENGINE + reiniciar o processo.
|
||||
const engine = process.env.BAILEYS_ENGINE ?? 'infinite'
|
||||
|
||||
if (engine === 'infinite') {
|
||||
module.exports = require('./infinite')
|
||||
} else {
|
||||
module.exports = require('./official')
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota TypeScript:** usar `require()` condicional ou um `export *` com re-export dinâmico.
|
||||
> Alternativa mais simples: dois arquivos de entrada separados e o `tsconfig` aponta para um deles via path alias.
|
||||
|
||||
### 4.5 Trocar imports nos 6 arquivos
|
||||
|
||||
| Arquivo | Import atual | Novo import |
|
||||
|---|---|---|
|
||||
| `WhatsAppConnectionManager.ts` | `@whiskeysockets/baileys` | `../engine` |
|
||||
| `handlers/MessageHandler.ts` | `@whiskeysockets/baileys` | `../../engine` |
|
||||
| `handlers/ContactHandler.ts` | `@whiskeysockets/baileys` | `../../engine` |
|
||||
| `rich-message.ts` | `@whiskeysockets/baileys` (3 imports) | `./engine` |
|
||||
| `modules/chatbot/chatbot.service.ts` | `@whiskeysockets/baileys` | `../whatsapp/engine` |
|
||||
| `shared/utils/whatsapp.ts` | (verificar) | path relativo para engine |
|
||||
|
||||
### 4.6 Simplificar `rich-message.ts` para InfiniteAPI
|
||||
|
||||
Com InfiniteAPI, o `sendRichMessage()` fica:
|
||||
|
||||
```typescript
|
||||
export async function sendRichMessage(
|
||||
sock: WASocket,
|
||||
jid: string,
|
||||
payload: RichPayload,
|
||||
quoted?: { key: proto.IMessageKey; message: proto.IMessage },
|
||||
): Promise<string> {
|
||||
const { text, footer, nativeButtons, nativeList, nativeCarousel, poll } = payload
|
||||
|
||||
if (poll) {
|
||||
const sent = await sock.sendMessage(jid, {
|
||||
poll: { name: poll.name, values: poll.values, selectableCount: poll.selectableCount ?? 1 },
|
||||
}, quoted ? { quoted } : undefined)
|
||||
return sent?.key?.id ?? `poll-${Date.now()}`
|
||||
}
|
||||
|
||||
if (nativeButtons) {
|
||||
const sent = await sock.sendMessage(jid, {
|
||||
text: text ?? '',
|
||||
footer,
|
||||
nativeButtons, // InfiniteAPI trata tudo internamente
|
||||
} as any, quoted ? { quoted } : undefined)
|
||||
return sent?.key?.id ?? `btn-${Date.now()}`
|
||||
}
|
||||
|
||||
if (nativeList) {
|
||||
const sent = await sock.sendMessage(jid, {
|
||||
text: text ?? '',
|
||||
footer,
|
||||
nativeList,
|
||||
} as any, quoted ? { quoted } : undefined)
|
||||
return sent?.key?.id ?? `list-${Date.now()}`
|
||||
}
|
||||
|
||||
if (nativeCarousel) {
|
||||
const sent = await sock.sendMessage(jid, {
|
||||
text: text ?? '',
|
||||
footer,
|
||||
nativeCarousel,
|
||||
} as any, quoted ? { quoted } : undefined)
|
||||
return sent?.key?.id ?? `carousel-${Date.now()}`
|
||||
}
|
||||
|
||||
// Texto simples
|
||||
const sent = await sock.sendMessage(jid, { text: text ?? '' }, quoted ? { quoted } : undefined)
|
||||
return sent?.key?.id ?? `text-${Date.now()}`
|
||||
}
|
||||
```
|
||||
|
||||
> **Fallback:** Manter o código atual do proto manual em `rich-message.official.ts` caso precise debugar com a engine oficial.
|
||||
|
||||
### 4.7 Adicionar BAILEYS_ENGINE ao .env
|
||||
|
||||
```bash
|
||||
# /home/deploy/projetos/newwhats.local/backend/.env
|
||||
BAILEYS_ENGINE=infinite # ou: official
|
||||
```
|
||||
|
||||
### 4.8 Endpoint de toggle no painel admin
|
||||
|
||||
**Backend** — novo endpoint em `whatsapp.routes.ts` ou `admin.routes.ts`:
|
||||
|
||||
```typescript
|
||||
// POST /api/admin/engine
|
||||
// Body: { engine: 'infinite' | 'official' }
|
||||
router.post('/engine', adminOnly, async (req, res) => {
|
||||
const { engine } = req.body
|
||||
if (!['infinite', 'official'].includes(engine)) return res.status(400).json({ error: 'invalid engine' })
|
||||
|
||||
// 1. Persiste no banco (tabela config ou settings)
|
||||
await prisma.config.upsert({
|
||||
where: { key: 'BAILEYS_ENGINE' },
|
||||
update: { value: engine },
|
||||
create: { key: 'BAILEYS_ENGINE', value: engine },
|
||||
})
|
||||
|
||||
// 2. Reinicia o processo (pm2 ou systemctl)
|
||||
// Usar exec() com o comando correto para o ambiente
|
||||
exec('pm2 restart newwhats-backend', (err) => {
|
||||
if (err) return res.status(500).json({ error: 'restart failed' })
|
||||
res.json({ ok: true, engine })
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/admin/engine — retorna engine ativa
|
||||
router.get('/engine', adminOnly, async (req, res) => {
|
||||
const config = await prisma.config.findUnique({ where: { key: 'BAILEYS_ENGINE' } })
|
||||
res.json({ engine: config?.value ?? process.env.BAILEYS_ENGINE ?? 'infinite' })
|
||||
})
|
||||
```
|
||||
|
||||
**Frontend** — componente simples na página de administração:
|
||||
|
||||
```tsx
|
||||
// Toggle na página /admin/settings ou /admin/system
|
||||
<div>
|
||||
<span>Engine WhatsApp</span>
|
||||
<select value={engine} onChange={handleSwitch}>
|
||||
<option value="infinite">InfiniteAPI (botões/lista/carrossel)</option>
|
||||
<option value="official">Baileys Oficial (estável, sem botões)</option>
|
||||
</select>
|
||||
<span className="text-yellow-500">⚠ Troca reinicia o servidor (~10s)</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Tipos Suportados por Engine
|
||||
|
||||
| Tipo de Mensagem | Baileys Oficial | InfiniteAPI |
|
||||
|---|---|---|
|
||||
| Texto simples | ✅ | ✅ |
|
||||
| Reply / Quote | ✅ | ✅ |
|
||||
| Menções (@) | ✅ | ✅ |
|
||||
| Mídia (foto/vídeo/doc/áudio) | ✅ | ✅ |
|
||||
| Enquete (poll) | ✅ | ✅ |
|
||||
| Botões (`nativeButtons`) — reply, url, copy, call | ❌ (quebrado no Web) | ✅ |
|
||||
| Lista (`nativeList`) | ❌ (quebrado no Web) | ✅ |
|
||||
| Carrossel (`nativeCarousel`) | ❌ (quebrado no Web) | ✅ |
|
||||
| Botão com header imagem/vídeo | ❌ | ✅ (`headerImage`, `headerVideo`) |
|
||||
|
||||
### Tipos de botão suportados pelo InfiniteAPI
|
||||
|
||||
```typescript
|
||||
type NativeButton =
|
||||
| { type: 'reply'; id: string; text: string } // Quick reply
|
||||
| { type: 'url'; text: string; url: string } // Abrir URL
|
||||
| { type: 'copy'; text: string; copyText: string } // Copiar texto
|
||||
| { type: 'call'; text: string; phoneNumber: string } // Ligar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Riscos e Mitigações
|
||||
|
||||
| Risco | Mitigação |
|
||||
|---|---|
|
||||
| InfiniteAPI é RC (não estável) | Engine swap — volta pro oficial em 10s |
|
||||
| Repo `github:rsalcara/InfiniteAPI` pode sair do ar | Fazer fork próprio no GitHub interno |
|
||||
| Breaking changes na API do fork | Wrapper `engine/` isola o impacto |
|
||||
| Auth state incompatível após troca | Não é problema — InfiniteAPI usa o mesmo formato `useMultiFileAuthState` |
|
||||
| TypeScript: tipos diferentes entre engines | Usar `as any` nos campos extras do InfiniteAPI |
|
||||
|
||||
---
|
||||
|
||||
## 7. Estimativa de Esforço
|
||||
|
||||
| Tarefa | Tempo |
|
||||
|---|---|
|
||||
| Instalar packages + criar `engine/` | 30 min |
|
||||
| Trocar imports nos 6 arquivos | 30 min |
|
||||
| Simplificar `rich-message.ts` | 30 min |
|
||||
| Endpoint admin + frontend toggle | 2 horas |
|
||||
| Teste real (celular + Web) | 1 hora |
|
||||
| **Total** | **~4–5 horas** |
|
||||
|
||||
---
|
||||
|
||||
## 8. Arquivos-Chave para Referência
|
||||
|
||||
| Arquivo | Descrição |
|
||||
|---|---|
|
||||
| `backend/src/modules/whatsapp/rich-message.ts` | Lógica atual de envio rico (proto manual) |
|
||||
| `backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts` | Core da conexão Baileys (1265 linhas) |
|
||||
| `backend/src/modules/whatsapp/handlers/MessageHandler.ts` | Processa mensagens recebidas (865 linhas) |
|
||||
| `backend/src/modules/whatsapp/whatsapp.send.routes.ts` | Rotas de envio (757 linhas) |
|
||||
| `/home/deploy/projetos/whatsbotoes/baileys_interactive/` | Projeto de referência usando InfiniteAPI |
|
||||
| `/home/deploy/projetos/whatsbotoes/tipos_de_disparo.md` | Documentação dos tipos de disparo |
|
||||
| `node_modules/baileys/lib/Types/Message.d.ts` | Tipos do InfiniteAPI (após instalar) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Compilar backend
|
||||
cd /home/deploy/projetos/newwhats.local/backend
|
||||
npx tsc --skipLibCheck
|
||||
|
||||
# Compilar só rich-message
|
||||
npx tsc src/modules/whatsapp/rich-message.ts --outDir dist/modules/whatsapp --skipLibCheck --esModuleInterop --module commonjs --moduleResolution node --target es2020
|
||||
|
||||
# Reiniciar backend
|
||||
pm2 restart newwhats-backend # ou o nome do processo
|
||||
|
||||
# Ver engine ativa
|
||||
grep BAILEYS_ENGINE .env
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
# Continuar — NewWhats como Motor Central (SaaS multi-tenant)
|
||||
|
||||
## Instrução inicial para a próxima sessão
|
||||
|
||||
> Faça uma **análise profissional** da ideia descrita abaixo. Antes de escrever qualquer código:
|
||||
> 1. Gere uma **lista de tarefas detalhada** (fases, dependências, critérios de aceite) e salve em `TAREFAS.md`.
|
||||
> 2. Mantenha um **relatório vivo** de todas as implementações em `RELATORIO.md` (o que foi feito, decisões técnicas, pontos de atenção, próximos passos).
|
||||
> 3. **Regra-ouro:** toda funcionalidade nova deve ser pensada e entregue **como plugin** (em `/plugins/<nome>/` com `manifest.json`, hooks, config_schema). Nada vai direto ao core se couber como plugin.
|
||||
|
||||
---
|
||||
|
||||
## Contexto do projeto
|
||||
|
||||
- **Projeto motor:** `/home/deploy/projetos/newwhats.local` — já possui Baileys (WhatsApp), inbox, sessões, storage (Wasabi), sistema de plugins estilo WordPress, e API de pareamento OTP (`/api/pair/*`) com `integration_key`.
|
||||
- **Projetos satélites:** múltiplos sistemas (ex.: Alemão Conveniências em `/home/deploy/projetos/alemaoconveniencias.local`) que **não devem instalar Baileys**. Devem consumir o motor via API autenticada por `integration_key`.
|
||||
- **Já implementado:** pair system (OTP por e-mail), plugin `newwhats` no Alemão com `x-nw-key` auth, modal de pareamento em 3 passos no dashboard.
|
||||
- **Falta:** expor inbox, sessões e secretária virtual como serviço consumível pelos satélites.
|
||||
|
||||
## Ideia a analisar
|
||||
|
||||
Transformar o NewWhats em **motor SaaS multi-tenant**, onde cada projeto satélite é um cliente leve que consome:
|
||||
- **Inbox** (chats, mensagens, envio, mídias)
|
||||
- **Sessões** (status WA, QR, pareamento de números)
|
||||
- **Secretária virtual** (IA com contexto — roda 100% no motor, satélite só consome output)
|
||||
|
||||
## Arquitetura-alvo
|
||||
|
||||
```
|
||||
NewWhats (motor)
|
||||
├─ Baileys + Inbox + Storage + Secretária (IA)
|
||||
└─ API /api/ext/* (auth: x-nw-key)
|
||||
│
|
||||
▼ HTTPS + WebSocket
|
||||
┌───────┴────────┬──────────┐
|
||||
Alemão Projeto B Projeto C
|
||||
(plugin (plugin (plugin
|
||||
nw-client) nw-client) nw-client)
|
||||
```
|
||||
|
||||
## Decisões propostas (validar antes de implementar)
|
||||
|
||||
1. **Plugin `nw-client` portável** em `/plugins-sdk/newwhats-client/` — reutilizável em todos os satélites, zero dependência de Baileys.
|
||||
2. **API `/api/ext/*` nova** (além de `/api/pair/*` já existente), autenticada por `x-nw-key`:
|
||||
- `GET /api/ext/sessions`
|
||||
- `GET /api/ext/inbox?session=X`
|
||||
- `GET /api/ext/inbox/:chatId/messages`
|
||||
- `POST /api/ext/inbox/:chatId/send`
|
||||
- `POST /api/ext/secretaria/ask`
|
||||
- `WS /api/ext/stream` (eventos realtime — fallback polling 5s)
|
||||
3. **Secretária virtual sempre no motor** — centraliza custo LLM e contexto.
|
||||
4. **Isolamento multi-tenant** — `integration_key` resolve para `user_id` + `allowed_sessions[]`. Satélite nunca vê dados de outro tenant.
|
||||
5. **Distribuição do client plugin** — opções:
|
||||
- (a) `npm publish @newwhats/client` (registry privado/público)
|
||||
- (b) git submodule
|
||||
- (c) **CLI `nw plugin install`** que baixa tarball assinado do motor ← recomendado
|
||||
6. **Tudo como plugin:**
|
||||
- Motor: `plugins/ext-api/` (expõe `/api/ext/*`), `plugins/secretaria-virtual/` (IA), `plugins/realtime-bridge/` (WS).
|
||||
- Satélite: `plugins/newwhats-client/` (consumidor único, com UI de inbox embutida).
|
||||
|
||||
## Tradeoffs a documentar no relatório
|
||||
|
||||
- **WS vs polling** — WS é melhor para UX, mas adiciona complexidade de reconexão/auth. V1 polling 5s, V2 WS.
|
||||
- **Proxy de mídias** — satélite baixa mídia via motor (`/api/ext/media/:id`) ou recebe URL assinada Wasabi? URL assinada é mais leve mas expõe storage.
|
||||
- **Rate limit por integration_key** — necessário desde o v1 para proteger motor.
|
||||
- **Versionamento da API** — `/api/ext/v1/*` desde o início.
|
||||
|
||||
## Pendências herdadas (do `project_alemao_plugin_session.md`)
|
||||
|
||||
- `aria-hidden` warning no pairModal (Bootstrap 5 — usar `inert` ou Bootstrap 5.3+).
|
||||
- Inbox no Alemão via plugin — **esta é a primeira entrega desta nova fase**.
|
||||
- SMTP não configurado no NewWhats — dev_code exposto na resposta até configurar.
|
||||
|
||||
## Entregáveis esperados da próxima sessão
|
||||
|
||||
1. `TAREFAS.md` — backlog fase-a-fase (motor → SDK → satélite → secretária).
|
||||
2. `RELATORIO.md` — iniciado com análise profissional da ideia, riscos, e decisões arquiteturais.
|
||||
3. Esqueleto dos plugins (manifests + hooks declarados, sem lógica ainda) para validar a forma antes de implementar.
|
||||
4. Pergunta ao usuário: **(A)** contrato detalhado da API primeiro, ou **(B)** PoC end-to-end de `/api/ext/inbox` no Alemão para validar fluxo.
|
||||
|
||||
## Referências rápidas
|
||||
|
||||
- Sistema de plugins: `backend/src/core/` (types, hook-bus, plugin-loader, plugin-registry)
|
||||
- Pair system: `backend/src/modules/pair/pair.routes.ts` + `pair.service.ts`
|
||||
- Plugin consumidor existente: `/home/deploy/projetos/alemaoconveniencias.local/sistema-nuvem/plugins/newwhats/`
|
||||
- Build backend: `npm run build` em `/home/deploy/projetos/newwhats.local/backend`
|
||||
- Serviços: `newwhats-backend.service` (8008), `alemaoconveniencias.service` (4001)
|
||||
@@ -0,0 +1,201 @@
|
||||
# Correção Wasabi Storage — newwhats
|
||||
|
||||
Data: 2026-05-07
|
||||
Investigado e corrigido por: Claude Code
|
||||
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
O plugin `UnifiedStorageProvider` estava configurado para enviar mídias do WhatsApp ao Wasabi, porém os arquivos continuavam acumulando localmente em `backend/media`, chegando a **7.8 GB** de mídia que deveria estar na nuvem. A causa raiz foi uma combinação de 4 bugs no código e uma inconsistência de path descoberta na revisão final.
|
||||
|
||||
---
|
||||
|
||||
## Diagnóstico — 4 bugs + 1 inconsistência corrigidos
|
||||
|
||||
### Bug 1 — Arquivo local nunca era deletado após upload Wasabi (crítico)
|
||||
**Arquivo:** `backend/src/modules/whatsapp/handlers/MessageHandler.ts`
|
||||
|
||||
O fluxo era:
|
||||
1. Arquivo de mídia chegava → salvo em `backend/media/{instanceId}/{msgId}.ext` ✅
|
||||
2. StorageProvider registrado → upload para Wasabi ✅
|
||||
3. ❌ **Arquivo local NUNCA era deletado após upload bem-sucedido**
|
||||
|
||||
Resultado: disco enchia indefinidamente mesmo com Wasabi funcionando.
|
||||
|
||||
**Correção:** Após upload bem-sucedido (`provider !== 'local_fallback'`), o arquivo local é removido com `fs.unlink()` assíncrono.
|
||||
|
||||
---
|
||||
|
||||
### Bug 2 — `mediaPath` apontava para arquivo local inexistente (crítico)
|
||||
**Arquivo:** `backend/src/modules/whatsapp/handlers/MessageHandler.ts`
|
||||
|
||||
Mesmo após o upload para Wasabi, o campo `mediaPath` no banco continuava sendo salvo com o caminho local completo (`/home/deploy/.../media/...`), que após o Bug 1 corrigido deixa de existir no disco.
|
||||
|
||||
**Correção:** Quando `uploadedToRemote = true`, `mediaPath` é salvo como `null` no banco. O acesso à mídia passa a ser exclusivamente pelo `mediaUrl` que aponta para o Wasabi.
|
||||
|
||||
---
|
||||
|
||||
### Bug 3 — Rotas HTTP duplicadas ao reativar plugin via admin (médio)
|
||||
**Arquivo:** `plugins/UnifiedStorageProvider/index.ts`
|
||||
|
||||
O `pluginRegistry.reactivate()` chama `activate()` novamente a cada salvamento de config. O `activate()` registrava as rotas Express (`/api/storage/view/*`, `/api/storage/wasabi/list-buckets`, etc.) a cada chamada, acumulando handlers duplicados em memória.
|
||||
|
||||
**Correção:** Adicionada flag `private routesRegistered = false`. As rotas Express são registradas apenas na primeira chamada de `activate()`. Reativações subsequentes reinicializam apenas o S3 client e o registro do StorageProvider.
|
||||
|
||||
---
|
||||
|
||||
### Bug 4 — Deleção de instância não limpava mídias no Wasabi (funcionalidade ausente)
|
||||
**Arquivo:** `backend/src/modules/whatsapp/whatsapp.routes.ts`
|
||||
|
||||
Ao deletar uma instância pelo admin, o fluxo só desconectava o socket Baileys, removia os arquivos de sessão e apagava o registro do banco. As mídias no Wasabi (e no fallback local) ficavam permanentemente órfãs.
|
||||
|
||||
**Implementação:**
|
||||
- Adicionado `deletePrefix(prefix: string): Promise<{ deleted: number }>` à interface `StorageProviderInterface` em `types.ts`
|
||||
- Exposto `deletePrefix()` no `StorageProviderRegistry` em `StorageProvider.ts` (silencioso se provider não registrado)
|
||||
- Implementado no plugin com `ListObjectsV2Command` paginado em batches de 1000 + `DeleteObjectsCommand`, limpando também o fallback local
|
||||
- Chamado em background na rota `DELETE /:id` após `repo.delete(id)`, sem bloquear a resposta
|
||||
|
||||
---
|
||||
|
||||
### Inconsistência 5 — Path incorreto no deletePrefix (crítico)
|
||||
**Arquivo:** `backend/src/modules/whatsapp/whatsapp.routes.ts`
|
||||
|
||||
O plugin grava arquivos no Wasabi com o path:
|
||||
```
|
||||
whatsapp/{tenantId}/{instanceId}/{fileName}
|
||||
```
|
||||
|
||||
A implementação inicial do `deletePrefix` usava:
|
||||
```
|
||||
whatsapp/{instanceId}/ ← errado, nunca encontraria os arquivos
|
||||
```
|
||||
|
||||
**Correção:** Prefix corrigido para `whatsapp/${tenantId}/${id}/`, que bate exatamente com a estrutura gravada pelo plugin.
|
||||
|
||||
---
|
||||
|
||||
## Arquivos modificados
|
||||
|
||||
| Arquivo | Mudança |
|
||||
|---|---|
|
||||
| `backend/src/core/types.ts` | Adicionado `deletePrefix()` à interface `StorageProviderInterface` |
|
||||
| `backend/src/core/StorageProvider.ts` | Exposto `deletePrefix()` no registry com fallback silencioso |
|
||||
| `backend/src/modules/whatsapp/handlers/MessageHandler.ts` | `fs.unlink()` após upload + `mediaPath = null` quando remoto |
|
||||
| `backend/src/modules/whatsapp/whatsapp.routes.ts` | `deletePrefix("whatsapp/{tenantId}/{id}/")` ao deletar instância |
|
||||
| `plugins/UnifiedStorageProvider/index.ts` | Flag `routesRegistered` + import `DeleteObjectsCommand` + implementação de `deletePrefix()` |
|
||||
|
||||
---
|
||||
|
||||
## Configuração atual (verificada)
|
||||
|
||||
```json
|
||||
// DragonflyDB — plugin:config:UnifiedStorageProvider
|
||||
{
|
||||
"wasabiAccessKey": "OFLCYUUZRYJFKHLB81QC",
|
||||
"wasabiSecretKey": "R4GI2s7W...",
|
||||
"wasabiBucket": "whatsbasemidia",
|
||||
"wasabiRegion": "us-east-1"
|
||||
}
|
||||
```
|
||||
|
||||
Endpoint padrão: `https://s3.wasabisys.com`
|
||||
|
||||
---
|
||||
|
||||
## Situação das mídias antigas (pós-investigação)
|
||||
|
||||
| Situação | Quantidade | Ação |
|
||||
|---|---|---|
|
||||
| Migradas para Wasabi pelo script | 231 arquivos | ✅ no Wasabi (path: `whatsapp/{instanceId}/`) |
|
||||
| Deletadas do servidor após migração | 231 arquivos | ✅ removidas de `backend/media` |
|
||||
| Banco atualizado (mediaUrl → Wasabi) | 231 registros | ✅ `mediaPath = null`, `mediaUrl = whatsapp/...` |
|
||||
| Ainda locais em `backend/media` | ~1.430 registros | Serão refeitos ao reconectar sessões |
|
||||
| Novas mídias (pós-correção) | todas | ✅ vão direto para Wasabi e local é deletado |
|
||||
|
||||
> ⚠️ Os 231 arquivos migrados pelo script têm path `whatsapp/{instanceId}/` (sem tenantId) pois foram lidos direto do disco. O plugin novo grava como `whatsapp/{tenantId}/{instanceId}/`. Isso não quebra o funcionamento mas o `deletePrefix` automático não os apagará quando a instância for deletada — precisarão ser removidos manualmente do bucket se necessário.
|
||||
|
||||
---
|
||||
|
||||
## Script de migração criado
|
||||
|
||||
**Arquivo:** `migrate-media-wasabi.mjs` (raiz do projeto)
|
||||
|
||||
Script Node.js criado para migrar os arquivos acumulados em `backend/media` para o Wasabi. Suporta `--dry-run` para simulação sem alterações.
|
||||
|
||||
**O que o script faz por arquivo:**
|
||||
1. Lê `messages` do banco onde `mediaUrl LIKE '/media/%'`
|
||||
2. Verifica se o arquivo já existe no Wasabi (`HeadObjectCommand`) — pula upload se sim
|
||||
3. Faz upload para `whatsapp/{instanceId}/{fileName}` no bucket `whatsbasemidia`
|
||||
4. Atualiza o banco: `mediaUrl = 'whatsapp/...'`, `mediaPath = NULL`
|
||||
5. Remove o arquivo local com `fs.unlinkSync()`
|
||||
6. Ao final, remove diretórios vazios de `backend/media`
|
||||
|
||||
**Execução:**
|
||||
```bash
|
||||
# Simulação (sem alterar nada):
|
||||
node migrate-media-wasabi.mjs --dry-run
|
||||
|
||||
# Migração real:
|
||||
node migrate-media-wasabi.mjs
|
||||
```
|
||||
|
||||
**Status da execução em 2026-05-07:**
|
||||
- Migração iniciada e interrompida manualmente aos 231/1.661 arquivos
|
||||
- Motivo da parada: decisão de deletar sessões manualmente e reconectar para que as mídias sejam baixadas novamente do WhatsApp já com o Wasabi funcionando
|
||||
|
||||
---
|
||||
|
||||
## Backup gerado antes das operações
|
||||
|
||||
**Arquivo:** `/home/deploy/projetos/newwhats_export/newwhats_completo.tar.gz` — **7.7 GB**
|
||||
**Dump do banco:** `/home/deploy/projetos/newwhats_export/newwhats_dump.sql` — **18 MB**
|
||||
|
||||
O tar.gz contém o projeto completo (incluindo `backend/media` com todas as mídias locais no estado anterior à migração) e o dump PostgreSQL. Gerado em 2026-05-07 como snapshot de segurança antes de qualquer alteração.
|
||||
|
||||
---
|
||||
|
||||
## Fluxo completo após as correções
|
||||
|
||||
### Upload de nova mídia:
|
||||
```
|
||||
WhatsApp recebe mídia
|
||||
→ salva buffer temporariamente em backend/media/{instanceId}/{msgId}.ext
|
||||
→ storageProvider.uploadFile() → Wasabi: whatsapp/{tenantId}/{instanceId}/{fileName}
|
||||
→ fs.unlink(localFile) ← arquivo local removido
|
||||
→ banco: mediaPath = null, mediaUrl = whatsapp/{tenantId}/{instanceId}/{fileName}
|
||||
→ frontend acessa via /api/storage/view/whatsapp/... (proxy → Wasabi)
|
||||
```
|
||||
|
||||
### Se Wasabi falhar:
|
||||
```
|
||||
→ arquivo local mantido em backend/media
|
||||
→ banco: mediaPath = /caminho/local, mediaUrl = /media/{instanceId}/{fileName}
|
||||
→ SyncWorker tenta reenviar a cada 2 minutos
|
||||
→ quando enviado: arquivo local removido automaticamente
|
||||
```
|
||||
|
||||
### Ao deletar instância:
|
||||
```
|
||||
DELETE /api/whatsapp/{id}
|
||||
→ manager.disconnect() → desconecta Baileys
|
||||
→ repo.delete() → remove do banco (cascade: chats, mensagens, contatos)
|
||||
→ responde 200 ← não bloqueia
|
||||
→ [background] storageProvider.deletePrefix("whatsapp/{tenantId}/{id}/")
|
||||
→ ListObjectsV2 paginado (batches de 1000)
|
||||
→ DeleteObjects em cada batch
|
||||
→ Remove fallback local se existir
|
||||
→ Loga total removidos
|
||||
---
|
||||
|
||||
## Status da Implementação Física em Produção (VPS 1) — 2026-05-07
|
||||
|
||||
Todas as correções descritas neste documento foram fisicamente implementadas, compiladas, testadas e implantadas com sucesso na VPS 1 de Produção.
|
||||
|
||||
### Ações Executadas:
|
||||
1. **Ajuste de Interfaces e Código Core:** Modificados `types.ts`, `StorageProvider.ts`, `MessageHandler.ts`, e `whatsapp.routes.ts`.
|
||||
2. **Implementação de Limpeza Automática:** `UnifiedStorageProvider/index.ts` agora conta com `deletePrefix` S3 e deleção local robusta.
|
||||
3. **Correção de Arquitetura de Container (Crítica):** Foi detectado que nenhum plugin era carregado em produção por falta de mapeamento. Montamos o volume de plugins e criamos os links simbólicos necessários (`/plugins`, `/app/backend`, `/app/src`) no Dockerfile para que os imports relativos funcionem na perfeição.
|
||||
4. **Re-deploy Completo:** O container `newwhats-backend` foi totalmente rebuildado e reiniciado com `docker compose up -d --build`.
|
||||
|
||||
**Status Final:** ✅ **CORRIGIDO E IMPLEMENTADO EM PRODUÇÃO**
|
||||
@@ -0,0 +1,240 @@
|
||||
# Plugin Empresa — Arquitetura de Integração
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O sistema NewWhats roda em servidor próprio e nunca acessa diretamente o banco da empresa cliente.
|
||||
A comunicação acontece por dois plugins que se reconhecem via chave compartilhada:
|
||||
|
||||
```
|
||||
[ Servidor NewWhats ] [ Servidor da Empresa ]
|
||||
Plugin: secretaria ←→ Plugin: newwhats-client
|
||||
- Secretária IA - CRUD nos bancos
|
||||
- WhatsApp / Baileys - Agente de consulta
|
||||
- Inbox / Sessions - Frontend embarcado
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin da Empresa (`newwhats-client`)
|
||||
|
||||
### Responsabilidades
|
||||
|
||||
- Conecta ao banco de dados interno da empresa (leitura e escrita)
|
||||
- Expõe endpoints controlados para o NewWhats consumir
|
||||
- Recebe webhooks do NewWhats (eventos, notificações)
|
||||
- Serve o frontend embarcado (inbox, sessions, secretária)
|
||||
- Gerencia a chave de integração e autenticação cruzada
|
||||
|
||||
### Instalação
|
||||
|
||||
O plugin é distribuído como pacote independente.
|
||||
A empresa instala no próprio servidor — Node.js, PHP, Python ou via Docker.
|
||||
Após instalação, gera-se uma `integration_key` que é inserida nos dois lados.
|
||||
|
||||
```
|
||||
1. Empresa instala o plugin
|
||||
2. Plugin gera integration_key + registra URL do servidor NewWhats
|
||||
3. Admin copia a key e cola nas configurações do NewWhats
|
||||
4. Os dois sistemas se reconhecem e começam a conversar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comunicação entre os Plugins
|
||||
|
||||
### Direções
|
||||
|
||||
| Direção | Quando | Exemplo |
|
||||
|---|---|---|
|
||||
| NewWhats → Empresa (pull) | Secretária precisa de dados em tempo real | Consultar agenda do Dr. |
|
||||
| Empresa → NewWhats (push) | Evento proativo no sistema da empresa | Dr. cancelou consulta, avisar paciente |
|
||||
|
||||
### Autenticação
|
||||
|
||||
- Chave JWT gerada no NewWhats, compartilhada com o plugin da empresa
|
||||
- Rotacionável pelo admin a qualquer momento
|
||||
- Cada requisição carrega o JWT no header `Authorization: Bearer <key>`
|
||||
- Expiração configurável (padrão: 90 dias)
|
||||
|
||||
---
|
||||
|
||||
## Contrato de API (Endpoints Mínimos)
|
||||
|
||||
O plugin da empresa **obrigatoriamente** expõe estes endpoints para a secretária funcionar:
|
||||
|
||||
### Agenda
|
||||
```
|
||||
GET /nw/agenda?data=YYYY-MM-DD&profissional_id= → horários disponíveis
|
||||
POST /nw/agenda/agendar → confirmar agendamento
|
||||
PUT /nw/agenda/:id/cancelar → cancelar agendamento
|
||||
GET /nw/agenda/:id → detalhes de um agendamento
|
||||
```
|
||||
|
||||
### Profissionais / Responsáveis
|
||||
```
|
||||
GET /nw/profissionais → lista com nome, área, número WhatsApp
|
||||
GET /nw/profissionais/:id → dados de um profissional
|
||||
```
|
||||
|
||||
### Clientes / Pacientes / Contatos
|
||||
```
|
||||
GET /nw/cliente?telefone=5511999999999 → dados pelo número de WhatsApp
|
||||
GET /nw/cliente/:id → dados por ID
|
||||
POST /nw/cliente → cadastrar novo cliente
|
||||
```
|
||||
|
||||
### Webhook
|
||||
```
|
||||
POST /nw/webhook/registrar → registrar URL de callback no NewWhats
|
||||
```
|
||||
|
||||
### Endpoints opcionais (dependem do ramo)
|
||||
```
|
||||
GET /nw/produtos → catálogo
|
||||
GET /nw/pedidos?cliente_id= → histórico de pedidos
|
||||
GET /nw/financeiro?cliente_id= → situação financeira
|
||||
GET /nw/historico?cliente_id= → histórico de atendimentos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agente de Consulta (dentro do plugin da empresa)
|
||||
|
||||
O plugin não expõe o banco diretamente — ele tem um **agente interno** que:
|
||||
|
||||
1. Recebe a query da secretária (ex: `agenda?data=2026-04-15&profissional=Dr. Carlos`)
|
||||
2. Traduz para uma query SQL ou chamada interna do sistema da empresa
|
||||
3. Filtra apenas os campos permitidos (sem dados sensíveis)
|
||||
4. Devolve JSON padronizado
|
||||
|
||||
```
|
||||
Secretária IA (NewWhats)
|
||||
↓ GET /nw/agenda?data=15/04
|
||||
Plugin da Empresa
|
||||
↓ SELECT * FROM agenda WHERE data = '2026-04-15' AND ativo = true
|
||||
Banco da Empresa
|
||||
↓ rows
|
||||
Plugin filtra e formata
|
||||
↓ JSON limpo
|
||||
Secretária injeta no contexto e responde o cliente
|
||||
```
|
||||
|
||||
### Permissões por endpoint
|
||||
|
||||
Cada endpoint tem uma flag de permissão configurável pelo admin da empresa:
|
||||
|
||||
```
|
||||
agenda.read = true ✓
|
||||
agenda.write = true ✓
|
||||
cliente.read = true ✓
|
||||
cliente.write = false ✗ (somente leitura por enquanto)
|
||||
financeiro.read = false ✗ (dado sensível, bloqueado)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Embarcado
|
||||
|
||||
### O que é
|
||||
|
||||
As três telas principais do NewWhats servidas **dentro do sistema da empresa**,
|
||||
sem precisar abrir outro sistema ou fazer login separado.
|
||||
|
||||
```
|
||||
Sistema da Empresa (ERP / CRM)
|
||||
└── Menu
|
||||
├── [módulos da empresa]
|
||||
└── NewWhats ← plugin embarcado
|
||||
├── Inbox (conversas WhatsApp em tempo real)
|
||||
├── Sessions (instâncias Baileys, status de conexão)
|
||||
└── Secretária IA (configuração + chat de teste)
|
||||
```
|
||||
|
||||
### Opções de implementação
|
||||
|
||||
**Opção 1 — iFrame com token SSO** (mais simples)
|
||||
- Plugin gera um token de sessão temporário (15min, renovável)
|
||||
- Abre `https://newwhats-server/embed?token=<sso_token>` em iFrame
|
||||
- NewWhats valida o token e serve a interface sem tela de login
|
||||
|
||||
**Opção 2 — Micro-frontend (componentes standalone)** (mais integrado)
|
||||
- Plugin distribui componentes React/Vue que consomem a API do NewWhats
|
||||
- Renderizam dentro do layout da empresa sem iFrame
|
||||
- Requerem que a empresa use React/Vue no frontend
|
||||
|
||||
**Recomendação:** começar com iFrame SSO — funciona em qualquer stack,
|
||||
entrega valor rápido. Migrar para micro-frontend quando houver demanda.
|
||||
|
||||
### Autenticação cruzada (SSO)
|
||||
|
||||
```
|
||||
1. Usuário logado no sistema da empresa clica em "NewWhats"
|
||||
2. Sistema da empresa chama POST /nw/auth/sso no NewWhats com a integration_key
|
||||
3. NewWhats retorna um token de sessão temporário
|
||||
4. Plugin abre o iFrame com esse token
|
||||
5. Usuário entra direto, sem segunda tela de login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Números e Papéis
|
||||
|
||||
O plugin da empresa cadastra os números WhatsApp relevantes e seus papéis:
|
||||
|
||||
| Papel | Descrição | Exemplo |
|
||||
|---|---|---|
|
||||
| `secretary_virtual` | Número principal da secretária IA | Atende clientes |
|
||||
| `clinic` | Número geral da empresa | Recebe notificações |
|
||||
| `doctor` / `specialist` | Profissional responsável por área | Recebe dúvidas específicas |
|
||||
| `manager` | Fallback universal | Recebe quando ninguém mais responde |
|
||||
| `reserve` | Backup da secretária | Entra quando principal cai ou é banido |
|
||||
| `human_secretary` | Secretária humana | Pode assumir conversa da IA |
|
||||
|
||||
O NewWhats consulta esses papéis via `GET /nw/profissionais` e usa para roteamento interno.
|
||||
|
||||
---
|
||||
|
||||
## Fluxo Completo — Exemplo Real
|
||||
|
||||
**Cenário:** Paciente pergunta se pode agendar com o Dr. Carlos na quinta-feira.
|
||||
|
||||
```
|
||||
1. Paciente envia mensagem no WhatsApp
|
||||
2. Baileys recebe → NewWhats aciona Secretária IA
|
||||
3. Nó de intenção classifica: "agendamento"
|
||||
4. Nó de calendário chama GET /nw/agenda?data=2026-04-16&profissional=Dr.Carlos
|
||||
5. Plugin da empresa consulta banco → retorna horários livres
|
||||
6. Secretária oferece opções ao paciente
|
||||
7. Paciente confirma horário das 14h
|
||||
8. Secretária chama POST /nw/agenda/agendar com os dados
|
||||
9. Plugin grava no banco da empresa
|
||||
10. Secretária confirma para o paciente via WhatsApp
|
||||
11. Sistema da empresa exibe o agendamento normalmente no próprio sistema
|
||||
```
|
||||
|
||||
**Tudo isso sem o paciente saber que são dois sistemas.**
|
||||
|
||||
---
|
||||
|
||||
## Roadmap de Implementação
|
||||
|
||||
### Fase 1 — Conexão básica
|
||||
- [ ] Geração e troca de `integration_key` entre os dois plugins
|
||||
- [ ] Endpoints mínimos no plugin da empresa (agenda, profissionais, cliente)
|
||||
- [ ] Nó do tipo `api_query` no cérebro da secretária
|
||||
- [ ] Teste end-to-end: secretária consulta agenda real
|
||||
|
||||
### Fase 2 — Agente de consulta
|
||||
- [ ] Agente interno no plugin da empresa com mapeamento de permissões
|
||||
- [ ] Push de eventos (webhook): cancelamentos, atualizações
|
||||
- [ ] Roteamento por papel: secretária sabe para qual número perguntar
|
||||
|
||||
### Fase 3 — Frontend embarcado
|
||||
- [ ] SSO por token temporário
|
||||
- [ ] iFrame com Inbox + Sessions + Secretária IA
|
||||
- [ ] Personalização básica (logo, cores da empresa)
|
||||
|
||||
### Fase 4 — Expansão
|
||||
- [ ] SDK para empresas implementarem o plugin em qualquer stack
|
||||
- [ ] Marketplace de conectores prontos (ex: conector para sistemas populares do setor)
|
||||
- [ ] Painel de auditoria cruzada (o que a secretária consultou e quando)
|
||||
Reference in New Issue
Block a user