From 0dbab52aa1c3af11f0fe17679ae3290a89c7f298 Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Mon, 30 Mar 2026 21:52:13 -0300 Subject: [PATCH] =?UTF-8?q?diagnostico:=20adicionada=20fun=C3=A7=C3=A3o=20?= =?UTF-8?q?(TESTING)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rodrigo Bertoldo de Sousa Martins --- src/bot/handler.js | 360 +++++++++++++++++++++++++++++++++++++++++++ src/nlp/processor.js | 19 ++- src/ssh/manager.js | 52 +++++++ 3 files changed, 430 insertions(+), 1 deletion(-) diff --git a/src/bot/handler.js b/src/bot/handler.js index 27097d4..66c372a 100644 --- a/src/bot/handler.js +++ b/src/bot/handler.js @@ -226,6 +226,11 @@ class BotHandler { await this.handleListarServicos(chatId, target); break; + case 'diagnosticar': + console.log(`🩺 Executando handleDiagnosticarCliente com target: ${target}`); + await this.handleDiagnosticarCliente(chatId, target); + break; + case 'dados_cliente': case 'dados_cadastrais': console.log(`📋 Executando handleDadosClientePorBusca com target: ${target}`); @@ -871,6 +876,355 @@ Digite /help para ver todos os comandos disponíveis! } } + /** + * Diagnóstico completo de um cliente: TopSapp + Juniper PPPoE + ONU + */ + async handleDiagnosticarCliente(chatId, query) { + if (!query) { + await this.bot.sendMessage(chatId, + `🩺 *Diagnóstico de Cliente*\n\n` + + `💡 *Como usar:*\n` + + `• "diagnosticar João da Silva"\n` + + `• "diagnosticar 123.456.789-10"\n\n` + + `O diagnóstico verifica: status no TopSapp, sessão PPPoE no concentrador e status da ONU.`, + { parse_mode: 'Markdown' } + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + let location = ''; + let searchQuery = query; + + if (/guarai|colmeia/i.test(query)) { + location = 'Guarai/Colmeia'; + searchQuery = query.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (/pedro afonso/i.test(query)) { + location = 'Pedro Afonso'; + searchQuery = query.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(searchQuery, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${searchQuery}"`); + return; + } + + const instancia = resultado.instancia || location; + + if (resultado.clientes.length === 1) { + await this._mostrarServicosParaDiagnostico(chatId, resultado.clientes[0].id, instancia); + return; + } + + // Múltiplos clientes → exibir teclado de seleção + let message = `🔍 *${resultado.clientes.length} clientes encontrados*\n\n`; + const keyboard = []; + + resultado.clientes.slice(0, 8).forEach((cliente, index) => { + message += `*${index + 1}.* ${cliente.nome}`; + if (cliente.cpf) message += ` — CPF: \`${cliente.cpf}\``; + message += `\n`; + + keyboard.push([{ + text: `🩺 Diagnosticar: ${cliente.nome.substring(0, 30)}`, + callback_data: `diag_cliente_${cliente.id}_${instancia}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + + } catch (error) { + console.error('❌ Erro em handleDiagnosticarCliente:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } + } + + async _mostrarServicosParaDiagnostico(chatId, idCliente, location) { + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + + const servicos = (servicosResult.servicos || []).filter(s => { + const st = (s.id_status || s.statusNumerico); + return st === 1 || st === '1' || String(s.status).toLowerCase() === 'liberado' || String(s.status).toLowerCase() === 'ativo'; + }); + + if (servicos.length === 0) { + await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos para diagnosticar.`); + return; + } + + if (servicos.length === 1) { + await this.runDiagnosticoServico(chatId, idCliente, servicos[0].id_servico || servicos[0].id, location, servicos[0]); + return; + } + + let message = `🩺 *Selecione o serviço para diagnosticar:*\n\n`; + const keyboard = []; + + servicos.forEach((s, i) => { + const nome = s.descricao || s.nome || `Serviço ${i + 1}`; + const login = s.usuario || s.pppoe_login || s.login || ''; + message += `*${i + 1}.* ${nome}${login ? ` — \`${login}\`` : ''}\n`; + keyboard.push([{ + text: `🔍 ${nome.substring(0, 35)}`, + callback_data: `diag_servico_${idCliente}_${s.id_servico || s.id}_${location}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + async handleDiagClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('diag_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this._mostrarServicosParaDiagnostico(chatId, idCliente, location); + } + + async handleDiagServicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('diag_servico_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.runDiagnosticoServico(chatId, idCliente, idServico, location, null); + } + + async runDiagnosticoServico(chatId, idCliente, idServico, location, servicoPreCarregado) { + try { + await this.bot.sendMessage(chatId, '⏳ Executando diagnóstico completo...'); + + // Buscar serviços caso não tenhamos o objeto do serviço + let servico = servicoPreCarregado; + if (!servico) { + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + const idNum = parseInt(idServico, 10); + servico = (servicosResult.servicos || []).find(s => + (s.id_servico || s.id) === idNum || String(s.id_servico || s.id) === String(idServico) + ); + } + + if (!servico) { + await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); + return; + } + + const pppoeLogin = servico.usuario || servico.pppoe_login || servico.login || null; + const serial = servico.serial || servico.comodato_serial || servico.mac || null; + const statusServico = this.topsapp.getStatusServicoDetalhado + ? this.topsapp.getStatusServicoDetalhado(servico.id_status) + : (servico.status || 'N/A'); + const nomePlano = servico.descricao || servico.nome || 'N/A'; + const endereco = servico.instalacao_endereco || servico.end_rua || null; + const cidade = servico.instalacao_cidade || null; + + // Executar consultas em paralelo: Juniper + ONU + const [juniperResult, onuResult] = await Promise.all([ + pppoeLogin + ? this.ssh.getJuniperSubscriberInfo('juniper', pppoeLogin).catch(e => ({ success: false, error: e.message })) + : Promise.resolve({ success: false, skipped: true }), + serial + ? this._diagnosticarONU(serial).catch(e => ({ success: false, error: e.message })) + : Promise.resolve({ success: false, skipped: true }) + ]); + + // Montar relatório + let msg = `🩺 *Diagnóstico do Cliente*\n`; + msg += `━━━━━━━━━━━━━━━━━━━━━\n\n`; + + // Bloco 1: TopSapp + msg += `📋 *Serviço TopSapp*\n`; + msg += ` 📦 Plano: ${nomePlano}\n`; + msg += ` 🔌 Status: ${statusServico}\n`; + if (pppoeLogin) msg += ` 👤 Login PPPoE: \`${pppoeLogin}\`\n`; + if (endereco) msg += ` 📍 Endereço: ${endereco}${cidade ? `, ${cidade}` : ''}\n`; + msg += '\n'; + + // Bloco 2: Concentrador Juniper + msg += `🌐 *Concentrador Juniper*\n`; + if (juniperResult.skipped) { + msg += ` ⚫ Login PPPoE não informado no cadastro\n`; + } else if (!juniperResult.success) { + msg += ` ⚠️ Erro ao consultar: ${juniperResult.error || 'falha na consulta'}\n`; + } else if (juniperResult.connected) { + msg += ` 🟢 *Conectado*\n`; + msg += ` 🌐 IP: \`${juniperResult.ip || 'N/A'}\`\n`; + msg += ` 🔌 Interface: \`${juniperResult.interface || 'N/A'}\`\n`; + } else { + msg += ` 🔴 *Desconectado* (sem sessão PPPoE ativa)\n`; + } + msg += '\n'; + + // Bloco 3: ONU + msg += `📡 *ONU / ONT*\n`; + if (onuResult.skipped) { + msg += ` ⚫ Serial não informado no cadastro\n`; + } else if (!onuResult.success) { + msg += ` ❌ ${onuResult.error || 'ONU não encontrada'}\n`; + if (serial) msg += ` 📋 Serial buscado: \`${serial}\`\n`; + } else { + const rxLabel = onuResult.rxPower || 'N/A'; + const rxNum = parseFloat(rxLabel); + const rxEmoji = isNaN(rxNum) ? '⚫' : rxNum >= -27 ? '🟢' : rxNum >= -30 ? '🟡' : '🔴'; + + msg += ` 📋 Serial: \`${serial}\`\n`; + msg += ` 🏢 OLT: ${onuResult.oltName || onuResult.oltType || 'N/A'}\n`; + if (onuResult.slot || onuResult.pon) { + msg += ` 📍 Slot/PON: ${onuResult.slot || '?'}/${onuResult.pon || '?'}\n`; + } + msg += ` 🔌 Estado: ${onuResult.online ? '🟢 Online' : '🔴 Offline'}\n`; + msg += ` ${rxEmoji} Rx Power: *${rxLabel}*`; + if (onuResult.type === 'fiberhome') msg += ` _(compensado)_`; + msg += '\n'; + + if (onuResult.portaEthernet) { + const ethOk = /forward|up|1gbit|100mbit/i.test(onuResult.portaEthernet); + msg += ` ${ethOk ? '🟢' : '🔴'} Porta Ethernet: ${onuResult.portaEthernet}\n`; + } + } + + await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); + + } catch (error) { + console.error('❌ Erro em runDiagnosticoServico:', error); + await this.bot.sendMessage(chatId, `❌ Erro no diagnóstico: ${error.message}`); + } + } + + /** + * Diagnóstico de ONU: detecta tipo (Datacom/FiberHome) e retorna dados estruturados + */ + async _diagnosticarONU(serial) { + const serialUpper = serial.toUpperCase(); + + if (serialUpper.startsWith('DACM')) { + return await this._diagnosticarONUDatacom(serial); + } else { + return await this._diagnosticarONUFiberhome(serial); + } + } + + async _diagnosticarONUDatacom(serial) { + const olts = this.ssh.listOLTs(); + if (olts.length === 0) { + return { success: false, error: 'Nenhuma OLT Datacom configurada' }; + } + + // Buscar ONU em todas as OLTs em paralelo + const searches = olts.map(olt => + this.ssh.findOnuBySerial(olt.id, serial) + .then(r => ({ ...r, oltId: olt.id, oltName: olt.name })) + .catch(() => ({ success: false })) + ); + const results = await Promise.allSettled(searches); + + let found = null; + for (const r of results) { + if (r.status === 'fulfilled' && r.value.success) { + found = r.value; + break; + } + } + + if (!found) { + return { success: false, error: `ONU ${serial} não encontrada em nenhuma OLT Datacom` }; + } + + const { oltId, oltName, onuInfo } = found; + const { slot, onuId } = onuInfo; + + // Buscar sinal e porta ethernet em paralelo + const [signalResult, ethResult] = await Promise.all([ + this.ssh.checkOnuSignal(oltId, slot, onuId).catch(e => ({ success: false, error: e.message })), + this.ssh.checkOnuEthernetDatacom(oltId, slot, onuId).catch(e => ({ success: false })) + ]); + + // Parsear sinal + const rawSignal = signalResult.output || ''; + const stateMatch = rawSignal.match(/Operational\s+state\s*:\s*(\S+)/i); + const rxMatch = rawSignal.match(/Rx\s+Optical\s+Power\s*\[dBm\]\s*:\s*([-\d.]+)/i); + + const operationalState = stateMatch ? stateMatch[1] : null; + const rxPower = rxMatch ? `${rxMatch[1]} dBm` : null; + const online = operationalState ? /up|active|online/i.test(operationalState) : false; + + // Parsear ethernet + let portaEthernet = null; + if (ethResult.success) { + const ethRaw = ethResult.rawOutput || ''; + const ethMatch = ethRaw.match(/\b(Forwarding|Down|Up|Blocking)\b[^\n]*([\d]+(?:Gbit|Mbit)[^\n]*|N\/A)?/i); + if (ethMatch) { + portaEthernet = ethMatch[0].trim().replace(/\s+/g, ' '); + } + } + + return { + success: true, + type: 'datacom', + oltName, + slot, + pon: slot ? slot.split('/')[2] || slot : null, + onuId, + online, + rxPower, + operationalState, + portaEthernet + }; + } + + async _diagnosticarONUFiberhome(serial) { + const resultado = await this.fiberhome.obterStatusONU(serial); + + if (!resultado.success) { + return { success: false, error: resultado.error || 'ONU não encontrada na FiberHome' }; + } + + const portaPrincipal = resultado.portasEthernet && resultado.portasEthernet.length > 0 + ? resultado.portasEthernet[0] + : null; + + return { + success: true, + type: 'fiberhome', + oltName: 'FiberHome', + slot: resultado.slot, + pon: resultado.pon, + onuId: resultado.onuId, + online: resultado.online, + rxPower: resultado.rxPower, + txPower: resultado.txPower, + portaEthernet: portaPrincipal ? `${portaPrincipal.status}` : null + }; + } + /** * Verificar sinal de ONU */ @@ -1539,6 +1893,12 @@ Digite /help para ver todos os comandos disponíveis! const serialNumber = data.replace('fiberhome_details_', ''); await this.bot.answerCallbackQuery(callbackQuery.id); await this.handleStatusONU(chatId, serialNumber); + } else if (data.startsWith('diag_cliente_')) { + console.log(`🔍 Condição diag_cliente_ atendida`); + await this.handleDiagClienteCallback(callbackQuery, data); + } else if (data.startsWith('diag_servico_')) { + console.log(`🔍 Condição diag_servico_ atendida`); + await this.handleDiagServicoCallback(callbackQuery, data); } else if (data.startsWith('dados_cliente_')) { console.log(`🔍 Condição dados_cliente_ atendida`); await this.handleDadosClienteCallback(callbackQuery, data); diff --git a/src/nlp/processor.js b/src/nlp/processor.js index 8b6e56f..205d1b8 100644 --- a/src/nlp/processor.js +++ b/src/nlp/processor.js @@ -32,7 +32,8 @@ class NLPProcessor { 'dados_cliente': ['dados cliente', 'dados cadastrais', 'cadastro cliente', 'informações cliente', 'info cliente', 'perfil cliente', 'ficha cliente', 'registro cliente', 'ver dados'], 'listar_servicos': ['listar serviços', 'servicos cliente', 'planos cliente', 'contratos cliente', 'ver servicos', 'plano cliente', 'assinaturas', 'planos', 'serviços ativos', 'ver plano'], 'abrir_suporte': ['abrir suporte', 'criar suporte', 'adicionar suporte', 'novo suporte', 'suporte', 'chamado', 'ticket', 'atendimento', 'suporte tecnico', 'atendimento tecnico', 'abrir chamado', 'registrar chamado', 'criar chamado', 'visita tecnica', 'agendar visita', 'reclamação', 'reclamacao'], - 'consultar_usuario': ['consultar usuario', 'consultar user', 'ver usuario', 'status usuario', 'status user', 'info usuario', 'info user', 'checar usuario', 'verificar usuario', 'pesquisar usuario', 'localizar usuario', 'buscar usuario', 'usuario conectado', 'user conectado', 'usuario online', 'user online', 'usuario ativo', 'user ativo'] + 'consultar_usuario': ['consultar usuario', 'consultar user', 'ver usuario', 'status usuario', 'status user', 'info usuario', 'info user', 'checar usuario', 'verificar usuario', 'pesquisar usuario', 'localizar usuario', 'buscar usuario', 'usuario conectado', 'user conectado', 'usuario online', 'user online', 'usuario ativo', 'user ativo'], + 'diagnosticar': ['diagnosticar', 'diagnostico', 'diagnose', 'diagnosticar cliente', 'diagnostico cliente', 'checar cliente completo', 'verificar cliente completo', 'relatorio cliente', 'relatorio completo', 'status completo cliente', 'analisar cliente', 'inspecionar cliente', 'situacao do cliente', 'situação do cliente'] }; // Padrões de comandos @@ -81,6 +82,8 @@ class NLPProcessor { // Padrões para consultar usuário específico /^(consultar|ver|status|info|checar|verificar|pesquisar|localizar|buscar)\s+(usuario|user)\s+(.+)$/i, /^(usuario|user)\s+(.+)$/i, + // Padrões para diagnosticar cliente + /^(diagnosticar|diagnostico|diagnose|analisar|inspecionar)\s+(cliente\s+)?(.+)$/i, // Padrões para listar usuários /^(listar|mostrar)\s+(usuários|usuarios|clientes|users)(\s+conectados)?$/i, // Padrões gerais (devem vir por último) @@ -255,6 +258,20 @@ class NLPProcessor { }; } + // Detectar diagnóstico de cliente + if (/^diagnosticar$|^diagnostico$|^diagnose$|^analisar$|^inspecionar$/i.test(action)) { + // match[2] pode ser "cliente " ou já o nome, match[3] é o nome quando match[2]="cliente " + let query = match[3] || match[2]; + // remover prefixo "cliente" se vier junto + if (query) query = query.replace(/^cliente\s+/i, '').trim(); + return { + action: 'diagnosticar', + target: query || null, + confidence: 0.95, + type: 'diagnosticar_pattern' + }; + } + // Detectar comandos de suporte if (/abrir|criar|adicionar|novo|suporte|chamado|ticket|atendimento/i.test(action) && /suporte|chamado|ticket|atendimento/i.test(fullMatch)) { diff --git a/src/ssh/manager.js b/src/ssh/manager.js index 24c30a5..87f8eaa 100644 --- a/src/ssh/manager.js +++ b/src/ssh/manager.js @@ -468,6 +468,58 @@ class SSHManager { } } + /** + * Verifica o status da porta Ethernet de uma ONU Datacom pelo slot e ID + * @param {string} equipmentId - ID do equipamento OLT + * @param {string} slot - Slot da ONU (ex: 1/1/1) + * @param {string} onuId - ID da ONU + * @returns {Promise} - { success, portStatus, speed, rawOutput } + */ + async checkOnuEthernetDatacom(equipmentId, slot, onuId) { + try { + const connection = await this.connect(equipmentId); + + if (!connection.success) { + throw new Error(`Falha na conexão: ${connection.message}`); + } + + const command = `show interface gpon ${slot} onu ${onuId} ethernet`; + const result = await this.executeCommand(connection.connectionId, command); + + this.disconnect(connection.connectionId); + + if (!result.success) { + return { success: false, error: result.error, rawOutput: result.output }; + } + + // Extrai status e velocidade da porta ethernet + // Linha típica: " 1 Forwarding 1Gbit Full-Duplex" + // ou: " 1 Down N/A" + const portStatus = null; + const speed = null; + const output = result.output || ''; + + // Procurar por linha de porta ethernet + const lineMatch = output.match(/\b(Forwarding|Down|Up|Blocking)\b.*?([\d]+(?:Gbit|Mbit)[^\n]*|N\/A)/i); + const statusVal = lineMatch ? lineMatch[1] : null; + const speedVal = lineMatch ? lineMatch[2]?.trim() : null; + + return { + success: true, + portStatus: statusVal, + speed: speedVal, + rawOutput: output + }; + + } catch (error) { + return { + success: false, + error: error.message, + rawOutput: null + }; + } + } + /** * Busca informações de uma ONU pelo serial number na OLT * @param {string} equipmentId - ID do equipamento OLT