fix: criar tabela MENSAGENS_CLIENTES (migracao #25) e corrigir insert com MSC_CODIGO_ID

- add: migration #25 cria tabela MENSAGENS_CLIENTES + generator
- fix: insert agora usa db.nextId para gerar MSC_CODIGO_ID
- fix: adiciona GEN_MENSAGENS_CLIENTES ao whitelist no database.js
This commit is contained in:
2026-07-01 16:30:10 +00:00
parent 3113f9afbd
commit 0b2748a579
7 changed files with 161 additions and 7 deletions
+25
View File
@@ -137,6 +137,31 @@ const MIGRACOES = [
'ALTER TABLE CHATC2_CONVERSAS ADD CON_RESOLUCAO VARCHAR(4000)',
],
},
// ----------------------------------------------------------
// MIGRAÇÃO 25: Tabela MENSAGENS_CLIENTES
// ----------------------------------------------------------
{
id: 25,
descricao: 'Criar tabela MENSAGENS_CLIENTES para salvar resolução do atendimento',
sql: [
`CREATE TABLE MENSAGENS_CLIENTES (
MSC_CODIGO_ID INTEGER NOT NULL PRIMARY KEY,
MSC_CLIENTE_ID INTEGER,
MSC_USUARIO_ID INTEGER,
MSC_DT_MENSAGEM TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
MSC_AVISO CHAR(1) DEFAULT 'N',
MSC_MENSAGEM VARCHAR(4000)
)`,
'CREATE GENERATOR GEN_MENSAGENS_CLIENTES',
],
async after(alias) {
try {
const r = await db.query(alias, 'SELECT COALESCE(MAX(MSC_CODIGO_ID),0)+1 AS V FROM MENSAGENS_CLIENTES');
await db.execute(alias, 'SET GENERATOR GEN_MENSAGENS_CLIENTES TO ' + (r[0]?.V || 1));
} catch (_) {}
},
},
];
// ============================================================
+12
View File
@@ -50,6 +50,18 @@ const databases = {
pageSize: 4096,
wireCrypt: 1,
},
novo: {
driver: 'firebird',
host: '177.70.23.53',
port: 3050,
database: 'sigef_web_novo',
user: process.env.DB_USER || 'SYSDBA',
password: process.env.DB_PASSWORD || 'masterkey',
encoding: process.env.DB_ENCODING || 'UTF-8',
lowercase_keys: false,
pageSize: 4096,
wireCrypt: 1,
},
};
function salvarCustomDatabases() {
+4 -3
View File
@@ -736,10 +736,11 @@ class ChatController {
// Salva resolução na tabela MENSAGENS_CLIENTES como aviso (se configurado)
if (isS(fl.CFE_RESOLUCAO_SALVAR_MSC) && resolucao && c.CON_CLIENTE_ID) {
try {
const mscId = await db.nextId(alias, 'GEN_MENSAGENS_CLIENTES');
await db.execute(alias,
`INSERT INTO MENSAGENS_CLIENTES (MSC_CLIENTE_ID, MSC_USUARIO_ID, MSC_DT_MENSAGEM, MSC_AVISO, MSC_MENSAGEM)
VALUES (?, ?, CURRENT_TIMESTAMP, 'S', ?)`,
[c.CON_CLIENTE_ID, c.CON_USUARIO_ID || null, resolucao]);
`INSERT INTO MENSAGENS_CLIENTES (MSC_CODIGO_ID, MSC_CLIENTE_ID, MSC_USUARIO_ID, MSC_DT_MENSAGEM, MSC_AVISO, MSC_MENSAGEM)
VALUES (?, ?, ?, CURRENT_TIMESTAMP, 'S', ?)`,
[mscId, c.CON_CLIENTE_ID, c.CON_USUARIO_ID || null, resolucao]);
console.log('[Resolucao] Aviso salvo em MENSAGENS_CLIENTES para cliente', c.CON_CLIENTE_ID);
} catch (mscErr) {
console.error('[Resolucao] Erro ao salvar em MENSAGENS_CLIENTES:', mscErr.message);
+69 -3
View File
@@ -3,6 +3,31 @@ const https = require('https');
const http = require('http');
const { listAliases } = db;
const TriageController = require('./triageController');
const mediaService = require('../services/mediaService');
// ponytail: anti-loop — limita respostas automáticas por conversa
const triagemRateLimit = new Map();
const TRIAGEM_MAX = 5; // max respostas auto em 60s
const TRIAGEM_WINDOW = 60000; // janela de 60 segundos
function triagemExcedeu(conversaId) {
const agora = Date.now();
const entry = triagemRateLimit.get(conversaId);
if (!entry || agora - entry.inicio > TRIAGEM_WINDOW) {
triagemRateLimit.set(conversaId, { count: 1, inicio: agora });
return false;
}
entry.count++;
return entry.count > TRIAGEM_MAX;
}
// Limpeza periódica
setInterval(() => {
const agora = Date.now();
for (const [k, v] of triagemRateLimit) {
if (agora - v.inicio > TRIAGEM_WINDOW * 2) triagemRateLimit.delete(k);
}
}, 60000).unref();
// Cache de instancia -> alias para evitar consultas repetidas
var cacheInstanciaAlias = {};
@@ -262,6 +287,27 @@ class EvolutionController {
}
}
/**
* Listar instâncias de uma empresa
/**
* Busca instâncias já conectadas no servidor Evolution.
* POST /api/:alias/evolution/fetch-instances
*/
static async fetchInstances(req, res) {
try {
const { url, apiKey } = req.body;
if (!url || !apiKey) return res.status(400).json({ success: false, error: 'URL e API Key obrigatórios.' });
const result = await evolutionRequest(url, apiKey, '/instance/fetchInstances', 'GET');
if (Array.isArray(result)) {
res.json({ success: true, data: result.map(r => ({ instanceName: r.instanceName || r.name, connectionStatus: r.connectionStatus || r.status })) });
} else if (result && result.instance) {
res.json({ success: true, data: [{ instanceName: result.instance.instanceName || result.instance.name, connectionStatus: result.instance.connectionStatus || result.instance.status }] });
} else {
res.json({ success: true, data: [] });
}
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
}
/**
* Listar instâncias de uma empresa
* GET /api/:alias/evolution/instances?empresaId=
@@ -531,17 +577,30 @@ class EvolutionController {
var u = new URL(fotoUrl);
var lib = u.protocol === 'https:' ? https : http;
lib.get(fotoUrl, { timeout: 15000 }, function(imgRes) {
// Valida Content-Type
var ct = (imgRes.headers['content-type'] || '').toLowerCase();
if (ct && !ct.startsWith('image/')) {
console.log('[Foto] Content-Type invalido:', ct, '- recusando');
return reject(new Error('Not an image: ' + ct));
}
var chunks = [];
imgRes.on('data', function(c) { chunks.push(c); });
imgRes.on('end', function() { resolve(Buffer.concat(chunks)); });
}).on('error', reject).on('timeout', function() { this.destroy(); reject(new Error('Timeout')); });
});
if (fotoBuffer && fotoBuffer.length > 100) {
// Valida buffer: tamanho realista (500B a 2MB) e header de imagem
if (fotoBuffer && fotoBuffer.length > 500 && fotoBuffer.length < 2 * 1024 * 1024) {
if (!mediaService.validarHeaderMidia(fotoBuffer, 'image/jpeg')) {
console.log('[Foto] Buffer nao tem header de imagem valido - recusando. Header:', fotoBuffer.slice(0, 4).toString('hex'));
return false;
}
await db.execute(alias,
'UPDATE CLIENTES SET CLI_FOTO = ? WHERE CLI_CODIGO_ID = ?',
[fotoBuffer.toString('base64'), clienteId]);
console.log('[Foto] Foto atualizada para cliente', clienteId, '- bytes:', fotoBuffer.length);
return true;
} else if (fotoBuffer) {
console.log('[Foto] Buffer com tamanho invalido:', fotoBuffer ? fotoBuffer.length : 0);
}
} catch(imgErr) {
console.error('[Foto] Erro ao baixar imagem:', (imgErr.message || '').substring(0, 120));
@@ -1280,8 +1339,15 @@ async function processWebhook(alias, body) {
var emTriagem = conversa.CON_MENU_ESTADO &&
(!conversa.CON_USUARIO_ID || !conversa.CON_EQUIPE_ID);
if (!isNovaConversa && messageText && messageType === 'conversation' && !data.key?.fromMe && emTriagem) {
TriageController.processResponse(alias, conversa.CON_CODIGO_ID, empresaId, instanciaId, numero, messageText)
.catch(function(e) { console.error('[Triagem] Erro ao processar resposta:', e.message); });
if (triagemExcedeu(conversa.CON_CODIGO_ID)) {
console.log('[Webhook] Loop detectado na conversa', conversa.CON_CODIGO_ID, '- triagem pausada. Aguardando atendente.');
await db.execute(alias,
"UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = NULL WHERE CON_CODIGO_ID = ?",
[conversa.CON_CODIGO_ID]);
} else {
TriageController.processResponse(alias, conversa.CON_CODIGO_ID, empresaId, instanciaId, numero, messageText)
.catch(function(e) { console.error('[Triagem] Erro ao processar resposta:', e.message); });
}
}
}
} catch (err) {
+1
View File
@@ -98,6 +98,7 @@ const GENERATORS = [
'GEN_CHATC2_CSAT_AVALIACOES',
'GEN_CHATC2_MOTIVOS_ATENDIMENTO',
'GEN_CHATC2_AGENDAMENTOS',
'GEN_MENSAGENS_CLIENTES',
];
async function nextId(alias, generatorName) {
+49 -1
View File
@@ -280,6 +280,14 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
<div class="form-group"><label for="conApiKey">API Key</label><input type="text" id="conApiKey" placeholder="API Key"></div>
<div class="form-group"><label for="conInstance">Nome da Instância (Evolution)</label><input type="text" id="conInstance" placeholder="Ex: empresa1-wpp"></div>
<div id="conexaoFeedback" class="feedback-msg"></div>
<div class="form-group" id="instanciasExistentesGroup" style="display:none">
<label>Instâncias já conectadas no servidor</label>
<div id="instanciasExistentesList" style="max-height:150px;overflow-y:auto;border:2px solid var(--border);border-radius:8px;padding:8px">
<button class="btn-sm" onclick="buscarInstanciasExistentes()" style="width:100%;padding:8px;font-size:12px">🔍 Buscar instâncias no servidor</button>
</div>
<span style="font-size:11px;color:var(--text-faint)">Clique em uma instância existente para reutilizá-la.</span>
</div>
<div id="conexaoFeedback" class="feedback-msg"></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="fecharModal('modalConexao')">Cancelar</button>
<button class="btn-primary" onclick="salvarConexao()" id="btnSalvarConexao">Conectar</button>
@@ -954,6 +962,45 @@ window.mostrarModalConexao = function() {
document.getElementById('modalConexaoTitle').textContent = 'Nova Conexão WhatsApp';
document.getElementById('btnSalvarConexao').textContent = 'Conectar';
document.getElementById('modalConexao').classList.add('show');
document.getElementById('instanciasExistentesGroup').style.display = 'block';
document.getElementById('instanciasExistentesList').innerHTML = '<button class="btn-sm" onclick="buscarInstanciasExistentes()" style="width:100%;padding:8px;font-size:12px">🔍 Buscar instâncias no servidor</button>';
};
window.buscarInstanciasExistentes = async function() {
var list = document.getElementById('instanciasExistentesList');
var url = document.getElementById('conUrl').value.trim();
var apiKey = document.getElementById('conApiKey').value.trim();
if (!url || !apiKey) { list.innerHTML = '<span style="font-size:12px;color:var(--text-faint)">Preencha URL e API Key primeiro</span>'; return; }
list.innerHTML = '<span style="font-size:12px;color:var(--text-faint)">Buscando...</span>';
try {
var r = await fetch('/api/' + alias + '/evolution/fetch-instances', {
method: 'POST', body: JSON.stringify({ url: url, apiKey: apiKey }),
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }
});
var d = await r.json();
if (d.success && d.data && d.data.length > 0) {
list.innerHTML = d.data.map(function(i) {
var nm = (i.instanceName || i.name || '');
var st = i.connectionStatus === 'open' ? '🟢' : '🔴';
return '<div data-instance="' + nm.replace(/"/g, '&quot;') + '" class="instancia-existente" style="padding:6px 8px;border-radius:4px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--border);color:var(--text-primary)">' + st + ' <strong>' + nm + '</strong><span style="margin-left:auto;font-size:11px;color:var(--text-muted)">' + (i.connectionStatus || i.status || '-') + '</span></div>';
}).join('');
// Delegation: click em qualquer .instancia-existente
list.querySelectorAll('.instancia-existente').forEach(function(el) {
el.addEventListener('click', function() { reutilizarInstancia(this.dataset.instance); });
});
} else { list.innerHTML = '<span style="font-size:12px;color:var(--text-faint)">Nenhuma instância no servidor</span>'; }
} catch(e) { list.innerHTML = '<span style="font-size:12px;color:var(--danger)">Erro ao buscar</span>'; }
};
window.reutilizarInstancia = function(nm) {
document.getElementById('conInstance').value = nm;
var existente = document.getElementById('conInstanceExistente');
if (!existente) {
existente = document.createElement('span'); existente.id = 'conInstanceExistente';
existente.style.cssText = 'font-size:11px;color:#059669;display:block;margin-top:3px';
document.getElementById('conInstance').parentNode.appendChild(existente);
}
existente.textContent = '✓ Reutilizando: ' + nm;
};
window.gerarQRCode = async function(id) {
@@ -1007,8 +1054,9 @@ window.salvarConexao = async function() {
INS_INSTANCE_NAME: document.getElementById('conInstance').value.trim(),
};
if (!data.INS_NOME || !data.INS_URL || !data.INS_API_KEY || !data.INS_INSTANCE_NAME)
if (!data.INS_NOME || !data.INS_URL || !data.INS_API_KEY || !data.INS_INSTANCE_NAME) {
mostrarFeedbackModal('conexaoFeedback', 'Preencha todos os campos', 'erro'); return;
}
var r;
if (editId) {
+1
View File
@@ -39,6 +39,7 @@ router.post('/:alias/evolution/connect', EvolutionController.connect);
router.put('/:alias/evolution/instances/:id', EvolutionController.updateInstance);
router.delete('/:alias/evolution/instances/:id', EvolutionController.deleteInstance);
router.post('/:alias/evolution/qrcode/:id', EvolutionController.generateQRCode);
router.post('/:alias/evolution/fetch-instances', EvolutionController.fetchInstances);
router.post('/:alias/evolution/refresh-photo/:conversaId', EvolutionController.refreshPhoto);
module.exports = router;