'use strict'; /** * Gera o workflow n8n da V2 a partir dos modulos testados em src/v2/*.js. * * Por que este script existe: os Code nodes do n8n (na instancia remota * devn8n.gtigrupo.com.br) rodam isolados e nao conseguem fazer * `require('./src/v2/...')`. Para nao duplicar/reescrever manualmente a * logica ja testada (e arriscar divergencia entre o que foi testado e o * que roda de verdade no n8n), este script LE os arquivos fonte, remove * `'use strict'`, linhas `require(...)` e o bloco `module.exports`, e * concatena o que cada Code node precisa. * * Uso: * node tools/build_v2_workflow.js * * Saida: * "Resumo Operacional GLPI v2 - IA.n8n.json" (raiz do repo) * * Se voce alterar algo em src/v2/*.js, rode este script de novo e reimporte * o workflow no n8n (ou use n8n_update_full_workflow) para manter os dois * em sincronia. */ const fs = require('fs'); const path = require('path'); const SRC_DIR = path.join(__dirname, '..', 'src', 'v2'); const OUT_FILE = path.join(__dirname, '..', 'Resumo Operacional GLPI v2 - IA.n8n.json'); function readModuleBody(filename) { const raw = fs.readFileSync(path.join(SRC_DIR, filename), 'utf8'); const semExports = raw.split(/\nmodule\.exports\s*=/)[0]; return semExports .replace(/^'use strict';\s*$/m, '') // remove requires de destructuring, inclusive multilinha: const { a, b } = require('./x'); .replace(/const\s*\{[\s\S]*?\}\s*=\s*require\([^)]*\);?/g, '') // remove requires simples: const x = require('./x'); .replace(/^const\s+\w+\s*=\s*require\([^)]*\);?.*$/gm, '') .trim(); } const MOD = { config: readModuleBody('config.js'), period: readModuleBody('period.js'), baseline: readModuleBody('baseline.js'), anomaly: readModuleBody('anomaly.js'), insights: readModuleBody('insights.js'), aiSchema: readModuleBody('aiSchema.js'), format: readModuleBody('format.js'), sqlGuard: readModuleBody('sqlGuard.js'), queries: readModuleBody('queries.js'), sendGuard: readModuleBody('sendGuard.js'), pipeline: readModuleBody('pipeline.js'), }; function joinBlocks(...blocks) { return blocks.join('\n\n// ---\n\n'); } const HEADER_AVISO = '// Gerado por tools/build_v2_workflow.js a partir de src/v2/*.js — NAO editar\n' + '// manualmente aqui; edite a fonte e rode o script de novo.\n'; // --------------------------------------------------------------------- // Node A: Code - Periodo e Queries SQL // --------------------------------------------------------------------- const CODE_PERIODO_QUERIES = `${HEADER_AVISO} ${joinBlocks(MOD.config, MOD.period, MOD.sqlGuard, MOD.queries)} const overrides = $json; const config = loadConfig(overrides); const referencia = new Date(); const period = resolvePeriod(config.data_alvo, referencia, config); period.history_bucket = period.tipo_periodo === 'semanal' ? 'semanal' : 'diario'; const queries = { indicadores: buildIndicatorsQuery(period, config), ranking_categoria: buildRankingCompletoQuery(period, config, 'categoria'), ranking_cliente: buildRankingCompletoQuery(period, config, 'cliente'), contexto_tickets: buildTicketContextQuery(period, config), baseline_categoria: buildBaselineDailyQuery(period.baseline.rolling, config, 'categoria', period), baseline_cliente: buildBaselineDailyQuery(period.baseline.rolling, config, 'cliente', period), }; return [{ json: { period, config, queries } }]; `; // --------------------------------------------------------------------- // Node: Code - Preparar Contexto IA // --------------------------------------------------------------------- const CODE_CONTEXTO_IA = `${HEADER_AVISO} ${joinBlocks(MOD.insights, MOD.aiSchema)} const base = $('Code - Periodo e Queries SQL').first().json; const config = base.config; const period = base.period; const ticketsContexto = $('MySQL - Contexto de Chamados').all().map((i) => i.json); const ticketContext = buildTicketContext(ticketsContexto, config); const schema = buildJsonSchema(); const userPrompt = buildUserPrompt({ resumoDeterministico: $('MySQL - Indicadores do Periodo').first().json, baseline: period.baseline, anomaliasDeterministicas: null, contexto: ticketContext, }); return [{ json: { ticketContext, openai_request_body: { model: config.AI_MODEL, // Sem "temperature": modelos da familia gpt-5.x so aceitam o valor // padrao (1) — mandar um valor customizado (ex: 0.2, usado ate aqui) // e rejeitado por alguns desses modelos. Preferimos nao mandar o // parametro a arriscar outro "Bad request" nao testado. // // gpt-5.x exige max_completion_tokens em vez do legado max_tokens // (validado ao vivo em 2026-08-25: "Unsupported parameter: 'max_tokens' // is not supported with this model. Use 'max_completion_tokens' instead.") max_completion_tokens: config.AI_MAX_COMPLETION_TOKENS, reasoning_effort: config.AI_REASONING_EFFORT, response_format: { type: 'json_schema', json_schema: schema }, messages: [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: userPrompt }, ], }, }, }]; `; // --------------------------------------------------------------------- // Node: Code - Normalizar Resposta IA // --------------------------------------------------------------------- const CODE_NORMALIZAR_IA = `${HEADER_AVISO} const item = $input.first().json; const erro = item.error || null; let conteudo = null; if (!erro) { conteudo = (item.choices && item.choices[0] && item.choices[0].message && item.choices[0].message.content) || null; } return [{ json: { aiCall: { attempted: true, error: erro ? String((erro && erro.message) || erro) : (conteudo ? null : 'resposta_sem_conteudo'), content: conteudo, }, }, }]; `; // --------------------------------------------------------------------- // Node: Code - IA Desativada (fallback direto, ramo FALSE do IF) // --------------------------------------------------------------------- const CODE_IA_DESATIVADA = `${HEADER_AVISO} return [{ json: { aiCall: { attempted: false, error: null, content: null } } }]; `; // --------------------------------------------------------------------- // Node: Code - Motor V2: Relatorio Completo // --------------------------------------------------------------------- const CODE_MOTOR_V2 = `${HEADER_AVISO} ${joinBlocks(MOD.period, MOD.baseline, MOD.anomaly, MOD.insights, MOD.aiSchema, MOD.format, MOD.sendGuard, MOD.pipeline)} const base = $('Code - Periodo e Queries SQL').first().json; const config = base.config; const period = base.period; const indicators = $('MySQL - Indicadores do Periodo').first().json; const rankingCategorias = $('MySQL - Ranking Categorias').all().map((i) => i.json); const rankingClientes = $('MySQL - Ranking Clientes').all().map((i) => i.json); const baselineRowsCategoria = $('MySQL - Baseline Diario Categoria').all().map((i) => i.json); const baselineRowsCliente = $('MySQL - Baseline Diario Cliente').all().map((i) => i.json); const ticketsContexto = $('MySQL - Contexto de Chamados').all().map((i) => i.json); const aiCall = $input.first().json.aiCall; // Data Table - Buscar Memoria/Historico usam alwaysOutputData (para nao // interromper o pipeline quando a tabela esta vazia, ex: primeira execucao // sem memoria ainda). Isso faz o n8n emitir um item vazio {json:{}} quando // nao ha linhas — filtramos esse item fantasma pelo campo obrigatorio de // cada tabela, para nao tratar {} como um registro real. const previousMemoryRows = $('Data Table - Buscar Memoria de Insights') .all() .map((i) => i.json) .filter((r) => r && r.fingerprint); const historicoRows = $('Data Table - Buscar Historico Anterior') .all() .map((i) => i.json) .filter((r) => r && r.tipo_periodo); const historicoAnterior = historicoRows.length ? historicoRows[0] : null; const todayIso = new Date().toISOString().slice(0, 10); // $execution.mode: 'manual' quando disparado pelo botao "Execute workflow"; // qualquer outro valor (ex: 'trigger') quando disparado pelo Schedule // Trigger. Ver src/v2/sendGuard.js#canSend — manual so envia com // FORCE_SEND=true explicito. const triggerType = $execution.mode === 'manual' ? 'manual' : 'scheduled'; const sendLogRows = $('Data Table - Buscar Log de Envios') .all() .map((i) => i.json) .filter((r) => r && r.send_key); const input = { period, indicators, rankingCategorias, rankingClientes, baselineRowsCategoria, baselineRowsCliente, ticketsContexto, aiCall, previousMemoryRows, previousPendentes: historicoAnterior ? Number(historicoAnterior.pendentes) : null, altaPrioridadeAnterior: historicoAnterior ? Number(historicoAnterior.alta_prioridade_pendentes) : null, todayIso, triggerType, sendLogRows, }; const relatorio = buildReport(input, config); return [{ json: { ...relatorio, period, config, indicators, todayIso } }]; `; // --------------------------------------------------------------------- // Node: Code - Preparar Historico (flatten para insert na Data Table) // --------------------------------------------------------------------- const CODE_PREPARAR_HISTORICO = `${HEADER_AVISO} const item = $input.first().json; const p = item.period; const ind = item.indicators; return [{ json: { tipo_periodo: p.tipo_periodo, history_bucket: p.history_bucket, periodo_label: p.periodo_label, data_inicio: p.data_inicio, data_fim: p.data_fim, abertos: Number(ind.abertos), fechados: Number(ind.fechados), saldo: Number(ind.saldo), pendentes: Number(ind.pendentes), alta_prioridade_pendentes: Number(ind.alta_prioridade_pendentes), executed_at: new Date().toISOString(), }, }]; `; module.exports = { CODE_PERIODO_QUERIES, CODE_CONTEXTO_IA, CODE_NORMALIZAR_IA, CODE_IA_DESATIVADA, CODE_MOTOR_V2, CODE_PREPARAR_HISTORICO, OUT_FILE, }; if (require.main === module) { // eslint-disable-next-line global-require require('./build_v2_workflow_assemble.js')(module.exports); }