Actualizar server.js
This commit is contained in:
109
server.js
109
server.js
@@ -937,64 +937,57 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
||||
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 nombreCia = datosExpediente.compania || "su Aseguradora";
|
||||
const esSeguro = !nombreCia.toLowerCase().includes('particular');
|
||||
const noTieneTecnico = !datosExpediente.worker_id;
|
||||
|
||||
let directivaEstricta = "";
|
||||
|
||||
if (esEstadoFinal) {
|
||||
if (esSeguro) {
|
||||
directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el informe está enviado a ${nombreCia} y esperamos respuesta. NO AGENDES NADA.`;
|
||||
} else {
|
||||
directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Despídete o da soporte post-servicio. NO AGENDES NADA.`;
|
||||
}
|
||||
directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el servicio por su avería (${datosExpediente.averia}) está finalizado. NO AGENDES NADA.`;
|
||||
} else if (noTieneTecnico) {
|
||||
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.`;
|
||||
} else if (citaYaPaso) {
|
||||
if (esSeguro) {
|
||||
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.`;
|
||||
}
|
||||
directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Informa que estamos tramitando su avería (${datosExpediente.averia}). NO AGENDES NADA.`;
|
||||
} 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) {
|
||||
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) {
|
||||
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 {
|
||||
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 = `
|
||||
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).
|
||||
- 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'}.
|
||||
|
||||
--- AGENDA DEL TÉCNICO ASIGNADO ---
|
||||
--- 📅 AGENDA DEL TÉCNICO ASIGNADO ---
|
||||
${agendaOcupadaTexto}
|
||||
|
||||
--- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE ---
|
||||
${directivaEstricta}
|
||||
|
||||
--- 🗓️ REGLAS ESTRICTAS DE FECHA Y HORA (FORMATO HUMANO) ---
|
||||
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").
|
||||
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").
|
||||
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]
|
||||
--- ⚡ REGLA CRÍTICA DE AGENDA (COMANDO SECRETO) ---
|
||||
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:
|
||||
[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 ---
|
||||
${instruccionesExtra ? instruccionesExtra : 'No hay reglas extra.'}
|
||||
|
||||
--- REGLAS DE ORO DE COMUNICACIÓN ---
|
||||
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.
|
||||
1. Máximo 2 frases. Mensajes cortos y directos.
|
||||
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.
|
||||
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 + ').' : ''}
|
||||
--- ⚙️ REGLAS DE COMUNICACIÓN ---
|
||||
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").
|
||||
3. NO TE PRESENTES si ya habéis intercambiado mensajes.
|
||||
4. ⛔ MULETILLAS PROHIBIDAS: NUNCA digas "¿En qué más te puedo ayudar?". Da la información y pon un punto.
|
||||
${esPrimerMensaje ? '5. Primer mensaje: preséntate y menciona el aviso (#' + datosExpediente.ref + ').' : ''}
|
||||
${instruccionesExtra ? '6. Instrucción extra de la empresa: ' + instruccionesExtra : ''}
|
||||
`;
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
@@ -3935,7 +3928,7 @@ app.post("/webhook/evolution", async (req, res) => {
|
||||
const ownerId = instanceName.split("_")[1];
|
||||
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(`
|
||||
SELECT s.id, s.service_ref, s.assigned_to, u.full_name as worker_name, s.is_urgent,
|
||||
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->>'requested_date' as cita_pendiente_fecha,
|
||||
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
|
||||
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
|
||||
WHERE s.owner_id = $1
|
||||
AND s.status != 'archived'
|
||||
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%'))
|
||||
ORDER BY s.created_at DESC LIMIT 1
|
||||
`, [ownerId, `%${cleanPhone}%`]);
|
||||
@@ -3960,19 +3953,13 @@ app.post("/webhook/evolution", async (req, res) => {
|
||||
if (svcQ.rowCount > 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) {
|
||||
// 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)`,
|
||||
[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;
|
||||
}
|
||||
|
||||
if (candadosIA.has(service.id)) return;
|
||||
candadosIA.add(service.id);
|
||||
|
||||
try {
|
||||
@@ -3985,11 +3972,11 @@ app.post("/webhook/evolution", async (req, res) => {
|
||||
if (checkHumanQ.rowCount > 0) {
|
||||
const lastMsg = checkHumanQ.rows[0];
|
||||
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;
|
||||
}
|
||||
|
||||
// 🧠 LLAMADA A LA IA (Con la hora inyectada)
|
||||
// 🧠 LLAMADA A LA IA
|
||||
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
||||
dbId: service.id,
|
||||
ref: service.service_ref,
|
||||
@@ -3997,37 +3984,47 @@ app.post("/webhook/evolution", async (req, res) => {
|
||||
operario: service.worker_name,
|
||||
worker_id: service.assigned_to,
|
||||
cita: service.cita,
|
||||
hora_cita: service.hora_cita, // 👈 AHORA SÍ PASA LA HORA EXACTA
|
||||
hora_cita: service.hora_cita,
|
||||
poblacion: service.poblacion || "",
|
||||
is_urgent: service.is_urgent,
|
||||
appointment_status: service.appointment_status,
|
||||
cita_pendiente_fecha: service.cita_pendiente_fecha,
|
||||
cita_pendiente_hora: service.cita_pendiente_hora,
|
||||
compania: service.compania // 👈 NUEVO: PASAMOS LA COMPAÑÍA
|
||||
compania: service.compania,
|
||||
averia: service.averia
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
if (matchPropuesta) {
|
||||
const fechaSugerida = matchPropuesta[1];
|
||||
const horaSugerida = matchPropuesta[2];
|
||||
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]);
|
||||
|
||||
// 🚀 GUARDADO DIRECTO A CITA CONFIRMADA (Va al calendario del Operario sin preguntar)
|
||||
const statusQ = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%citado%' LIMIT 1", [ownerId]);
|
||||
const idCitado = statusQ.rowCount > 0 ? String(statusQ.rows[0].id) : null;
|
||||
|
||||
const rawQ = await pool.query("SELECT raw_data FROM scraped_services WHERE id=$1", [service.id]);
|
||||
let rawActual = rawQ.rows[0].raw_data || {};
|
||||
|
||||
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 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]);
|
||||
}
|
||||
} finally {
|
||||
// 🟢 ABRIMOS EL CANDADO SIEMPRE AL TERMINAR (Aunque haya error)
|
||||
candadosIA.delete(service.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user