Actualizar server.js
This commit is contained in:
111
server.js
111
server.js
@@ -937,64 +937,57 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
if (citaTime < hoyTime) citaYaPaso = true;
|
if (citaTime < hoyTime) citaYaPaso = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🛑 AÑADIDO: DETECTOR DE ESTADO FINALIZADO Y COMPAÑÍA
|
// 🛑 DETECTORES DE ESTADO
|
||||||
const esEstadoFinal = datosExpediente.estado && (datosExpediente.estado.toLowerCase().includes('finalizado') || datosExpediente.estado.toLowerCase().includes('terminado') || datosExpediente.estado.toLowerCase().includes('anulado'));
|
const esEstadoFinal = datosExpediente.estado && (datosExpediente.estado.toLowerCase().includes('finalizado') || datosExpediente.estado.toLowerCase().includes('terminado') || datosExpediente.estado.toLowerCase().includes('anulado'));
|
||||||
const nombreCia = datosExpediente.compania || "su Aseguradora";
|
const nombreCia = datosExpediente.compania || "su Aseguradora";
|
||||||
const esSeguro = !nombreCia.toLowerCase().includes('particular');
|
const esSeguro = !nombreCia.toLowerCase().includes('particular');
|
||||||
|
const noTieneTecnico = !datosExpediente.worker_id;
|
||||||
|
|
||||||
let directivaEstricta = "";
|
let directivaEstricta = "";
|
||||||
|
|
||||||
if (esEstadoFinal) {
|
if (esEstadoFinal) {
|
||||||
if (esSeguro) {
|
directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el servicio por su avería (${datosExpediente.averia}) está finalizado. NO AGENDES NADA.`;
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el informe está enviado a ${nombreCia} y esperamos respuesta. NO AGENDES NADA.`;
|
} else if (noTieneTecnico) {
|
||||||
} else {
|
directivaEstricta = `🛑 ESTADO ACTUAL: SIN TÉCNICO ASIGNADO.\nTU ÚNICO OBJETIVO: Informar al cliente que hemos recibido el aviso de su avería (${datosExpediente.averia}) y que estamos coordinando para asignarle un técnico en su zona.\n⛔ PROHIBICIÓN ABSOLUTA: NO ofrezcas citas, NO des horas, NO agendes nada hasta que se le asigne un técnico.`;
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Despídete o da soporte post-servicio. NO AGENDES NADA.`;
|
|
||||||
}
|
|
||||||
} else if (citaYaPaso) {
|
} else if (citaYaPaso) {
|
||||||
if (esSeguro) {
|
directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Informa que estamos tramitando su avería (${datosExpediente.averia}). NO AGENDES NADA.`;
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Informa que estamos tramitando el informe con ${nombreCia}. NO AGENDES NADA.`;
|
|
||||||
} else {
|
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Pregunta si el problema quedó resuelto. NO AGENDES NADA.`;
|
|
||||||
}
|
|
||||||
} else if (esUrgencia) {
|
} else if (esUrgencia) {
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: URGENCIA. Tranquiliza al cliente y dile que el técnico está avisado. NO PROPONGAS HORAS.`;
|
directivaEstricta = `🛑 ESTADO ACTUAL: URGENCIA. Tranquiliza al cliente sobre su avería (${datosExpediente.averia}) y dile que el técnico está avisado. NO PROPONGAS HORAS.`;
|
||||||
} else if (hayCitaPendiente) {
|
} else if (hayCitaPendiente) {
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN.\n📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}.\nTU ÚNICO OBJETIVO: Informar que esperamos confirmación.\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIAR o CANCELAR, ofrécele un hueco libre nuevo y si acepta, lanza la etiqueta oculta: [PROPUESTA:YYYY-MM-DD HH:mm]`;
|
directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN.\n📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}.\nTU OBJETIVO: Informar que esperamos confirmación de la oficina para reparar su avería (${datosExpediente.averia}).\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIAR o CANCELAR, ofrécele un hueco nuevo.`;
|
||||||
} else if (tieneCitaConfirmada) {
|
} else if (tieneCitaConfirmada) {
|
||||||
directivaEstricta = `🛑 ESTADO ACTUAL: CITA CONFIRMADA para el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita.\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIARLA o CANCELARLA, ofrécele un hueco libre nuevo y si acepta, lanza la etiqueta oculta: [PROPUESTA:YYYY-MM-DD HH:mm]`;
|
directivaEstricta = `🛑 ESTADO ACTUAL: CITA CONFIRMADA para el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita para su avería (${datosExpediente.averia}).\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIARLA o CANCELARLA, ofrécele un hueco nuevo.`;
|
||||||
} else {
|
} else {
|
||||||
directivaEstricta = `🟢 ESTADO ACTUAL: PENDIENTE DE AGENDAR CITA.\nTU OBJETIVO: Acordar fecha y hora. NUNCA ofrezcas horas ocupadas. Fines de semana solo URGENCIAS.\n⚠️ MUY IMPORTANTE: Si el cliente ACEPTA un hueco, aclárale que le pasas la propuesta al técnico para confirmación final. Añade AL FINAL la etiqueta oculta: [PROPUESTA:YYYY-MM-DD HH:mm]`;
|
directivaEstricta = `🟢 ESTADO ACTUAL: PENDIENTE DE AGENDAR CITA.\nTU OBJETIVO: Acordar fecha y hora para reparar su avería (${datosExpediente.averia}). NUNCA ofrezcas horas ocupadas. Fines de semana solo URGENCIAS.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptSistema = `
|
const promptSistema = `
|
||||||
Eres el coordinador humano de "${empresaNombre}". Hablas de tú, de forma muy natural, empática y al con un buen sentido de humor por WhatsApp.
|
Eres el coordinador humano de "${empresaNombre}". Hablas de tú, de forma muy natural, empática y al con un buen sentido de humor por WhatsApp.
|
||||||
|
|
||||||
--- CONTEXTO BÁSICO ---
|
--- 📋 CONTEXTO BÁSICO ---
|
||||||
- Hoy es: ${fechaHoyTexto}. (Año 2026).
|
- Hoy es: ${fechaHoyTexto}. (Año 2026).
|
||||||
- Horario de la empresa: L-V de ${horarios.m_start} a ${horarios.m_end} y de ${horarios.a_start} a ${horarios.a_end}. Fines de semana solo URGENCIAS.
|
- Horario de la empresa: L-V de ${horarios.m_start} a ${horarios.m_end} y de ${horarios.a_start} a ${horarios.a_end}. Fines de semana solo URGENCIAS.
|
||||||
- Problema o Avería reportada por el cliente: ${datosExpediente.averia || 'Avería general (no especificada)'}.
|
|
||||||
- Localidad del cliente actual: ${datosExpediente.poblacion || 'Localidad no especificada'}.
|
- Localidad del cliente actual: ${datosExpediente.poblacion || 'Localidad no especificada'}.
|
||||||
|
|
||||||
--- AGENDA DEL TÉCNICO ASIGNADO ---
|
--- 📅 AGENDA DEL TÉCNICO ASIGNADO ---
|
||||||
${agendaOcupadaTexto}
|
${agendaOcupadaTexto}
|
||||||
|
|
||||||
--- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE ---
|
--- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE ---
|
||||||
${directivaEstricta}
|
${directivaEstricta}
|
||||||
|
|
||||||
--- 🗓️ REGLAS ESTRICTAS DE FECHA Y HORA (FORMATO HUMANO) ---
|
--- ⚡ REGLA CRÍTICA DE AGENDA (COMANDO SECRETO) ---
|
||||||
1. DÍAS DE LA SEMANA: NUNCA uses fechas frías como "2026-03-10". Usa SIEMPRE el día de la semana y el mes (Ej: "el martes de la semana que viene", "el jueves día 12").
|
Si (y solo si) has propuesto un hueco y el cliente ACEPTA FIRMEMENTE, DEBES añadir AL FINAL ABSOLUTO de tu respuesta este texto literal:
|
||||||
2. TRAMOS HORARIOS: NUNCA le des una hora exacta al cliente. Usa SIEMPRE un margen de 1 hora. (Ej: "entre las 10:00 y las 11:00 aprox").
|
[PROPUESTA:YYYY-MM-DD HH:mm]
|
||||||
3. (Si estás agendando): Aunque hables en tramos, tu código interno de cierre DEBE SER la hora exacta de inicio en formato reloj: [PROPUESTA:YYYY-MM-DD HH:mm]
|
Ejemplo de respuesta tuya: "Perfecto, te agendo para el miércoles entre las 10:00 y las 11:00. ¡Nos vemos! [PROPUESTA:2026-03-25 10:00]"
|
||||||
|
⛔ PROHIBICIÓN: NUNCA menciones al cliente las palabras "código", "confirmación" ni "propuesta". Solo pega los corchetes al final de tu mensaje y ya está.
|
||||||
|
|
||||||
--- 📝 INSTRUCCIONES PERSONALIZADAS DE LA EMPRESA ---
|
--- ⚙️ REGLAS DE COMUNICACIÓN ---
|
||||||
${instruccionesExtra ? instruccionesExtra : 'No hay reglas extra.'}
|
1. MÁXIMO 2 FRASES. Mensajes cortos y directos.
|
||||||
|
2. NUNCA uses fechas frías (Usa "el martes"). NUNCA des una hora exacta (Usa "entre las 10:00 y las 11:00 aprox").
|
||||||
--- REGLAS DE ORO DE COMUNICACIÓN ---
|
3. NO TE PRESENTES si ya habéis intercambiado mensajes.
|
||||||
0. LA BASE DE DATOS MANDA: Los datos del "ESTADO ACTUAL" son la única verdad. Si contradicen el historial, la oficina ha modificado la cita.
|
4. ⛔ MULETILLAS PROHIBIDAS: NUNCA digas "¿En qué más te puedo ayudar?". Da la información y pon un punto.
|
||||||
1. Máximo 2 frases. Mensajes cortos y directos.
|
${esPrimerMensaje ? '5. Primer mensaje: preséntate y menciona el aviso (#' + datosExpediente.ref + ').' : ''}
|
||||||
2. ⛔ MULETILLAS PROHIBIDAS: NUNCA termines tus frases diciendo "Si necesitas algo más, aquí estoy", "¿En qué más te puedo ayudar?" o similares. Suena a contestador automático. Da la información y pon un punto y final.
|
${instruccionesExtra ? '6. Instrucción extra de la empresa: ' + instruccionesExtra : ''}
|
||||||
3. NO TE PRESENTES si ya habéis intercambiado mensajes antes.
|
|
||||||
${esPrimerMensaje ? '4. Primer mensaje: preséntate brevemente diciendo de dónde eres y el aviso (#' + datosExpediente.ref + ').' : ''}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
@@ -3935,7 +3928,7 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
const ownerId = instanceName.split("_")[1];
|
const ownerId = instanceName.split("_")[1];
|
||||||
const cleanPhone = telefonoCliente.slice(-9);
|
const cleanPhone = telefonoCliente.slice(-9);
|
||||||
|
|
||||||
// 🔍 BUSCAMOS EL EXPEDIENTE ACTIVO MÁS RECIENTE (Ignorando finalizados/anulados)
|
// 🔍 BUSCAMOS EL EXPEDIENTE ACTIVO MÁS RECIENTE
|
||||||
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,
|
st.name as status_name,
|
||||||
@@ -3945,14 +3938,14 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
s.raw_data->>'appointment_status' as appointment_status,
|
s.raw_data->>'appointment_status' as appointment_status,
|
||||||
s.raw_data->>'requested_date' as cita_pendiente_fecha,
|
s.raw_data->>'requested_date' as cita_pendiente_fecha,
|
||||||
s.raw_data->>'requested_time' as cita_pendiente_hora,
|
s.raw_data->>'requested_time' as cita_pendiente_hora,
|
||||||
s.raw_data->>'Compañía' as compania
|
s.raw_data->>'Compañía' as compania,
|
||||||
|
COALESCE(s.raw_data->>'Descripción', s.raw_data->>'DESCRIPCION') as averia
|
||||||
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
|
||||||
WHERE s.owner_id = $1
|
WHERE s.owner_id = $1
|
||||||
AND s.status != 'archived'
|
AND s.status != 'archived'
|
||||||
AND s.raw_data::text ILIKE $2
|
AND s.raw_data::text ILIKE $2
|
||||||
-- 👇 MAGIA: Excluimos los estados muertos para que no coja un finalizado si tiene varios partes
|
|
||||||
AND (st.name IS NULL OR (st.name NOT ILIKE '%finalizado%' AND st.name NOT ILIKE '%anulado%' AND st.name NOT ILIKE '%desasignado%'))
|
AND (st.name IS NULL OR (st.name NOT ILIKE '%finalizado%' AND st.name NOT ILIKE '%anulado%' AND st.name NOT ILIKE '%desasignado%'))
|
||||||
ORDER BY s.created_at DESC LIMIT 1
|
ORDER BY s.created_at DESC LIMIT 1
|
||||||
`, [ownerId, `%${cleanPhone}%`]);
|
`, [ownerId, `%${cleanPhone}%`]);
|
||||||
@@ -3960,19 +3953,13 @@ 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];
|
||||||
|
|
||||||
// 🚨 CAMBIO 2: EL NUEVO ESCUDO HUMANO 🚨
|
|
||||||
// Si detecta que el mensaje lo has mandado TÚ desde tu móvil de empresa
|
|
||||||
if (data.data.key.fromMe) {
|
if (data.data.key.fromMe) {
|
||||||
// Lo guarda en la base de datos para que quede constancia y la IA sepa que estás al mando
|
|
||||||
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, "Técnico (WhatsApp)", "operario", mensajeTexto]);
|
[service.id, ownerId, "Técnico (WhatsApp)", "operario", mensajeTexto]);
|
||||||
return; // Cortamos la ejecución. ¡ChatGPT no dirá ni mu!
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🛑 SEMÁFORO ANTI-METRALLETA
|
|
||||||
if (candadosIA.has(service.id)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (candadosIA.has(service.id)) return;
|
||||||
candadosIA.add(service.id);
|
candadosIA.add(service.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3985,11 +3972,11 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
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);
|
||||||
// Como tu mensaje se guardó como 'operario', aquí saltará esta regla y detendrá a la IA durante 120 min
|
// PUESTO A 0 PARA PRUEBAS: CÁMBIALO A 120 CUANDO TERMINES
|
||||||
if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 0) return;
|
if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 0) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🧠 LLAMADA A LA IA (Con la hora inyectada)
|
// 🧠 LLAMADA A LA IA
|
||||||
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
||||||
dbId: service.id,
|
dbId: service.id,
|
||||||
ref: service.service_ref,
|
ref: service.service_ref,
|
||||||
@@ -3997,37 +3984,47 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
operario: service.worker_name,
|
operario: service.worker_name,
|
||||||
worker_id: service.assigned_to,
|
worker_id: service.assigned_to,
|
||||||
cita: service.cita,
|
cita: service.cita,
|
||||||
hora_cita: service.hora_cita, // 👈 AHORA SÍ PASA LA HORA EXACTA
|
hora_cita: service.hora_cita,
|
||||||
poblacion: service.poblacion || "",
|
poblacion: service.poblacion || "",
|
||||||
is_urgent: service.is_urgent,
|
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,
|
||||||
compania: service.compania // 👈 NUEVO: PASAMOS LA COMPAÑÍA
|
compania: service.compania,
|
||||||
|
averia: service.averia
|
||||||
});
|
});
|
||||||
|
|
||||||
if (respuestaIA) {
|
if (respuestaIA) {
|
||||||
|
// 🛡️ REGEX BLINDADO: Pilla la etiqueta aunque la IA meta espacios raros
|
||||||
const matchPropuesta = respuestaIA.match(/\[PROPUESTA:\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]/i);
|
const matchPropuesta = respuestaIA.match(/\[PROPUESTA:\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]/i);
|
||||||
|
|
||||||
if (matchPropuesta) {
|
if (matchPropuesta) {
|
||||||
const fechaSugerida = matchPropuesta[1];
|
const fechaSugerida = matchPropuesta[1];
|
||||||
const horaSugerida = matchPropuesta[2];
|
const horaSugerida = matchPropuesta[2];
|
||||||
await pool.query(`
|
|
||||||
UPDATE scraped_services
|
// 🚀 GUARDADO DIRECTO A CITA CONFIRMADA (Va al calendario del Operario sin preguntar)
|
||||||
SET raw_data = raw_data || jsonb_build_object(
|
const statusQ = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%citado%' LIMIT 1", [ownerId]);
|
||||||
'requested_date', $1::text,
|
const idCitado = statusQ.rowCount > 0 ? String(statusQ.rows[0].id) : null;
|
||||||
'requested_time', $2::text,
|
|
||||||
'appointment_status', 'pending'
|
const rawQ = await pool.query("SELECT raw_data FROM scraped_services WHERE id=$1", [service.id]);
|
||||||
) WHERE id = $3
|
let rawActual = rawQ.rows[0].raw_data || {};
|
||||||
`, [fechaSugerida, horaSugerida, service.id]);
|
|
||||||
|
rawActual.scheduled_date = fechaSugerida;
|
||||||
|
rawActual.scheduled_time = horaSugerida;
|
||||||
|
rawActual.appointment_status = 'approved';
|
||||||
|
if (idCitado) rawActual.status_operativo = idCitado;
|
||||||
|
|
||||||
|
await pool.query("UPDATE scraped_services SET raw_data = $1 WHERE id = $2", [JSON.stringify(rawActual), service.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/, "").trim();
|
// 🧹 BORRAMOS EL TEXTO DEL CÓDIGO PARA QUE EL CLIENTE NO LO VEA NUNCA
|
||||||
|
const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/gi, "").replace(/código:/gi, "").trim();
|
||||||
|
|
||||||
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
|
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
|
||||||
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 {
|
} finally {
|
||||||
// 🟢 ABRIMOS EL CANDADO SIEMPRE AL TERMINAR (Aunque haya error)
|
|
||||||
candadosIA.delete(service.id);
|
candadosIA.delete(service.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user