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:
@@ -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 (_) {}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, '"') + '" 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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user