diff --git a/app.js b/app.js index f59b25a..43b18ae 100644 --- a/app.js +++ b/app.js @@ -16,6 +16,7 @@ const agentRoutes = require('./routes/agent'); const adminRoutes = require('./routes/admin'); const saasRoutes = require('./routes/saas'); const companyRoutes = require('./routes/company'); +const apiRoutes = require('./routes/api'); const app = express(); @@ -51,6 +52,7 @@ app.use('/agent', agentRoutes); app.use('/admin', adminRoutes); app.use('/saas', saasRoutes); app.use('/company', companyRoutes); +app.use('/api', apiRoutes); const { reportarError } = require('./config/errorReporter'); @@ -86,6 +88,10 @@ app.use((err, req, res, next) => { }); const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`Servidor corriendo en puerto ${PORT}`); -}); +if (require.main === module) { + app.listen(PORT, () => { + console.log(`Servidor corriendo en puerto ${PORT}`); + }); +} + +module.exports = app; diff --git a/config/setup.js b/config/setup.js index b08009a..472a9d8 100644 --- a/config/setup.js +++ b/config/setup.js @@ -76,10 +76,16 @@ async function setup() { await migrateColumn('users', 'invite_token', 'invite_token VARCHAR(100) NULL'); await migrateColumn('users', 'invite_token_expires', 'invite_token_expires TIMESTAMP 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', '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 { await conn.query("ALTER TABLE users MODIFY COLUMN password VARCHAR(255) NULL"); 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'); } 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 await conn.query(` CREATE TABLE IF NOT EXISTS ticket_visitas ( diff --git a/models/Ticket.js b/models/Ticket.js index d2f1336..8af868b 100644 --- a/models/Ticket.js +++ b/models/Ticket.js @@ -48,13 +48,33 @@ const Ticket = { `, [id]); 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( - 'INSERT INTO tickets (company_id, user_id, department_id, asunto, mensaje, prioridad) VALUES (?, ?, ?, ?, ?, ?)', - [companyId, userId, departmentId, asunto, mensaje, prioridad] + `INSERT INTO tickets + (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; }, + 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) => { const fields = []; const values = []; diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..7c82288 --- /dev/null +++ b/routes/api.js @@ -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 ' + }); + } + + // 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; diff --git a/scripts/sigma_robot_worker.js b/scripts/sigma_robot_worker.js new file mode 100755 index 0000000..d7100b5 --- /dev/null +++ b/scripts/sigma_robot_worker.js @@ -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 };