163 lines
5.7 KiB
JavaScript
163 lines
5.7 KiB
JavaScript
'use strict'
|
|
|
|
/**
|
|
* Monitor de mensagens sem resposta no WhatsApp
|
|
*
|
|
* Detecta chats com mensagens recebidas há mais de LIMIAR_MINUTOS
|
|
* que não receberam resposta da IA (status='sent') nem estão em handoff humano.
|
|
*
|
|
* A cada ciclo:
|
|
* - Busca chats pendentes
|
|
* - Cria mensagem interna no setor 'atendimento' (uma por chat, sem duplicar em 30min)
|
|
* - Emite evento SSE 'wpp.sem_resposta' para o admin
|
|
*/
|
|
|
|
const { query, queryOne, execute } = require('../database/postgres')
|
|
|
|
const LIMIAR_MINUTOS = 5 // tempo sem resposta para considerar pendente
|
|
const INTERVALO_MS = 5 * 60 * 1000 // roda a cada 5 minutos
|
|
const DEDUP_MINUTOS = 30 // não gera alerta duplicado no mesmo chat em 30min
|
|
|
|
let timer = null
|
|
|
|
async function checarMsgsSemResposta() {
|
|
try {
|
|
// Chats com mensagem recebida há mais de LIMIAR_MINUTOS sem reply enviado
|
|
// e que não estão em handoff humano ativo
|
|
const pendentes = await query(`
|
|
SELECT DISTINCT ON (el.chat_id)
|
|
el.chat_id,
|
|
el.created_at AS ultima_msg_em,
|
|
el.payload->>'body' AS ultima_msg,
|
|
el.payload->>'pushName' AS push_name,
|
|
el.payload->>'author' AS author,
|
|
EXTRACT(EPOCH FROM (NOW() - el.created_at)) / 60 AS minutos_aguardando
|
|
FROM nw_event_logs el
|
|
WHERE el.direction = 'in'
|
|
AND el.event = 'message.new'
|
|
AND el.created_at > NOW() - INTERVAL '2 hours'
|
|
AND el.created_at < NOW() - ($1 || ' minutes')::INTERVAL
|
|
-- sem resposta da IA depois desta mensagem
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM nw_auto_replies ar
|
|
WHERE ar.chat_id = el.chat_id
|
|
AND ar.status = 'sent'
|
|
AND ar.created_at >= el.created_at
|
|
)
|
|
-- não está em handoff humano ativo
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM nw_handoff h
|
|
WHERE h.chat_id = el.chat_id
|
|
AND h.human_until > NOW()
|
|
)
|
|
-- sem Human API pendente para este chat (operador já está sendo acionado)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM human_api_requests har
|
|
WHERE har.chat_id = el.chat_id
|
|
AND har.status = 'pending'
|
|
)
|
|
ORDER BY el.chat_id, el.created_at DESC
|
|
`, [LIMIAR_MINUTOS])
|
|
|
|
if (!pendentes.length) return
|
|
|
|
for (const p of pendentes) {
|
|
// Dedup por chat_id — não repete alerta para o mesmo chat em DEDUP_MINUTOS
|
|
const jaAlertado = await queryOne(`
|
|
SELECT id FROM mensagens_internas
|
|
WHERE setor = 'atendimento'
|
|
AND remetente = 'Monitor IA'
|
|
AND chat_id = $1
|
|
AND created_at > NOW() - ($2 || ' minutes')::INTERVAL
|
|
LIMIT 1
|
|
`, [p.chat_id, DEDUP_MINUTOS])
|
|
|
|
if (jaAlertado) continue
|
|
|
|
const mins = Math.round(Number(p.minutos_aguardando))
|
|
const preview = (p.ultima_msg || '').slice(0, 80)
|
|
|
|
// Monta identificador legível: nome > número formatado > UUID curto
|
|
let clienteId = p.push_name?.trim() || ''
|
|
if (!clienteId && p.author) {
|
|
const num = p.author.replace('@s.whatsapp.net', '').replace('@c.us', '')
|
|
if (/^\d+$/.test(num)) {
|
|
clienteId = `+${num.replace(/^55(\d{2})(\d{4,5})(\d{4})$/, '55 ($1) $2-$3')}`
|
|
}
|
|
}
|
|
if (!clienteId) clienteId = p.chat_id.slice(0, 8) + '…'
|
|
|
|
await execute(`
|
|
INSERT INTO mensagens_internas (tenant_id, setor, remetente, chat_id, mensagem)
|
|
VALUES (1, 'atendimento', 'Monitor IA', $1, $2)
|
|
`, [
|
|
p.chat_id,
|
|
`⚠️ Sem resposta há ${mins} min — ${clienteId}\n"${preview}"`,
|
|
])
|
|
|
|
console.log(`[monitor-wpp] ⚠️ ${clienteId} (${p.chat_id}) sem resposta há ${mins}min`)
|
|
}
|
|
} catch (err) {
|
|
console.error('[monitor-wpp] Erro:', err.message)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retorna os chats atualmente pendentes (usado pelo endpoint de admin).
|
|
*/
|
|
async function getPendentes(limitMinutos = LIMIAR_MINUTOS) {
|
|
return query(`
|
|
SELECT DISTINCT ON (el.chat_id)
|
|
el.chat_id,
|
|
el.created_at AS ultima_msg_em,
|
|
el.payload->>'body' AS ultima_msg,
|
|
el.payload->>'pushName' AS push_name,
|
|
el.payload->>'author' AS author,
|
|
ROUND(EXTRACT(EPOCH FROM (NOW() - el.created_at)) / 60) AS minutos_aguardando,
|
|
(SELECT ar2.reply
|
|
FROM nw_auto_replies ar2
|
|
WHERE ar2.chat_id = el.chat_id
|
|
ORDER BY ar2.created_at DESC LIMIT 1
|
|
) AS ultima_reply_ia,
|
|
(SELECT ar2.status
|
|
FROM nw_auto_replies ar2
|
|
WHERE ar2.chat_id = el.chat_id
|
|
ORDER BY ar2.created_at DESC LIMIT 1
|
|
) AS ultima_reply_status
|
|
FROM nw_event_logs el
|
|
WHERE el.direction = 'in'
|
|
AND el.event = 'message.new'
|
|
AND el.created_at > NOW() - INTERVAL '2 hours'
|
|
AND el.created_at < NOW() - ($1 || ' minutes')::INTERVAL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM nw_auto_replies ar
|
|
WHERE ar.chat_id = el.chat_id
|
|
AND ar.status = 'sent'
|
|
AND ar.created_at >= el.created_at
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM nw_handoff h
|
|
WHERE h.chat_id = el.chat_id
|
|
AND h.human_until > NOW()
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM human_api_requests har
|
|
WHERE har.chat_id = el.chat_id
|
|
AND har.status = 'pending'
|
|
)
|
|
ORDER BY el.chat_id, el.created_at DESC
|
|
`, [limitMinutos])
|
|
}
|
|
|
|
function startMonitorJob() {
|
|
console.log(`[monitor-wpp] Job iniciado — verifica a cada ${INTERVALO_MS / 60000}min`)
|
|
checarMsgsSemResposta()
|
|
timer = setInterval(checarMsgsSemResposta, INTERVALO_MS)
|
|
}
|
|
|
|
function stopMonitorJob() {
|
|
if (timer) { clearInterval(timer); timer = null }
|
|
}
|
|
|
|
module.exports = { startMonitorJob, stopMonitorJob, getPendentes }
|