Actualizar server.js

This commit is contained in:
2026-03-07 22:34:27 +00:00
parent 0e3b3cb046
commit 011d51d258

166
server.js
View File

@@ -434,85 +434,70 @@ async function registrarMovimiento(serviceId, userId, action, details) {
async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
try {
// 1. Extraemos TODO: nombre, settings y horarios reales de la ruta
const userQ = await pool.query("SELECT wa_settings, full_name, portal_settings FROM users WHERE id=$1", [ownerId]);
const userData = userQ.rows[0];
const settings = userData?.wa_settings || {};
const empresaNombre = userData?.full_name || "nuestra empresa";
// Si no hay horario configurado, usa un estándar lógico
const horarios = userData?.portal_settings || { m_start: "09:00", m_end: "14:00", a_start: "16:00", a_end: "19:00" };
if (!settings.wa_ai_enabled) return null;
const ahora = new Date();
const opciones = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const fechaHoyTexto = ahora.toLocaleDateString('es-ES', opciones);
const fechaHoyTexto = ahora.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
const esViernesOSabado = ahora.getDay() === 5 || ahora.getDay() === 6;
// 🧠 MEMORIA: Evita parecer tonto
const chatCheck = await pool.query(`
SELECT id FROM service_communications
// 🧠 MEMORIA TOTAL: Recuperamos los últimos 10 mensajes del expediente
const historyQ = await pool.query(`
SELECT sender_role, message
FROM service_communications
WHERE scraped_id = $1
AND created_at > NOW() - INTERVAL '60 minutes' LIMIT 1
ORDER BY created_at ASC LIMIT 10
`, [datosExpediente.dbId]);
const yaSeHaPresentado = chatCheck.rowCount > 0;
// 📍 LOGÍSTICA: Buscar técnicos cerca
const poblacion = datosExpediente.poblacion || "";
let fechasSugeridas = "";
// Formateamos el historial para que ChatGPT lo entienda
const historialChat = historyQ.rows.map(row => ({
role: (row.sender_role === 'ia' || row.sender_role === 'admin' || row.sender_role === 'operario') ? 'assistant' : 'user',
content: row.message
}));
if (poblacion) {
const rutasCercanas = await pool.query(`
SELECT raw_data->>'scheduled_date' as fecha
FROM scraped_services
WHERE owner_id = $1 AND raw_data->>'Población' ILIKE $2
AND raw_data->>'scheduled_date' > CURRENT_DATE::text AND id != $3
GROUP BY fecha ORDER BY fecha ASC LIMIT 2
`, [ownerId, poblacion, datosExpediente.dbId]);
if (rutasCercanas.rowCount > 0) {
fechasSugeridas = rutasCercanas.rows.map(r => {
const [y, m, d] = r.fecha.split('-');
return `${d}/${m}`;
}).join(" y el ");
}
}
// Evaluamos cuántos mensajes hay para saber si es el primer saludo
const esPrimerMensaje = historialChat.length === 0;
const promptSistema = `
Eres el Asistente Humano de "${empresaNombre}". Tienes que ser MUY natural, cercano y resolutivo. Nada de sonar robótico.
Eres el Asistente de "${empresaNombre}". Habla de tú, de forma natural, sin parecer un robot.
CONTEXTO Y HORARIOS:
- Hoy es: ${fechaHoyTexto}. Año 2026.
- HORARIO DE TRABAJO: Lunes a Viernes. Mañanas de ${horarios.m_start} a ${horarios.m_end}. Tardes de ${horarios.a_start} a ${horarios.a_end}.
- ⛔ LOS FINES DE SEMANA NO SE TRABAJA. Si el cliente pide un Sábado o Domingo, dile amablemente que descansamos el fin de semana y ofrécele un Viernes o Lunes dentro del horario.
DATOS DEL EXPEDIENTE #${datosExpediente.ref}:
- Estado actual: ${datosExpediente.estado}
- Operario asignado: ${datosExpediente.operario || 'Pendiente de asignar'}
- Cita registrada: ${datosExpediente.cita || 'Ninguna'}
- Urgencia: ${datosExpediente.is_urgent ? 'SÍ (CRÍTICO)' : 'No'}
- Población: ${datosExpediente.poblacion}
DATOS EXPEDIENTE #${datosExpediente.ref}:
- Estado: ${datosExpediente.estado}
- Población: ${poblacion}
REGLAS SEGÚN LA SITUACIÓN (CUMPLE A RAJATABLA):
1. SI ES URGENCIA: NO pidas cita. Diles que al ser un aviso de urgencia, el técnico (${datosExpediente.operario || 'de guardia'}) se pondrá en contacto y acudirá lo antes posible.
2. SI YA ESTÁ CITADO: No intentes dar cita nueva a menos que pidan cambiarla. Recuérdales que su cita es el ${datosExpediente.cita || 'día acordado'} y que irá ${datosExpediente.operario || 'el técnico'}.
3. SI YA ESTÁ FINALIZADO/ANULADO: Informa de que el expediente ya está cerrado.
LÓGICA DE SALUDO (MUY IMPORTANTE):
${!yaSeHaPresentado
? `- Es el inicio. Preséntate con empatía: "¡Hola! Soy el asistente de ${empresaNombre}. Te escribo sobre tu aviso #${datosExpediente.ref}..."`
: `- ⛔ YA HABÉIS HABLADO RECIENTEMENTE. PROHIBIDO decir "Hola" o volver a presentarte. Ve DIRECTO a la respuesta como harías en un chat real con un amigo.`
}
REGLA PARA AGENDAR (SOLO SI NO ES URGENCIA Y NO ESTÁ CITADO):
- Horario: L-V de ${horarios.m_start} a ${horarios.m_end} y ${horarios.a_start} a ${horarios.a_end}. Fines de semana NO trabajamos. Hoy es ${fechaHoyTexto}.
- Si el cliente confirma día/hora, anótalo y pon al final de tu frase: [PROPUESTA:YYYY-MM-DD HH:mm]
ESTRATEGIA DE RUTA:
${fechasSugeridas
? `- Casualmente tenemos técnicos yendo a ${poblacion} el ${fechasSugeridas}. Sugiere esas fechas amigablemente para aprovechar el viaje.`
: `- Pregúntale qué día de Lunes a Viernes le viene bien y si prefiere mañana o tarde.`
}
REGLA PARA GUARDAR LA CITA:
- Si el cliente confirma un día y hora válidos (L-V), dile que lo dejas anotado para que el técnico lo confirme y añade AL FINAL tu respuesta este código oculto: [PROPUESTA:YYYY-MM-DD HH:mm]
- Ejemplo: "Perfecto, te dejo anotado para el viernes a las 10:00. El técnico te confirmará en breve. [PROPUESTA:2026-03-13 10:00]"
REGLAS DE CONVERSACIÓN:
${esPrimerMensaje ? `- Es tu primer mensaje. Preséntate: "¡Hola! Soy el asistente de ${empresaNombre}..."` : `- Ya estás hablando con él. NO te presentes ni digas hola. Continúa la charla.`}
- Responde en máximo 2 frases. Corto y directo.
`;
// Construimos el array final de mensajes: Prompt + Historial + Mensaje actual
const mensajesParaIA = [
{ role: "system", content: promptSistema },
...historialChat,
{ role: "user", content: mensajeCliente }
];
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: promptSistema },
{ role: "user", content: mensajeCliente }
],
temperature: 0.4, // Controlado para no divagar
messages: mensajesParaIA,
temperature: 0.3, // Muy bajo para que sea estricto con las reglas de urgencia y estados
});
return completion.choices[0].message.content;
@@ -2969,6 +2954,7 @@ app.post("/services/:id/chat", authMiddleware, async (req, res) => {
});
// 🤖 WEBHOOK CON ESCUDO DE INTERVENCIÓN HUMANA
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
app.post("/webhook/evolution", async (req, res) => {
try {
const data = req.body;
@@ -2981,13 +2967,19 @@ app.post("/webhook/evolution", async (req, res) => {
if (!mensajeTexto) return res.sendStatus(200);
// Filtro de seguridad: Evitar procesar instancias del sistema como "ADMIN"
if (!instanceName || !instanceName.startsWith("cliente_")) {
return res.sendStatus(200);
}
const ownerId = instanceName.split("_")[1];
const cleanPhone = telefonoCliente.slice(-9);
// 1. BUSCAMOS EL SINIESTRO
// 1. BUSCAMOS EL SINIESTRO (Extrayendo urgencia y población)
const svcQ = await pool.query(`
SELECT s.id, s.service_ref, u.full_name as worker_name,
st.name as status_name, s.raw_data->>'scheduled_date' as cita
SELECT s.id, s.service_ref, u.full_name as worker_name, s.is_urgent,
st.name as status_name, s.raw_data->>'scheduled_date' as cita,
s.raw_data->>'Población' as poblacion
FROM scraped_services s
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
@@ -2998,7 +2990,7 @@ app.post("/webhook/evolution", async (req, res) => {
if (svcQ.rowCount > 0) {
const service = svcQ.rows[0];
// 🛡️ 2. VERIFICAR INTERVENCIÓN HUMANA (NUEVO)
// 🛡️ 2. VERIFICAR INTERVENCIÓN HUMANA
// Miramos si el último mensaje fue de un humano en las últimas 2 horas
const checkHumanQ = await pool.query(`
SELECT sender_role, created_at
@@ -3018,52 +3010,54 @@ app.post("/webhook/evolution", async (req, res) => {
}
}
// 3. SI NO HAY HUMANO RECIENTE, LLAMAMOS A LA IA
// 3. SI NO HAY HUMANO RECIENTE, LLAMAMOS A LA IA CON TODO EL CONTEXTO
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
dbId: service.id, // 👈 CRÍTICO para que la IA lea el historial
ref: service.service_ref,
estado: service.status_name || "En proceso",
operario: service.worker_name,
cita: service.cita
cita: service.cita,
poblacion: service.poblacion || "", // 👈 CRÍTICO para buscar rutas en la misma zona
is_urgent: service.is_urgent // 👈 CRÍTICO para que no pida cita en urgencias
});
if (respuestaIA) {
// 1. --- MAGIA: DETECTAR SI LA IA RECOGIÓ UNA PROPUESTA DE CITA ---
// --- 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})\]/);
if (matchPropuesta) {
const fechaSugerida = matchPropuesta[1];
const horaSugerida = matchPropuesta[2];
const fechaSugerida = matchPropuesta[1];
const horaSugerida = matchPropuesta[2];
console.log(`📅 PROPUESTA RECIBIDA (IA): ${fechaSugerida} a las ${horaSugerida} para exp #${service.service_ref}`);
console.log(`📅 PROPUESTA RECIBIDA (IA): ${fechaSugerida} a las ${horaSugerida} para exp #${service.service_ref}`);
// 🚀 LA MAGIA: Guardar como "requested_date" y "appointment_status = pending"
// Esto es lo que lee tu panel de control para que aparezca en "Citas Solicitadas"
await pool.query(`
UPDATE scraped_services
SET raw_data = raw_data || jsonb_build_object(
'requested_date', $1::text,
'requested_time', $2::text,
'appointment_status', 'pending'
)
WHERE id = $3
`, [fechaSugerida, horaSugerida, service.id]);
// 🚀 Guardar como "requested_date" para que aparezca en el Panel en "Citas Solicitadas"
await pool.query(`
UPDATE scraped_services
SET raw_data = raw_data || jsonb_build_object(
'requested_date', $1::text,
'requested_time', $2::text,
'appointment_status', 'pending'
)
WHERE id = $3
`, [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}.`
);
}
// 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}.`
);
}
// 2. --- LIMPIEZA Y ENVÍO ---
// --- LIMPIEZA Y ENVÍO ---
// Quitamos el código [PROPUESTA:...] del texto para que el cliente no lo vea
const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/, "").trim();
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
// 3. --- REGISTRO DEL CHAT ---
// --- 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)`,
[service.id, ownerId, "Asistente IA", "ia", textoLimpio]);
}