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
+8 -2
View File
@@ -16,6 +16,7 @@ const agentRoutes = require('./routes/agent');
const adminRoutes = require('./routes/admin'); const adminRoutes = require('./routes/admin');
const saasRoutes = require('./routes/saas'); const saasRoutes = require('./routes/saas');
const companyRoutes = require('./routes/company'); const companyRoutes = require('./routes/company');
const apiRoutes = require('./routes/api');
const app = express(); const app = express();
@@ -51,6 +52,7 @@ app.use('/agent', agentRoutes);
app.use('/admin', adminRoutes); app.use('/admin', adminRoutes);
app.use('/saas', saasRoutes); app.use('/saas', saasRoutes);
app.use('/company', companyRoutes); app.use('/company', companyRoutes);
app.use('/api', apiRoutes);
const { reportarError } = require('./config/errorReporter'); const { reportarError } = require('./config/errorReporter');
@@ -86,6 +88,10 @@ app.use((err, req, res, next) => {
}); });
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { if (require.main === module) {
app.listen(PORT, () => {
console.log(`Servidor corriendo en puerto ${PORT}`); console.log(`Servidor corriendo en puerto ${PORT}`);
}); });
}
module.exports = app;
+23 -1
View File
@@ -76,10 +76,16 @@ async function setup() {
await migrateColumn('users', 'invite_token', 'invite_token VARCHAR(100) NULL'); await migrateColumn('users', 'invite_token', 'invite_token VARCHAR(100) NULL');
await migrateColumn('users', 'invite_token_expires', 'invite_token_expires TIMESTAMP NULL'); await migrateColumn('users', 'invite_token_expires', 'invite_token_expires TIMESTAMP NULL');
await migrateColumn('users', 'invited_by', 'invited_by INT NULL'); await migrateColumn('users', 'invited_by', 'invited_by INT NULL');
await migrateColumn('users', 'provider', "provider ENUM('local', 'webcontrol', 'google', 'facebook') DEFAULT 'local'"); await migrateColumn('users', 'provider', "provider VARCHAR(50) DEFAULT 'local'");
await migrateColumn('users', 'provider_id', "provider_id VARCHAR(150) NULL"); await migrateColumn('users', 'provider_id', "provider_id VARCHAR(150) NULL");
await migrateColumn('users', 'avatar_url', "avatar_url VARCHAR(255) NULL"); await migrateColumn('users', 'avatar_url', "avatar_url VARCHAR(255) NULL");
try {
await conn.query("ALTER TABLE users MODIFY COLUMN provider VARCHAR(50) DEFAULT 'local'");
} catch (e) {
// Ignorar
}
try { try {
await conn.query("ALTER TABLE users MODIFY COLUMN password VARCHAR(255) NULL"); await conn.query("ALTER TABLE users MODIFY COLUMN password VARCHAR(255) NULL");
console.log(' + users.password modificado a nullable'); console.log(' + users.password modificado a nullable');
@@ -114,6 +120,22 @@ async function setup() {
await conn.query('ALTER TABLE tickets ADD FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE'); await conn.query('ALTER TABLE tickets ADD FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE');
} catch (e) { if (e.errno !== 1826 && e.errno !== 1005) throw e; } } catch (e) { if (e.errno !== 1826 && e.errno !== 1005) throw e; }
// Columnas M2M / sigma-robot en tickets
await migrateColumn('tickets', 'origen', "origen VARCHAR(50) DEFAULT 'web'");
await migrateColumn('tickets', 'origen_externo_id', 'origen_externo_id VARCHAR(100) NULL');
await migrateColumn('tickets', 'app_origen', 'app_origen VARCHAR(100) NULL');
await migrateColumn('tickets', 'solicitante_nombre', 'solicitante_nombre VARCHAR(150) NULL');
await migrateColumn('tickets', 'solicitante_email', 'solicitante_email VARCHAR(150) NULL');
await migrateColumn('tickets', 'solicitante_telefono', 'solicitante_telefono VARCHAR(50) NULL');
await migrateColumn('tickets', 'tipo', "tipo VARCHAR(50) DEFAULT 'consulta'");
try {
await conn.query('ALTER TABLE tickets ADD UNIQUE KEY idx_tickets_origen_ext (company_id, origen, origen_externo_id)');
console.log(' + indice unico idx_tickets_origen_ext creado');
} catch (e) {
if (e.errno === 1061) console.log(' ~ indice idx_tickets_origen_ext ya existe');
}
// Create ticket_visitas table // Create ticket_visitas table
await conn.query(` await conn.query(`
CREATE TABLE IF NOT EXISTS ticket_visitas ( CREATE TABLE IF NOT EXISTS ticket_visitas (
+23 -3
View File
@@ -48,13 +48,33 @@ const Ticket = {
`, [id]); `, [id]);
return rows[0]; return rows[0];
}, },
create: async (companyId, userId, departmentId, asunto, mensaje, prioridad = 'media') => { create: async (companyId, userId, departmentId, asunto, mensaje, prioridad = 'media', extra = {}) => {
const {
origen = 'web',
origen_externo_id = null,
app_origen = null,
solicitante_nombre = null,
solicitante_email = null,
solicitante_telefono = null,
tipo = 'consulta'
} = extra;
const [result] = await db.query( const [result] = await db.query(
'INSERT INTO tickets (company_id, user_id, department_id, asunto, mensaje, prioridad) VALUES (?, ?, ?, ?, ?, ?)', `INSERT INTO tickets
[companyId, userId, departmentId, asunto, mensaje, prioridad] (company_id, user_id, department_id, asunto, mensaje, prioridad, origen, origen_externo_id, app_origen, solicitante_nombre, solicitante_email, solicitante_telefono, tipo)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[companyId, userId, departmentId, asunto, mensaje, prioridad, origen, origen_externo_id, app_origen, solicitante_nombre, solicitante_email, solicitante_telefono, tipo]
); );
return result.insertId; return result.insertId;
}, },
findByOrigenExterno: async (companyId, origen, origenExternoId) => {
if (!origenExternoId) return null;
const [rows] = await db.query(
'SELECT * FROM tickets WHERE company_id = ? AND origen = ? AND origen_externo_id = ?',
[companyId, origen, String(origenExternoId)]
);
return rows[0] || null;
},
update: async (id, data) => { update: async (id, data) => {
const fields = []; const fields = [];
const values = []; const values = [];
+354
View File
@@ -0,0 +1,354 @@
const express = require('express');
const router = express.Router();
const Ticket = require('../models/Ticket');
const Company = require('../models/Company');
const Department = require('../models/Department');
const User = require('../models/User');
/**
* Middleware para validar llamadas M2M mediante API Key o Token de Agente
*/
async function authenticateM2M(req, res, next) {
const apiKey = req.header('x-api-key');
const authHeader = req.header('Authorization');
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
const provided = apiKey || token;
if (!provided) {
return res.status(401).json({
ok: false,
success: false,
error: 'Autenticación requerida. Proporcione header x-api-key o Authorization: Bearer <token>'
});
}
// 1. Validar contra variables locales de entorno autorizadas
const validTokens = [
process.env.WEBCONTROL_API_KEY,
process.env.SIGMA_AGENTE_TOKEN
].filter(Boolean);
if (validTokens.includes(provided)) {
req.authSource = 'local_env';
return next();
}
// 2. Validar contra la base de datos central webControl si está disponible
try {
const mysql = require('mysql2/promise');
const conn = await mysql.createConnection({
host: '192.168.10.149',
user: 'admin',
password: 'Dx.1706%',
database: 'webControl'
});
const [rows] = await conn.query(
'SELECT * FROM api_key WHERE key_value = ? AND is_active = 1 AND revoked_at IS NULL',
[provided]
);
await conn.end();
if (rows.length > 0) {
req.authSource = 'webcontrol_db';
req.apiKeyData = rows[0];
return next();
}
} catch (e) {
console.warn('[API Auth] Error consultando webControl.api_key:', e.message);
}
return res.status(403).json({
ok: false,
success: false,
error: 'Credencial inválida o no autorizada para el servicio de Help Desk'
});
}
// Aplicar autenticación M2M a todas las rutas de /api
router.use(authenticateM2M);
/**
* Función auxiliar para resolver o crear el usuario técnico de integración
*/
async function getTechnicalUser(companyId) {
const email = `bot_sigma-robot@sigmac.app`;
let user = await User.findByEmail(email);
if (!user) {
const id = await User.create(
companyId,
'Bot Soporte (sigma-robot)',
email,
null,
'client',
'bot',
'sigma-robot-m2m'
);
user = await User.findById(id);
}
return user;
}
/**
* Función auxiliar para resolver la empresa por ID, slug o dominio de app_origen
*/
async function resolveCompany(empresa, app_origen, company_id) {
if (company_id) {
const c = await Company.findById(parseInt(company_id));
if (c) return c;
}
if (empresa) {
if (!isNaN(empresa)) {
const c = await Company.findById(parseInt(empresa));
if (c) return c;
} else {
const c = await Company.findBySlug(String(empresa).toLowerCase().trim());
if (c) return c;
}
}
if (app_origen) {
const originLower = String(app_origen).toLowerCase();
if (originLower.includes('domus')) {
const c = await Company.findBySlug('domus');
if (c) return c;
}
if (originLower.includes('xsystem') || originLower.includes('webcontrol')) {
const c = await Company.findBySlug('xsystem');
if (c) return c;
}
}
// Default a Telcotronics (ID 1)
return (await Company.findById(1)) || { id: 1, name: 'Telcotronics', slug: 'telcotronics' };
}
/**
* Función auxiliar para resolver departamento según tipo
*/
async function resolveDepartment(companyId, tipo, departamento) {
const depts = await Department.findAll(companyId);
if (depts.length === 0) return null;
if (departamento) {
const found = depts.find(d =>
d.nombre.toLowerCase().includes(String(departamento).toLowerCase())
);
if (found) return found.id;
}
const tipoLower = String(tipo || '').toLowerCase();
let preferredName = 'Soporte Técnico';
if (tipoLower === 'cobro' || tipoLower === 'facturacion') preferredName = 'Facturación';
if (tipoLower === 'consulta' || tipoLower === 'otro') preferredName = 'General';
const match = depts.find(d => d.nombre.toLowerCase().includes(preferredName.toLowerCase()));
return match ? match.id : depts[0].id;
}
/**
* GET /api/empresas — Lista de empresas del ecosistema para canalizar tickets
*/
router.get('/empresas', async (req, res) => {
try {
const companies = await Company.findAll();
const simplified = companies.map(c => ({
id: c.id,
name: c.name,
slug: c.slug,
plan: c.plan
}));
res.json({ ok: true, success: true, empresas: simplified });
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
});
/**
* POST /api/external/tickets y POST /api/tickets
* Crea un ticket M2M con soporte para idempotencia, solicitante y categorización
*/
async function handleCreateTicket(req, res) {
try {
const {
origen = 'sigma-robot',
origen_externo_id = null,
empresa,
company_id,
app_origen = null,
solicitante = {},
asunto,
mensaje,
descripcion,
prioridad = 'media',
tipo = 'consulta',
departamento,
department_id
} = req.body;
const ticketAsunto = asunto || (mensaje ? String(mensaje).substring(0, 60) + '...' : null);
const ticketMensaje = mensaje || descripcion;
if (!ticketAsunto || !ticketMensaje) {
return res.status(400).json({
ok: false,
success: false,
error: 'Los campos asunto y mensaje son obligatorios'
});
}
// 1. Resolver Empresa
const company = await resolveCompany(empresa, app_origen, company_id);
// 2. Idempotencia: Verificar si ya existe este origen_externo_id
if (origen_externo_id) {
const existing = await Ticket.findByOrigenExterno(company.id, origen, origen_externo_id);
if (existing) {
return res.status(200).json({
ok: true,
success: true,
duplicado: true,
ticket_id: existing.id,
estado: existing.status,
empresa: company.name,
url: `https://soporte.sigmac.app/agent/ticket/${existing.id}`
});
}
}
// 3. Resolver Usuario Técnico y Solicitante
const techUser = await getTechnicalUser(company.id);
const solNombre = solicitante.nombre || req.body.cliente_nombre || null;
const solEmail = solicitante.email || req.body.cliente_email || null;
const solTelefono = solicitante.telefono || req.body.cliente_telefono || null;
// 4. Resolver Departamento
const deptId = department_id ? parseInt(department_id) : await resolveDepartment(company.id, tipo, departamento);
// 5. Prioridad normalizada
const cleanPriority = ['baja', 'media', 'alta', 'critica'].includes(String(prioridad).toLowerCase())
? String(prioridad).toLowerCase()
: 'media';
// 6. Crear Ticket
const ticketId = await Ticket.create(
company.id,
techUser.id,
deptId,
ticketAsunto.trim(),
ticketMensaje.trim(),
cleanPriority,
{
origen,
origen_externo_id: origen_externo_id ? String(origen_externo_id) : null,
app_origen,
solicitante_nombre: solNombre,
solicitante_email: solEmail,
solicitante_telefono: solTelefono,
tipo
}
);
const url = `https://soporte.sigmac.app/agent/ticket/${ticketId}`;
return res.status(201).json({
ok: true,
success: true,
ticket_id: ticketId,
estado: 'abierto',
empresa: company.name,
company_id: company.id,
asunto: ticketAsunto.trim(),
prioridad: cleanPriority,
url
});
} catch (err) {
console.error('[API Tickets] Error:', err);
return res.status(500).json({ ok: false, success: false, error: err.message });
}
}
router.post('/external/tickets', handleCreateTicket);
router.post('/tickets', handleCreateTicket);
/**
* GET /api/external/tickets/:id y GET /api/tickets/:id
* Consulta el estado y última respuesta de un ticket
*/
async function handleGetTicket(req, res) {
try {
const ticketId = parseInt(req.params.id);
const ticket = await Ticket.findById(ticketId);
if (!ticket) {
return res.status(404).json({ ok: false, success: false, error: 'Ticket no encontrado' });
}
const respuestas = await Ticket.getResponses(ticketId);
const ultimaRespuesta = respuestas.length > 0 ? respuestas[respuestas.length - 1] : null;
res.json({
ok: true,
success: true,
ticket_id: ticket.id,
estado: ticket.status,
asunto: ticket.asunto,
prioridad: ticket.prioridad,
asignado_a: ticket.asignado_nombre || null,
actualizado: ticket.updated_at || ticket.created_at,
origen: ticket.origen,
solicitante: {
nombre: ticket.solicitante_nombre || ticket.usuario_nombre,
email: ticket.solicitante_email || ticket.usuario_email
},
ultima_respuesta: ultimaRespuesta ? {
autor: ultimaRespuesta.usuario_nombre,
mensaje: ultimaRespuesta.mensaje,
fecha: ultimaRespuesta.created_at
} : null,
url: `https://soporte.sigmac.app/agent/ticket/${ticket.id}`
});
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
}
router.get('/external/tickets/:id', handleGetTicket);
router.get('/tickets/:id', handleGetTicket);
/**
* POST /api/external/tickets/:id/notas y POST /api/tickets/:id/respuestas
* Agrega una nota o seguimiento al ticket
*/
async function handleAddNote(req, res) {
try {
const ticketId = parseInt(req.params.id);
const { texto, mensaje, autor = 'sigma-robot' } = req.body;
const contenido = texto || mensaje;
if (!contenido) {
return res.status(400).json({ ok: false, success: false, error: 'El campo texto es obligatorio' });
}
const ticket = await Ticket.findById(ticketId);
if (!ticket) {
return res.status(404).json({ ok: false, success: false, error: 'Ticket no encontrado' });
}
const techUser = await getTechnicalUser(ticket.company_id);
const notaCompleta = `[${autor}]: ${contenido}`;
const respId = await Ticket.addResponse(ticketId, techUser.id, notaCompleta);
res.json({
ok: true,
success: true,
message: 'Nota agregada exitosamente',
respuesta_id: respId,
ticket_id: ticketId
});
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
}
router.post('/external/tickets/:id/notas', handleAddNote);
router.post('/tickets/:id/respuestas', handleAddNote);
module.exports = router;
+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 };