|
@@ -0,0 +1,349 @@
|
|
|
|
|
+import { config } from "../config/index.js";
|
|
|
|
|
+import { chatCompletion } from "./ollamaClient.js";
|
|
|
|
|
+import {
|
|
|
|
|
+ buscarDispositivo,
|
|
|
|
|
+ refPorPppoe,
|
|
|
|
|
+ refPorSerial,
|
|
|
|
|
+ statusConexao,
|
|
|
|
|
+ minutosDesde,
|
|
|
|
|
+ classificarSinalRx
|
|
|
|
|
+} from "./acsService.js";
|
|
|
|
|
+import { buscarIdClientePorPppoe } from "./ixcIntegradorService.js";
|
|
|
|
|
+import { formatarTrechoAtendimento } from "./atendimentosQueryService.js";
|
|
|
|
|
+import { Atendimento } from "../models/Atendimento.model.js";
|
|
|
|
|
+import { AtendimentoCliente } from "../models/AtendimentoCliente.model.js";
|
|
|
|
|
+import { mensagemErro } from "../chat/acsCommand.js";
|
|
|
|
|
+
|
|
|
|
|
+// Modo "equipamento" do chat: pergunta em linguagem natural sobre a CPE de UM cliente.
|
|
|
|
|
+// O ACS só tem o estado ATUAL, então é consultado na hora (nunca indexado no Qdrant).
|
|
|
|
|
+// Só leitura: este modo nunca dispara diagnóstico na CPE.
|
|
|
|
|
+
|
|
|
|
|
+// mesmo padrão de SERIAL_RE em chatRouter.js
|
|
|
|
|
+const SERIAL_RE = /\bserial\s*:?\s*([A-Z0-9]{6,})\b/i;
|
|
|
|
|
+const ATENDIMENTOS_LIMIT = 3;
|
|
|
|
|
+const MENSAGENS_POR_ATENDIMENTO = 40;
|
|
|
|
|
+// lado dos atendimentos é complemento (3-4 chamadas ao Alfred): se demorar, responde só com o ACS
|
|
|
|
|
+const ATENDIMENTOS_TIMEOUT_MS = 10_000;
|
|
|
|
|
+const CLIENTES_WIFI_LIMIT = 15;
|
|
|
|
|
+
|
|
|
|
|
+const STATUS_TEXTO = {
|
|
|
|
|
+ online: "online",
|
|
|
|
|
+ atrasado: "inform atrasado (sem contato com o ACS há mais de 10 min; o PPPoE do cliente pode estar conectado mesmo assim)",
|
|
|
|
|
+ offline: "offline no ACS (sem contato com o ACS há mais de 60 min; o PPPoE do cliente pode estar conectado mesmo assim)",
|
|
|
|
|
+ nunca_informou: "nunca informou ao ACS"
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const MOTIVO_SEM_ATENDIMENTOS = {
|
|
|
|
|
+ login_nao_encontrado: "o login PPPoE não foi encontrado no ERP",
|
|
|
|
|
+ sem_cliente: "o login PPPoE não tem cliente vinculado no ERP",
|
|
|
|
|
+ cliente_ambiguo: "há mais de um cliente com o mesmo nome no ERP, então não dá pra saber qual é sem confundir",
|
|
|
|
|
+ cliente_nao_encontrado: "o cliente do login não foi encontrado no ERP",
|
|
|
|
|
+ timeout: "a consulta ao ERP demorou demais",
|
|
|
|
|
+ erro: "a consulta ao ERP falhou"
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const EXTRACAO_SYSTEM_PROMPT = [
|
|
|
|
|
+ "Extraia de uma pergunta de suporte o identificador do equipamento (roteador/ONU) do cliente.",
|
|
|
|
|
+ 'Responda APENAS com JSON no formato {"pppoe": string | null, "serial": string | null}.',
|
|
|
|
|
+ "pppoe: o login PPPoE do cliente, exatamente como escrito (ex: 'joao.silva', 'ivandasilva', 'maria123').",
|
|
|
|
|
+ "serial: o número de série do equipamento, quando a pergunta disser explicitamente que é um serial.",
|
|
|
|
|
+ "Se a pergunta for continuação (ex: 'e o wifi dele?'), use o HISTÓRICO DA CONVERSA para achar o identificador.",
|
|
|
|
|
+ "O login costuma vir logo depois de 'do', 'da', 'de', 'cliente' ou 'pppoe' (ex: 'wifi do mariasouza', 'roteador da ana.paula'),",
|
|
|
|
|
+ "é uma palavra só, sem espaço, e normalmente não é uma palavra comum do português.",
|
|
|
|
|
+ "Copie o identificador letra por letra, sem corrigir nem completar. Se não houver identificador, use null.",
|
|
|
|
|
+ "Não inclua explicações nem markdown fora do JSON."
|
|
|
|
|
+].join("\n");
|
|
|
|
|
+
|
|
|
|
|
+// few-shot: sem eles o modelo só reconhecia o login quando a frase dizia "pppoe" ou "cliente"
|
|
|
|
|
+const EXTRACAO_EXEMPLOS = [
|
|
|
|
|
+ { role: "user", content: "quantos aparelhos estão no wifi do mariasouza?" },
|
|
|
|
|
+ { role: "assistant", content: '{"pppoe": "mariasouza", "serial": null}' },
|
|
|
|
|
+ { role: "user", content: "o roteador da ana.paula está online e em qual canal?" },
|
|
|
|
|
+ { role: "assistant", content: '{"pppoe": "ana.paula", "serial": null}' },
|
|
|
|
|
+ { role: "user", content: "qual o sinal da onu do cliente?" },
|
|
|
|
|
+ { role: "assistant", content: '{"pppoe": null, "serial": null}' }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+function hit(id, source, text, extra = {}) {
|
|
|
|
|
+ return { id, source, text, chunkIndex: 0, metadata: { type: id, ...extra }, score: null };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function respostaDireta(id, text) {
|
|
|
|
|
+ return [hit(id, null, text, { skipLlm: true })];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function textoHistorico(history) {
|
|
|
|
|
+ return history.map((m) => `${m.Role}: ${m.Content}`).join("\n");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// o LLM pode "completar" ou inventar um login: só vale se aparecer literalmente na conversa
|
|
|
|
|
+function apareceNoTexto(alvo, ...textos) {
|
|
|
|
|
+ const a = String(alvo ?? "").trim().toLowerCase();
|
|
|
|
|
+ if (!a) return false;
|
|
|
|
|
+ return textos.some((t) => String(t ?? "").toLowerCase().includes(a));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// o modelo às vezes devolve o substantivo da frase ("do cliente") como se fosse o login
|
|
|
|
|
+const NAO_E_LOGIN = new Set([
|
|
|
|
|
+ "cliente", "roteador", "onu", "ont", "cpe", "wifi", "wi-fi", "internet", "equipamento", "modem",
|
|
|
|
|
+ "sinal", "rede", "fibra", "pppoe", "login", "serial", "ele", "ela", "dele", "dela",
|
|
|
|
|
+ "casa", "hoje", "ontem", "agora", "novo", "nova", "mesmo", "mesma", "cada", "todos", "todas", "usuario", "usuário"
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+// rede de segurança pra frases curtas ("onu do gilsonjardim") em que o modelo devolve null:
|
|
|
|
|
+// última palavra depois de do/da/de/cliente/pppoe. Um falso positivo só vira "não encontrei
|
|
|
|
|
+// CPE com X" — o próprio ACS valida, nada é inventado.
|
|
|
|
|
+const CANDIDATO_LOGIN_RE = /\b(?:do|da|de|cliente|pppoe|login)\s+([a-z0-9][a-z0-9._-]{3,})/gi;
|
|
|
|
|
+
|
|
|
|
|
+function candidatoPorRegex(message) {
|
|
|
|
|
+ const candidatos = [...String(message ?? "").matchAll(CANDIDATO_LOGIN_RE)]
|
|
|
|
|
+ .map((m) => m[1].replace(/[._-]+$/, ""))
|
|
|
|
|
+ .filter((c) => !NAO_E_LOGIN.has(c.toLowerCase()));
|
|
|
|
|
+ return candidatos.length ? { tipo: "pppoe", valor: candidatos[candidatos.length - 1] } : null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function extrairDaMensagem(message, history, signal) {
|
|
|
|
|
+ const serialDireto = SERIAL_RE.exec(message);
|
|
|
|
|
+ if (serialDireto) return { tipo: "serial", valor: serialDireto[1] };
|
|
|
|
|
+
|
|
|
|
|
+ const historico = textoHistorico(history);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { content } = await chatCompletion({
|
|
|
|
|
+ messages: [
|
|
|
|
|
+ { role: "system", content: EXTRACAO_SYSTEM_PROMPT },
|
|
|
|
|
+ ...EXTRACAO_EXEMPLOS,
|
|
|
|
|
+ ...(history.length ? [{ role: "system", content: `HISTÓRICO DA CONVERSA:\n${historico}` }] : []),
|
|
|
|
|
+ { role: "user", content: message }
|
|
|
|
|
+ ],
|
|
|
|
|
+ format: "json",
|
|
|
|
|
+ options: { temperature: 0, num_ctx: config.llm.numCtx, num_predict: config.llm.auxNumPredict },
|
|
|
|
|
+ signal
|
|
|
|
|
+ });
|
|
|
|
|
+ const parsed = JSON.parse(String(content ?? "").trim());
|
|
|
|
|
+ const serial = typeof parsed?.serial === "string" ? parsed.serial.trim() : "";
|
|
|
|
|
+ const pppoe = typeof parsed?.pppoe === "string" ? parsed.pppoe.trim().replace(/^pppoe:/i, "") : "";
|
|
|
|
|
+ if (serial && apareceNoTexto(serial, message, historico)) return { tipo: "serial", valor: serial };
|
|
|
|
|
+ if (pppoe && !NAO_E_LOGIN.has(pppoe.toLowerCase()) && apareceNoTexto(pppoe, message, historico)) {
|
|
|
|
|
+ return { tipo: "pppoe", valor: pppoe };
|
|
|
|
|
+ }
|
|
|
|
|
+ return candidatoPorRegex(message);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (signal?.aborted) throw err;
|
|
|
|
|
+ console.warn("[equipamentoContextoService] extração do alvo falhou:", err.message);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const PERGUNTAS_ANTERIORES_MAX = 2;
|
|
|
|
|
+
|
|
|
|
|
+export async function extrairAlvoEquipamento(message, history = [], signal) {
|
|
|
|
|
+ // sem o histórico no prompt: com ele o modelo não resolvia o pronome e podia devolver
|
|
|
|
|
+ // qualquer login citado antes na conversa (passa no apareceNoTexto por estar no histórico)
|
|
|
|
|
+ const alvo = await extrairDaMensagem(message, [], signal);
|
|
|
|
|
+ if (alvo) return alvo;
|
|
|
|
|
+ // continuação ("e o wifi dele?"): herda o alvo da pergunta anterior do usuário, extraída sozinha
|
|
|
|
|
+ const anteriores = history
|
|
|
|
|
+ .filter((m) => String(m.Role).toLowerCase() === "user")
|
|
|
|
|
+ .slice(-PERGUNTAS_ANTERIORES_MAX)
|
|
|
|
|
+ .reverse();
|
|
|
|
|
+ for (const m of anteriores) {
|
|
|
|
|
+ const herdado = await extrairDaMensagem(m.Content, [], signal);
|
|
|
|
|
+ if (herdado) return herdado;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function numero(v, casas = 2) {
|
|
|
|
|
+ return typeof v === "number" && Number.isFinite(v) ? v.toFixed(casas) : null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function textoMinutos(min) {
|
|
|
|
|
+ if (min === null) return "desconhecido";
|
|
|
|
|
+ if (min < 1) return "menos de 1 min";
|
|
|
|
|
+ if (min < 60) return `${Math.round(min)} min`;
|
|
|
|
|
+ if (min < 1440) return `${Math.round(min / 60)} h`;
|
|
|
|
|
+ return `${Math.round(min / 1440)} dias`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function textoUptime(segundos) {
|
|
|
|
|
+ if (typeof segundos !== "number" || !Number.isFinite(segundos)) return null;
|
|
|
|
|
+ const d = Math.floor(segundos / 86400);
|
|
|
|
|
+ const h = Math.floor((segundos % 86400) / 3600);
|
|
|
|
|
+ const m = Math.floor((segundos % 3600) / 60);
|
|
|
|
|
+ return d ? `${d} dias e ${h} h` : h ? `${h} h e ${m} min` : `${m} min`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function blocoOptico(optico) {
|
|
|
|
|
+ if (!optico || optico.sinalRxDbm == null) return ["Sinal óptico: o equipamento não reporta sinal óptico ao ACS (roteador sem fibra ou modelo sem esse dado)."];
|
|
|
|
|
+ const classe = classificarSinalRx(optico.sinalRxDbm);
|
|
|
|
|
+ const linhas = [
|
|
|
|
|
+ `Sinal óptico RX (recepção da ONU): ${numero(optico.sinalRxDbm)} dBm — classe: ${classe ?? "?"} ` +
|
|
|
|
|
+ "(faixas: ok de -8 a -25 dBm; atenção de -25 a -27 dBm; crítico abaixo de -27 dBm; saturado acima de -8 dBm)"
|
|
|
|
|
+ ];
|
|
|
|
|
+ if (optico.sinalTxDbm != null) linhas.push(`Sinal óptico TX (transmissão): ${numero(optico.sinalTxDbm)} dBm`);
|
|
|
|
|
+ if (optico.temperaturaC != null) linhas.push(`Temperatura do módulo óptico: ${numero(optico.temperaturaC, 1)} °C`);
|
|
|
|
|
+ return linhas;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function blocoWifi(d) {
|
|
|
|
|
+ const radios = Array.isArray(d.radios) ? d.radios : [];
|
|
|
|
|
+ const linhas = radios.length
|
|
|
|
|
+ ? radios.map(
|
|
|
|
|
+ (r) =>
|
|
|
|
|
+ // canal 0 = o rádio está em seleção automática (alguns modelos não informam o canal em uso)
|
|
|
|
|
+ `Rádio Wi-Fi ${r.banda ?? "(banda não informada)"}: SSID "${r.ssid ?? "?"}", canal ${r.canal || "automático (canal em uso não informado)"}, ` +
|
|
|
|
|
+ `${r.habilitado === false ? "DESLIGADO" : "ligado"}`
|
|
|
|
|
+ )
|
|
|
|
|
+ : ["Rádios Wi-Fi: nenhum reportado."];
|
|
|
|
|
+
|
|
|
|
|
+ const clientes = Array.isArray(d.clientes_wifi) ? d.clientes_wifi : [];
|
|
|
|
|
+ const conectados = Array.isArray(d.clientes_conectados) ? d.clientes_conectados.length : null;
|
|
|
|
|
+ linhas.push(
|
|
|
|
|
+ `Aparelhos conectados no Wi-Fi agora: ${clientes.length}` +
|
|
|
|
|
+ (conectados !== null ? ` (total na rede local, cabo + Wi-Fi: ${conectados})` : "")
|
|
|
|
|
+ );
|
|
|
|
|
+ // pior sinal primeiro: são os que explicam reclamação de Wi-Fi ruim
|
|
|
|
|
+ const ordenados = [...clientes].sort((a, b) => (a.sinal ?? 0) - (b.sinal ?? 0));
|
|
|
|
|
+ ordenados.slice(0, CLIENTES_WIFI_LIMIT).forEach((c) => {
|
|
|
|
|
+ linhas.push(`- aparelho ${c.mac ?? "?"} na banda ${c.banda ?? "?"}, sinal ${c.sinal != null ? `${c.sinal} dBm` : "?"}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ if (clientes.length > CLIENTES_WIFI_LIMIT) linhas.push(`- ...e mais ${clientes.length - CLIENTES_WIFI_LIMIT} aparelhos`);
|
|
|
|
|
+ return linhas;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatarCpe(d, alvo, consultadoEm) {
|
|
|
|
|
+ const info = d.device_id ?? {};
|
|
|
|
|
+ const status = statusConexao(d.ultimo_inform);
|
|
|
|
|
+ const linhas = [
|
|
|
|
|
+ `ESTADO ATUAL DA CPE NO ACS — consultado em ${consultadoEm.toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo" })}`,
|
|
|
|
|
+ `PPPoE: ${d.pppoe_usuario ?? alvo}`,
|
|
|
|
|
+ `Equipamento: ${info._ProductClass ?? "modelo desconhecido"}${info._Manufacturer ? ` (${info._Manufacturer})` : ""}, serial ${info._SerialNumber ?? "?"}, firmware ${d.versao_software ?? "?"}`,
|
|
|
|
|
+ `Status: ${STATUS_TEXTO[status]} — último contato com o ACS há ${textoMinutos(minutosDesde(d.ultimo_inform))}`
|
|
|
|
|
+ ];
|
|
|
|
|
+ // no topo (e não no fim) — no fim do bloco o LLM ignorava o aviso e respondia como se estivesse online
|
|
|
|
|
+ if (status !== "online") {
|
|
|
|
|
+ linhas.push("ATENÇÃO: a CPE NÃO está informando ao ACS agora — todos os dados abaixo são do último contato, não do momento atual.");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (d.ip) linhas.push(`IP WAN: ${d.ip}`);
|
|
|
|
|
+ const uptime = textoUptime(d.uptime_segundos);
|
|
|
|
|
+ if (uptime) linhas.push(`Ligado há: ${uptime} (sem reiniciar)`);
|
|
|
|
|
+ linhas.push(...blocoOptico(d.optico));
|
|
|
|
|
+ if (d.saude?.cpuPercent != null || d.saude?.memUsadaPercent != null) {
|
|
|
|
|
+ linhas.push(`Uso de CPU: ${numero(d.saude.cpuPercent, 0) ?? "?"}% · uso de memória: ${numero(d.saude.memUsadaPercent, 0) ?? "?"}%`);
|
|
|
|
|
+ }
|
|
|
|
|
+ linhas.push(...blocoWifi(d));
|
|
|
|
|
+ return linhas.join("\n");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// CPE fora do ar é a informação mais importante pro suporte e o LLM a omitia mesmo com o
|
|
|
|
|
+// aviso no topo do bloco e regra explícita no prompt — sai fixo, sem LLM, antes da resposta
|
|
|
|
|
+function avisoStatus(d, rotulo) {
|
|
|
|
|
+ const status = statusConexao(d.ultimo_inform);
|
|
|
|
|
+ if (status === "online") return null;
|
|
|
|
|
+ if (status === "nunca_informou") return `⚠️ A CPE de **${rotulo}** nunca informou ao ACS — não há dados atuais dela.`;
|
|
|
|
|
+ // "offline no ACS" = a CPE parou de falar com a gerência TR-069; o PPPoE pode seguir
|
|
|
|
|
+ // conectado (visto em produção: login online no IXC com a CPE 14 h sem Inform)
|
|
|
|
|
+ return (
|
|
|
|
|
+ `⚠️ A CPE de **${rotulo}** não fala com o ACS há ${textoMinutos(minutosDesde(d.ultimo_inform))} ` +
|
|
|
|
|
+ `(${status === "offline" ? "offline" : "inform atrasado"} no ACS) — os dados abaixo são desse último contato. ` +
|
|
|
|
|
+ "Isso não significa necessariamente que o cliente está sem internet: confira a conexão PPPoE no ERP."
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function comTimeout(promise, ms) {
|
|
|
|
|
+ let timer;
|
|
|
|
|
+ const limite = new Promise((resolve) => {
|
|
|
|
|
+ timer = setTimeout(() => resolve({ timeout: true }), ms);
|
|
|
|
|
+ });
|
|
|
|
|
+ return Promise.race([promise, limite]).finally(() => clearTimeout(timer));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function buscarAtendimentosDoPppoe(pppoe) {
|
|
|
|
|
+ const { idCliente, motivo } = await buscarIdClientePorPppoe(pppoe);
|
|
|
|
|
+ if (!idCliente) return { atendimentos: [], motivo };
|
|
|
|
|
+
|
|
|
|
|
+ const clientes = await AtendimentoCliente.query().where("IdClienteIxc", idCliente).select("Id");
|
|
|
|
|
+ if (!clientes.length) return { atendimentos: [], motivo: null };
|
|
|
|
|
+
|
|
|
|
|
+ const atendimentos = await Atendimento.query()
|
|
|
|
|
+ .whereIn("ClienteId", clientes.map((c) => c.Id))
|
|
|
|
|
+ .withGraphFetched("[cliente, mensagens, avaliacao]")
|
|
|
|
|
+ .modifyGraph("mensagens", (q) => q.orderBy("Timestamp", "asc").orderBy("Id", "asc"))
|
|
|
|
|
+ .orderByRaw("COALESCE(Abertura, IngestedAt) DESC")
|
|
|
|
|
+ .limit(ATENDIMENTOS_LIMIT);
|
|
|
|
|
+ return { atendimentos, motivo: null };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function hitsAtendimentos(pppoe) {
|
|
|
|
|
+ if (!pppoe) return [];
|
|
|
|
|
+ let resultado;
|
|
|
|
|
+ try {
|
|
|
|
|
+ resultado = await comTimeout(buscarAtendimentosDoPppoe(pppoe), ATENDIMENTOS_TIMEOUT_MS);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn("[equipamentoContextoService] atendimentos do cliente falharam:", err.message);
|
|
|
|
|
+ resultado = { atendimentos: [], motivo: "erro" };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resultado.timeout) resultado = { atendimentos: [], motivo: "timeout" };
|
|
|
|
|
+
|
|
|
|
|
+ if (resultado.motivo) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ hit(
|
|
|
|
|
+ "equipamento_sem_atendimentos",
|
|
|
|
|
+ null,
|
|
|
|
|
+ `ATENDIMENTOS: não foi possível vincular os atendimentos deste cliente — ${MOTIVO_SEM_ATENDIMENTOS[resultado.motivo]}.`
|
|
|
|
|
+ )
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!resultado.atendimentos.length) {
|
|
|
|
|
+ return [hit("equipamento_sem_atendimentos", null, "ATENDIMENTOS: nenhum atendimento registrado para este cliente.")];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Promise.all(
|
|
|
|
|
+ resultado.atendimentos.map(async (atendimento) =>
|
|
|
|
|
+ hit(
|
|
|
|
|
+ "equipamento_atendimento",
|
|
|
|
|
+ `Atendimento ${atendimento.Codigo}`,
|
|
|
|
|
+ `ATENDIMENTO (histórico do cliente)\n${await formatarTrechoAtendimento(atendimento, {
|
|
|
|
|
+ incluirOs: false,
|
|
|
|
|
+ maxMensagens: MENSAGENS_POR_ATENDIMENTO
|
|
|
|
|
+ })}`
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function resolverContextoEquipamento(message, history = [], { signal } = {}) {
|
|
|
|
|
+ const alvo = await extrairAlvoEquipamento(message, history, signal);
|
|
|
|
|
+ if (!alvo) {
|
|
|
|
|
+ return respostaDireta(
|
|
|
|
|
+ "equipamento_sem_alvo",
|
|
|
|
|
+ "Para consultar o equipamento, preciso do login PPPoE do cliente ou do serial da CPE. " +
|
|
|
|
|
+ "Ex.: \"qual o sinal da ONU do joao.silva?\" ou \"como está o roteador serial XQFQU21806005392?\""
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const ref = alvo.tipo === "serial" ? refPorSerial(alvo.valor) : refPorPppoe(alvo.valor);
|
|
|
|
|
+ const rotulo = alvo.tipo === "serial" ? `serial:${alvo.valor}` : alvo.valor;
|
|
|
|
|
+
|
|
|
|
|
+ // por PPPoE já dá pra buscar os atendimentos em paralelo com o ACS; por serial, o PPPoE vem do ACS
|
|
|
|
|
+ const atendimentosAntecipados = alvo.tipo === "pppoe" ? hitsAtendimentos(alvo.valor) : null;
|
|
|
|
|
+
|
|
|
|
|
+ let dispositivo;
|
|
|
|
|
+ try {
|
|
|
|
|
+ dispositivo = await buscarDispositivo(ref);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ atendimentosAntecipados?.catch(() => {});
|
|
|
|
|
+ return respostaDireta("equipamento_erro_acs", mensagemErro(err, rotulo));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!dispositivo) {
|
|
|
|
|
+ atendimentosAntecipados?.catch(() => {});
|
|
|
|
|
+ return respostaDireta("equipamento_nao_encontrado", mensagemErro({ status: 404 }, rotulo));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pppoe = dispositivo.pppoe_usuario ?? (alvo.tipo === "pppoe" ? alvo.valor : null);
|
|
|
|
|
+ const cpe = hit("equipamento_acs", `ACS — CPE ${pppoe ?? rotulo}`, formatarCpe(dispositivo, rotulo, new Date()), {
|
|
|
|
|
+ avisoResposta: avisoStatus(dispositivo, pppoe ?? rotulo)
|
|
|
|
|
+ });
|
|
|
|
|
+ const atendimentos = await (atendimentosAntecipados ?? hitsAtendimentos(pppoe));
|
|
|
|
|
+ return [cpe, ...atendimentos];
|
|
|
|
|
+}
|