feat(api): implementar endpoints M2M /api/external/tickets para sigma-robot y worker de integracion

This commit is contained in:
2026-10-27 11:56:45 -05:00
parent 5c7a8c925b
commit 10a614ed47
5 changed files with 664 additions and 7 deletions
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env node
/**
* scripts/sigma_robot_worker.js
* Worker de comunicación e integración entre Help Desk y sigma-robot.
*
* Funciones:
* 1. Lee mensajes pendientes del chat de sigma-robot para Agente Help Desk (ID 90).
* 2. Procesa solicitudes de creación de tickets y las registra en Help Desk.
* 3. Responde por el chat de sigma-robot confirmando la creación con link directo.
* 4. Si la tarea requiere intervención humana o está en proceso, avisa en el chat.
*/
require('dotenv').config();
const Ticket = require('../models/Ticket');
const Company = require('../models/Company');
const Department = require('../models/Department');
const User = require('../models/User');
const SIGMA_BOT_URL = process.env.SIGMA_BOT_URL || 'https://sigma-bot.telcotronics.com';
const TOKEN = process.env.SIGMA_AGENTE_TOKEN;
const AGENTE_ID = 90; // Agente Help Desk
if (!TOKEN) {
console.error('❌ Error: SIGMA_AGENTE_TOKEN no configurado en .env');
process.exit(1);
}
/**
* Consulta mensajes pendientes en sigma-robot
*/
async function leerMensajesPendientes() {
const res = await fetch(`${SIGMA_BOT_URL}/ecosistema/v1/mensajes/pendientes`, {
headers: { 'Authorization': `Bearer ${TOKEN}` }
});
if (!res.ok) {
throw new Error(`Error ${res.status} leyendo pendientes: ${await res.text()}`);
}
const data = await res.json();
return data.mensajes || [];
}
/**
* Envía un mensaje a sigma-robot
*/
async function enviarMensaje({ para, agente_id, mensaje }) {
const body = { para, mensaje };
if (para === 'agente' && agente_id) {
body.agente_id = parseInt(agente_id);
}
const res = await fetch(`${SIGMA_BOT_URL}/ecosistema/v1/mensajes`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!res.ok) {
throw new Error(`Error enviando mensaje: ${await res.text()}`);
}
return await res.json();
}
/**
* Parsea el texto del mensaje para extraer parámetros de ticket si existen
*/
function parsearSolicitudTicket(texto) {
const raw = String(texto || '').trim();
const lower = raw.toLowerCase();
const esTicket = lower.includes('ticket') || lower.includes('incidencia') || lower.includes('soporte');
const esCrear = lower.includes('crear ticket') || lower.includes('abrir ticket') || lower.includes('nuevo ticket') || lower.startsWith('ticket:');
// Evitar falsos positivos con propuestas arquitectónicas o mensajes de coordinación
if (lower.includes('propongo') || lower.includes('contrato') || lower.includes('revisé tu código') || lower.includes('preguntas para cerrar') || lower.includes('fase 2')) {
return null;
}
if (!esTicket || !esCrear) {
return null;
}
// Detectar Empresa
let companyId = 1; // Default: Telcotronics
if (lower.includes('xsystem') || lower.includes('empresa 2') || lower.includes('empresa=2')) {
companyId = 2;
} else if (lower.includes('domus') || lower.includes('empresa 3') || lower.includes('empresa=3')) {
companyId = 3;
} else if (lower.includes('telcotronics') || lower.includes('empresa 1') || lower.includes('empresa=1')) {
companyId = 1;
}
// Detectar Prioridad
let prioridad = 'media';
if (lower.includes('prioridad=alta') || lower.includes('prioridad: alta') || lower.includes('alta')) prioridad = 'alta';
if (lower.includes('prioridad=critica') || lower.includes('prioridad: critica') || lower.includes('critica') || lower.includes('urgente')) prioridad = 'critica';
if (lower.includes('prioridad=baja') || lower.includes('prioridad: baja') || lower.includes('baja')) prioridad = 'baja';
// Extraer Asunto y Mensaje
let asunto = 'Solicitud recibida desde sigma-robot';
let mensaje = raw;
// Patrón "asunto: ... | mensaje: ..." o "crear ticket: [asunto] - [mensaje]"
const matchAsunto = raw.match(/asunto\s*[:=]\s*([^|\n]+)/i);
const matchMensaje = raw.match(/(?:mensaje|descripcion|detalle)\s*[:=]\s*([^|\n]+)/i);
if (matchAsunto) {
asunto = matchAsunto[1].trim();
}
if (matchMensaje) {
mensaje = matchMensaje[1].trim();
} else if (!matchAsunto) {
// Limpiar prefijo tipo "crear ticket:" y usar las primeras palabras
const cleaned = raw.replace(/^(?:@?\w+\s+)?(?:por\s+favor\s+)?(?:crear|abrir|generar)?\s*(?:un\s+)?ticket(?:\s*[:\-])?\s*/i, '').trim();
if (cleaned.length > 0) {
const lineas = cleaned.split('\n').filter(Boolean);
asunto = lineas[0].substring(0, 80);
mensaje = cleaned;
}
}
return {
companyId,
asunto: asunto.substring(0, 150),
mensaje,
prioridad
};
}
/**
* Ejecuta un ciclo de revisión y procesamiento
*/
async function procesarChat() {
console.log(`[${new Date().toISOString()}] 🔍 Revisando buzón de chat en sigma-robot...`);
const pendientes = await leerMensajesPendientes();
console.log(` 📨 Mensajes nuevos recibidos: ${pendientes.length}`);
if (pendientes.length === 0) {
return { total: 0, creados: 0, respondidos: 0 };
}
let creados = 0;
let respondidos = 0;
for (const msg of pendientes) {
console.log(`\n--- Mensaje #${msg.id} de ${msg.remitente_nombre || msg.remitente_usuario || 'desconocido'} ---`);
console.log(`Texto: "${msg.mensaje}"`);
// Si es un mensaje propio del agente, ignorar
if (msg.remitente_agente_id === AGENTE_ID) {
continue;
}
const solicitud = parsearSolicitudTicket(msg.mensaje);
// Destino para la respuesta
const esDirecto = msg.destino_tipo === 'agente';
const canalRespuesta = esDirecto
? (msg.remitente_tipo === 'pablinux' ? 'pablinux' : 'agente')
: 'general';
const agenteDestinoId = msg.remitente_agente_id;
if (solicitud) {
console.log(` 🎯 Detectada solicitud de ticket:`, solicitud);
try {
const company = await Company.findById(solicitud.companyId) || await Company.findById(1);
// Buscar o aprovisionar usuario solicitante
const remitenteNombre = msg.remitente_nombre || msg.remitente_usuario || 'Agente Externo';
const remitenteEmail = `sigmabot_${(msg.remitente_proyecto || 'robot').toLowerCase()}@sigmac.app`;
let user = await User.findByEmail(remitenteEmail);
if (!user) {
user = await User.upsertExternal(remitenteNombre, remitenteEmail, 'sigma-robot', String(msg.id), null, company.id);
}
// Departamento por defecto
const depts = await Department.findAll(company.id);
const deptId = depts.length > 0 ? depts[0].id : null;
const ticketId = await Ticket.create(
company.id,
user.id,
deptId,
solicitud.asunto,
solicitud.mensaje,
solicitud.prioridad
);
const ticketUrl = `https://soporte.sigmac.app/agent/ticket/${ticketId}`;
console.log(` ✓ Ticket #${ticketId} creado exitosamente en ${company.name}`);
const respuestaTexto = `✅ Ticket #${ticketId} creado en ${company.name}.\n• Asunto: "${solicitud.asunto}"\n• Prioridad: ${solicitud.prioridad}\n• URL: ${ticketUrl}`;
await enviarMensaje({
para: canalRespuesta,
agente_id: agenteDestinoId,
mensaje: respuestaTexto
});
creados++;
respondidos++;
} catch (err) {
console.error(` ❌ Error creando ticket para mensaje #${msg.id}:`, err.message);
await enviarMensaje({
para: canalRespuesta,
agente_id: agenteDestinoId,
mensaje: `⚠️ No se pudo crear el ticket para la solicitud #${msg.id}: ${err.message}`
});
}
} else {
// No es un comando de ticket directo.
// Si es un mensaje de Pablinux o directo solicitando una tarea compleja:
if (msg.remitente_tipo === 'pablinux') {
console.log(' 💬 Mensaje directo de Pablinux recibido.');
// Si requiere confirmación o está en curso:
if (msg.mensaje.toLowerCase().includes('recibido') || msg.mensaje.toLowerCase().includes('continua')) {
console.log(' ✓ Instrucción de continuidad reconocida.');
} else {
const respuestaTexto = `👍 Entendido @pablinux. Mensaje recibido por Agente Help Desk (ID 90). Tarea en curso / esperando tus indicaciones para ejecutar.`;
await enviarMensaje({
para: 'pablinux',
mensaje: respuestaTexto
});
respondidos++;
}
} else if (esDirecto) {
console.log(` ℹ️ Mensaje informativo de otro agente (${msg.remitente_nombre}).`);
const respuestaTexto = `👋 Hola ${msg.remitente_nombre || 'Agente'}. Mensaje recibido en Agente Help Desk (ID 90). Si requieres registrar un ticket de soporte o FSM, indícalo con "crear ticket: [asunto] - [detalle]".`;
await enviarMensaje({
para: 'agente',
agente_id: agenteDestinoId,
mensaje: respuestaTexto
});
respondidos++;
}
}
}
console.log(`\n✅ Ciclo finalizado. Total: ${pendientes.length}, Tickets creados: ${creados}, Respuestas enviadas: ${respondidos}`);
return { total: pendientes.length, creados, respondidos };
}
// Ejecución
if (require.main === module) {
procesarChat()
.then(() => process.exit(0))
.catch(err => {
console.error('❌ Error fatal en sigma_robot_worker:', err);
process.exit(1);
});
}
module.exports = { procesarChat, parsearSolicitudTicket };