Actualizar server.js
This commit is contained in:
79
server.js
79
server.js
@@ -9,6 +9,9 @@ import OpenAI from "openai";
|
|||||||
const { Pool } = pg;
|
const { Pool } = pg;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
// 🛑 SEMÁFORO IA: Guarda los IDs de los servicios que están siendo procesados
|
||||||
|
const candadosIA = new Set();
|
||||||
|
|
||||||
// Configuración de CORS Profesional
|
// Configuración de CORS Profesional
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: [
|
origin: [
|
||||||
@@ -3004,22 +3007,23 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
const mensajeTexto = data.data.message?.conversation || data.data.message?.extendedTextMessage?.text;
|
const mensajeTexto = data.data.message?.conversation || data.data.message?.extendedTextMessage?.text;
|
||||||
const instanceName = data.instance;
|
const instanceName = data.instance;
|
||||||
|
|
||||||
if (!mensajeTexto) return res.sendStatus(200);
|
if (!mensajeTexto || !instanceName || !instanceName.startsWith("cliente_")) {
|
||||||
|
|
||||||
// Filtro de seguridad: Evitar procesar instancias del sistema como "ADMIN"
|
|
||||||
if (!instanceName || !instanceName.startsWith("cliente_")) {
|
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚀 CRÍTICO: Responder a Evolution rápido para que no reintente
|
||||||
|
res.sendStatus(200);
|
||||||
|
|
||||||
const ownerId = instanceName.split("_")[1];
|
const ownerId = instanceName.split("_")[1];
|
||||||
const cleanPhone = telefonoCliente.slice(-9);
|
const cleanPhone = telefonoCliente.slice(-9);
|
||||||
|
|
||||||
// 1. BUSCAMOS EL SINIESTRO (Extrayendo urgencia y población)
|
|
||||||
// 1. Añadimos s.assigned_to a la consulta
|
|
||||||
const svcQ = await pool.query(`
|
const svcQ = await pool.query(`
|
||||||
SELECT s.id, s.service_ref, s.assigned_to, u.full_name as worker_name, s.is_urgent,
|
SELECT s.id, s.service_ref, s.assigned_to, u.full_name as worker_name, s.is_urgent,
|
||||||
st.name as status_name, s.raw_data->>'scheduled_date' as cita,
|
st.name as status_name, s.raw_data->>'scheduled_date' as cita,
|
||||||
s.raw_data->>'Población' as poblacion
|
s.raw_data->>'Población' as poblacion,
|
||||||
|
s.raw_data->>'appointment_status' as appointment_status,
|
||||||
|
s.raw_data->>'requested_date' as cita_pendiente_fecha,
|
||||||
|
s.raw_data->>'requested_time' as cita_pendiente_hora
|
||||||
FROM scraped_services s
|
FROM scraped_services s
|
||||||
LEFT JOIN users u ON s.assigned_to = u.id
|
LEFT JOIN users u ON s.assigned_to = u.id
|
||||||
LEFT JOIN service_statuses st ON (s.raw_data->>'status_operativo')::text = st.id::text
|
LEFT JOIN service_statuses st ON (s.raw_data->>'status_operativo')::text = st.id::text
|
||||||
@@ -3030,86 +3034,71 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
if (svcQ.rowCount > 0) {
|
if (svcQ.rowCount > 0) {
|
||||||
const service = svcQ.rows[0];
|
const service = svcQ.rows[0];
|
||||||
|
|
||||||
// 🛡️ 2. VERIFICAR INTERVENCIÓN HUMANA
|
// 🛑 SEMÁFORO ANTI-METRALLETA: Si ya estamos procesando este aviso, abortamos.
|
||||||
// Miramos si el último mensaje fue de un humano en las últimas 2 horas
|
if (candadosIA.has(service.id)) {
|
||||||
|
console.log(`⏳ [Bloqueo] Ignorando mensaje rápido concurrente para exp #${service.service_ref}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Cerramos el candado
|
||||||
|
candadosIA.add(service.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 🛡️ VERIFICAR INTERVENCIÓN HUMANA
|
||||||
const checkHumanQ = await pool.query(`
|
const checkHumanQ = await pool.query(`
|
||||||
SELECT sender_role, created_at
|
SELECT sender_role, created_at FROM service_communications
|
||||||
FROM service_communications
|
WHERE scraped_id = $1 ORDER BY created_at DESC LIMIT 1
|
||||||
WHERE scraped_id = $1
|
|
||||||
ORDER BY created_at DESC LIMIT 1
|
|
||||||
`, [service.id]);
|
`, [service.id]);
|
||||||
|
|
||||||
if (checkHumanQ.rowCount > 0) {
|
if (checkHumanQ.rowCount > 0) {
|
||||||
const lastMsg = checkHumanQ.rows[0];
|
const lastMsg = checkHumanQ.rows[0];
|
||||||
const diffMinutos = (new Date() - new Date(lastMsg.created_at)) / (1000 * 60);
|
const diffMinutos = (new Date() - new Date(lastMsg.created_at)) / (1000 * 60);
|
||||||
|
|
||||||
// Si el último que habló fue admin/operario y hace menos de 120 min, la IA no responde
|
|
||||||
if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 120) {
|
if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 120) {
|
||||||
console.log(`🤫 [IA Silenciada] Un humano ha intervenido hace ${Math.round(diffMinutos)} min en el exp #${service.service_ref}`);
|
return; // IA Silenciada
|
||||||
return res.sendStatus(200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. SI NO HAY HUMANO RECIENTE, LLAMAMOS A LA IA CON TODO EL CONTEXTO
|
// 🧠 LLAMADA A LA IA
|
||||||
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
||||||
dbId: service.id, // 👈 CRÍTICO para que la IA lea el historial
|
dbId: service.id,
|
||||||
ref: service.service_ref,
|
ref: service.service_ref,
|
||||||
estado: service.status_name || "En proceso",
|
estado: service.status_name || "En proceso",
|
||||||
operario: service.worker_name,
|
operario: service.worker_name,
|
||||||
cita: service.cita,
|
|
||||||
worker_id: service.assigned_to,
|
worker_id: service.assigned_to,
|
||||||
poblacion: service.poblacion || "", // 👈 CRÍTICO para buscar rutas en la misma zona
|
cita: service.cita,
|
||||||
is_urgent: service.is_urgent, // 👈 CRÍTICO para que no pida cita en urgencias
|
poblacion: service.poblacion || "",
|
||||||
|
is_urgent: service.is_urgent,
|
||||||
appointment_status: service.appointment_status,
|
appointment_status: service.appointment_status,
|
||||||
cita_pendiente_fecha: service.cita_pendiente_fecha,
|
cita_pendiente_fecha: service.cita_pendiente_fecha,
|
||||||
cita_pendiente_hora: service.cita_pendiente_hora
|
cita_pendiente_hora: service.cita_pendiente_hora
|
||||||
});
|
});
|
||||||
|
|
||||||
if (respuestaIA) {
|
if (respuestaIA) {
|
||||||
// --- MAGIA: DETECTAR SI LA IA RECOGIÓ UNA PROPUESTA DE CITA ---
|
|
||||||
const matchPropuesta = respuestaIA.match(/\[PROPUESTA:(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]/);
|
const matchPropuesta = respuestaIA.match(/\[PROPUESTA:(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]/);
|
||||||
|
|
||||||
if (matchPropuesta) {
|
if (matchPropuesta) {
|
||||||
const fechaSugerida = matchPropuesta[1];
|
const fechaSugerida = matchPropuesta[1];
|
||||||
const horaSugerida = matchPropuesta[2];
|
const horaSugerida = matchPropuesta[2];
|
||||||
|
|
||||||
console.log(`📅 PROPUESTA RECIBIDA (IA): ${fechaSugerida} a las ${horaSugerida} para exp #${service.service_ref}`);
|
|
||||||
|
|
||||||
// 🚀 Guardar como "requested_date" para que aparezca en el Panel en "Citas Solicitadas"
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
UPDATE scraped_services
|
UPDATE scraped_services
|
||||||
SET raw_data = raw_data || jsonb_build_object(
|
SET raw_data = raw_data || jsonb_build_object(
|
||||||
'requested_date', $1::text,
|
'requested_date', $1::text,
|
||||||
'requested_time', $2::text,
|
'requested_time', $2::text,
|
||||||
'appointment_status', 'pending'
|
'appointment_status', 'pending'
|
||||||
)
|
) WHERE id = $3
|
||||||
WHERE id = $3
|
|
||||||
`, [fechaSugerida, horaSugerida, service.id]);
|
`, [fechaSugerida, horaSugerida, service.id]);
|
||||||
|
|
||||||
// Registramos el movimiento en el historial
|
|
||||||
await registrarMovimiento(
|
|
||||||
service.id,
|
|
||||||
null,
|
|
||||||
"Cita Solicitada (IA)",
|
|
||||||
`El cliente solicita cita vía Asistente IA para el ${fechaSugerida} a las ${horaSugerida}.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- LIMPIEZA Y ENVÍO ---
|
|
||||||
// Quitamos el código [PROPUESTA:...] del texto para que el cliente no lo vea
|
|
||||||
const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/, "").trim();
|
const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/, "").trim();
|
||||||
|
|
||||||
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
|
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
|
||||||
|
|
||||||
// --- REGISTRO DEL CHAT ---
|
|
||||||
await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`,
|
await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`,
|
||||||
[service.id, ownerId, "Asistente IA", "ia", textoLimpio]);
|
[service.id, ownerId, "Asistente IA", "ia", textoLimpio]);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// 🟢 ABRIMOS EL CANDADO SIEMPRE AL TERMINAR (Aunque haya error)
|
||||||
|
candadosIA.delete(service.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.sendStatus(200);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ [WEBHOOK ERROR]:", e.message);
|
console.error("❌ [WEBHOOK ERROR]:", e.message);
|
||||||
if (!res.headersSent) res.sendStatus(500);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user