'use strict'; const fs = require('fs'); // Credencial real criada no n8n DEV em 2026-08-24 (nome "GLPI", tipo mySql). // O nome "MySQL GLPI - Coordenador" citado na missao nao existia; esta e a // que foi de fato provisionada e validada ao vivo (2026-08-25). const MYSQL_CRED = { mySql: { id: 'dsVj1nmOIXRvix9n', name: 'GLPI' } }; const OPENAI_CRED = { openAiApi: { id: 'WUGriAn5FgCiLza2', name: 'OpenAI - Coordenador GLPI' } }; const DATATABLE_MEMORIA = { mode: 'list', value: 'wCxOOGhtrzjkPglB', cachedResultName: 'dev_glpi_v2_insights_memory' }; const DATATABLE_HISTORICO = { mode: 'list', value: 'fa7GJSlCaguJ9OBl', cachedResultName: 'dev_glpi_v2_run_history' }; function setAssignment(id, name, type, value) { return { id, name, type, value }; } module.exports = function build(codeBodies) { const { CODE_PERIODO_QUERIES, CODE_CONTEXTO_IA, CODE_NORMALIZAR_IA, CODE_IA_DESATIVADA, CODE_MOTOR_V2, CODE_PREPARAR_HISTORICO, OUT_FILE, } = codeBodies; const nodes = [ { id: 'manual-trigger-v2', name: 'Manual Trigger', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 200], parameters: {}, }, { id: 'schedule-trigger-v2', name: 'Schedule Trigger - Dias uteis 08h', type: 'n8n-nodes-base.scheduleTrigger', typeVersion: 1.2, position: [0, 380], parameters: { rule: { interval: [{ field: 'cronExpression', expression: '0 8 * * 1-5' }] }, }, }, { id: 'config-v2', name: 'Config - V2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [260, 290], parameters: { assignments: { assignments: [ setAssignment('data_alvo', 'data_alvo', 'string', ''), setAssignment('slack_webhook_url', 'slack_webhook_url', 'string', 'https://hooks.slack.com/services/SEU/WEBHOOK/AQUI'), setAssignment('AI_ENABLED', 'AI_ENABLED', 'boolean', true), setAssignment('AI_MODEL', 'AI_MODEL', 'string', 'gpt-5.5'), setAssignment('AI_MAX_TICKETS', 'AI_MAX_TICKETS', 'number', 40), setAssignment('AI_MAX_CONTEXT_CHARS', 'AI_MAX_CONTEXT_CHARS', 'number', 12000), setAssignment('AI_MIN_CONFIDENCE', 'AI_MIN_CONFIDENCE', 'number', 0.6), setAssignment('AI_TIMEOUT_MS', 'AI_TIMEOUT_MS', 'number', 30000), setAssignment('AI_MAX_COMPLETION_TOKENS', 'AI_MAX_COMPLETION_TOKENS', 'number', 3000), setAssignment('AI_REASONING_EFFORT', 'AI_REASONING_EFFORT', 'string', 'low'), setAssignment('HISTORICAL_BASELINE_DAYS', 'HISTORICAL_BASELINE_DAYS', 'number', 20), setAssignment('HISTORICAL_BASELINE_WEEKS', 'HISTORICAL_BASELINE_WEEKS', 'number', 4), setAssignment('MAX_REPORT_INSIGHTS', 'MAX_REPORT_INSIGHTS', 'number', 5), setAssignment('DEV_MODE', 'DEV_MODE', 'boolean', true), setAssignment('SHADOW_MODE', 'SHADOW_MODE', 'boolean', true), setAssignment('EXTENDED_METRICS_ENABLED', 'EXTENDED_METRICS_ENABLED', 'boolean', false), setAssignment('ANOMALY_MULTIPLIER', 'ANOMALY_MULTIPLIER', 'number', 2.0), setAssignment('ANOMALY_MIN_DELTA', 'ANOMALY_MIN_DELTA', 'number', 3), setAssignment('CLIENT_CONCENTRATION_MIN_COUNT', 'CLIENT_CONCENTRATION_MIN_COUNT', 'number', 3), setAssignment('CLIENT_CONCENTRATION_MIN_SHARE', 'CLIENT_CONCENTRATION_MIN_SHARE', 'number', 0.4), setAssignment('PENDING_AGE_ATTENTION_DAYS', 'PENDING_AGE_ATTENTION_DAYS', 'number', 15), setAssignment('TICKET_CONTEXT_SQL_LIMIT', 'TICKET_CONTEXT_SQL_LIMIT', 'number', 300), ], }, options: {}, }, }, { id: 'code-periodo-queries', name: 'Code - Periodo e Queries SQL', type: 'n8n-nodes-base.code', typeVersion: 2, position: [520, 290], parameters: { jsCode: CODE_PERIODO_QUERIES }, }, mysqlNode('mysql-indicadores', 'MySQL - Indicadores do Periodo', [780, 100], 'queries.indicadores'), mysqlNode('mysql-ranking-categoria', 'MySQL - Ranking Categorias', [1040, 100], 'queries.ranking_categoria'), mysqlNode('mysql-ranking-cliente', 'MySQL - Ranking Clientes', [1300, 100], 'queries.ranking_cliente'), mysqlNode('mysql-contexto-tickets', 'MySQL - Contexto de Chamados', [1560, 100], 'queries.contexto_tickets'), mysqlNode('mysql-baseline-categoria', 'MySQL - Baseline Diario Categoria', [1820, 100], 'queries.baseline_categoria'), mysqlNode('mysql-baseline-cliente', 'MySQL - Baseline Diario Cliente', [2080, 100], 'queries.baseline_cliente'), { id: 'dt-buscar-memoria', name: 'Data Table - Buscar Memoria de Insights', type: 'n8n-nodes-base.dataTable', typeVersion: 1.1, position: [2340, 100], // executeOnce: sem isso, o n8n roda esta busca uma vez POR ITEM // recebido do node anterior (causa raiz real do timeout em Baseline // Diario Cliente, validada ao vivo em 2026-08-25 — ver comentario em // buildBaselineDailyQuery). alwaysOutputData: garante que o node // emita 1 item mesmo com a tabela vazia (1a execucao, sem memoria // ainda) — sem isso, 0 itens aqui paravam o resto do pipeline // (n8n pula nodes que recebem 0 itens de entrada). executeOnce: true, alwaysOutputData: true, parameters: { resource: 'row', operation: 'get', dataTableId: DATATABLE_MEMORIA, filters: { conditions: [] }, returnAll: true, }, }, { id: 'dt-buscar-historico', name: 'Data Table - Buscar Historico Anterior', type: 'n8n-nodes-base.dataTable', typeVersion: 1.1, position: [2600, 100], executeOnce: true, alwaysOutputData: true, parameters: { resource: 'row', operation: 'get', dataTableId: DATATABLE_HISTORICO, filters: { conditions: [ { keyName: 'history_bucket', condition: 'eq', keyValue: "={{ $('Code - Periodo e Queries SQL').first().json.period.history_bucket }}", }, ], }, returnAll: false, limit: 5, orderBy: true, orderByColumn: 'data_inicio', orderByDirection: 'DESC', }, }, { id: 'code-contexto-ia', name: 'Code - Preparar Contexto IA', type: 'n8n-nodes-base.code', typeVersion: 2, position: [2860, 100], parameters: { jsCode: CODE_CONTEXTO_IA }, }, { id: 'if-ia-habilitada', name: 'IF - IA Habilitada?', type: 'n8n-nodes-base.if', typeVersion: 2.2, position: [3120, 100], parameters: { conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, conditions: [ { id: 'cond-ia-habilitada', leftValue: "={{ $('Code - Periodo e Queries SQL').first().json.config.AI_ENABLED }}", rightValue: true, operator: { type: 'boolean', operation: 'true', singleValue: true }, }, ], combinator: 'and', }, options: {}, }, }, { id: 'http-openai', name: 'HTTP Request - OpenAI Chat Completions', type: 'n8n-nodes-base.httpRequest', typeVersion: 4.2, position: [3380, -40], onError: 'continueRegularOutput', retryOnFail: true, maxTries: 2, waitBetweenTries: 5000, alwaysOutputData: true, parameters: { method: 'POST', url: 'https://api.openai.com/v1/chat/completions', authentication: 'predefinedCredentialType', nodeCredentialType: 'openAiApi', sendBody: true, specifyBody: 'json', jsonBody: '={{ $json.openai_request_body }}', options: { timeout: "={{ $('Code - Periodo e Queries SQL').first().json.config.AI_TIMEOUT_MS }}", response: { response: { responseFormat: 'json' } }, }, }, credentials: OPENAI_CRED, }, { id: 'code-normalizar-ia', name: 'Code - Normalizar Resposta IA', type: 'n8n-nodes-base.code', typeVersion: 2, position: [3640, -40], parameters: { jsCode: CODE_NORMALIZAR_IA }, }, { id: 'code-ia-desativada', name: 'Code - IA Desativada', type: 'n8n-nodes-base.code', typeVersion: 2, position: [3380, 240], parameters: { jsCode: CODE_IA_DESATIVADA }, }, { id: 'code-motor-v2', name: 'Code - Motor V2: Relatorio Completo', type: 'n8n-nodes-base.code', typeVersion: 2, position: [3900, 100], parameters: { jsCode: CODE_MOTOR_V2 }, }, { id: 'split-out-memoria', name: 'Split Out - Memoria Atualizacoes', type: 'n8n-nodes-base.splitOut', typeVersion: 1, position: [4160, -120], parameters: { fieldToSplitOut: 'memoria_atualizacoes', options: {} }, }, { id: 'dt-upsert-memoria', name: 'Data Table - Upsert Memoria de Insights', type: 'n8n-nodes-base.dataTable', typeVersion: 1.1, position: [4420, -120], parameters: { resource: 'row', operation: 'upsert', dataTableId: DATATABLE_MEMORIA, matchType: 'anyCondition', filters: { conditions: [{ keyName: 'fingerprint', condition: 'eq', keyValue: '={{ $json.fingerprint }}' }], }, columns: { mappingMode: 'autoMapInputData', value: {}, matchingColumns: ['fingerprint'], schema: [] }, options: {}, }, }, { id: 'code-preparar-historico', name: 'Code - Preparar Historico', type: 'n8n-nodes-base.code', typeVersion: 2, position: [4160, 100], parameters: { jsCode: CODE_PREPARAR_HISTORICO }, }, { id: 'dt-insert-historico', name: 'Data Table - Insert Historico do Periodo', type: 'n8n-nodes-base.dataTable', typeVersion: 1.1, position: [4420, 100], parameters: { resource: 'row', operation: 'insert', dataTableId: DATATABLE_HISTORICO, columns: { mappingMode: 'autoMapInputData', value: {}, matchingColumns: [], schema: [] }, options: {}, }, }, { id: 'if-pode-enviar', name: 'IF - Pode Enviar (fora de Shadow/Dev)?', type: 'n8n-nodes-base.if', typeVersion: 2.2, position: [4160, 320], parameters: { conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, conditions: [ // ATENCAO: operation deve ser 'false' aqui (checa "e falso"), NAO // 'true'. Bug real encontrado e corrigido em 2026-08-25: com // operation:'true' a condicao checava "SHADOW_MODE e true" (o // OPOSTO do que o nome do node promete), e quase mandou uma // mensagem real ao Slack durante validacao em DEV — so nao // enviou porque a URL do webhook ainda era o placeholder. { id: 'cond-shadow', leftValue: '={{ $json.config.SHADOW_MODE }}', rightValue: false, operator: { type: 'boolean', operation: 'false', singleValue: true }, }, { id: 'cond-dev', leftValue: '={{ $json.config.DEV_MODE }}', rightValue: false, operator: { type: 'boolean', operation: 'false', singleValue: true }, }, ], combinator: 'and', }, options: {}, }, }, { id: 'http-slack-v2', name: 'HTTP - Slack Enviar Mensagem V2', type: 'n8n-nodes-base.httpRequest', typeVersion: 4.2, position: [4420, 260], parameters: { method: 'POST', url: "={{ $('Config - V2').first().json.slack_webhook_url }}", sendBody: true, contentType: 'json', specifyBody: 'keypair', bodyParameters: { parameters: [{ name: 'text', value: '={{ $json.mensagem_v2 }}' }] }, options: {}, }, }, { id: 'set-envio-suprimido', name: 'Set - Envio Suprimido (Shadow/Dev Mode)', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [4420, 400], parameters: { assignments: { assignments: [ setAssignment('envio_suprimido', 'envio_suprimido', 'boolean', true), setAssignment( 'motivo', 'motivo', 'string', '=Envio real bloqueado: SHADOW_MODE={{ $json.config.SHADOW_MODE }}, DEV_MODE={{ $json.config.DEV_MODE }}' ), ], }, options: { includeOtherFields: true }, }, }, ]; const connections = { 'Manual Trigger': mainTo('Config - V2'), 'Schedule Trigger - Dias uteis 08h': mainTo('Config - V2'), 'Config - V2': mainTo('Code - Periodo e Queries SQL'), 'Code - Periodo e Queries SQL': mainTo('MySQL - Indicadores do Periodo'), 'MySQL - Indicadores do Periodo': mainTo('MySQL - Ranking Categorias'), 'MySQL - Ranking Categorias': mainTo('MySQL - Ranking Clientes'), 'MySQL - Ranking Clientes': mainTo('MySQL - Contexto de Chamados'), 'MySQL - Contexto de Chamados': mainTo('MySQL - Baseline Diario Categoria'), 'MySQL - Baseline Diario Categoria': mainTo('MySQL - Baseline Diario Cliente'), 'MySQL - Baseline Diario Cliente': mainTo('Data Table - Buscar Memoria de Insights'), 'Data Table - Buscar Memoria de Insights': mainTo('Data Table - Buscar Historico Anterior'), 'Data Table - Buscar Historico Anterior': mainTo('Code - Preparar Contexto IA'), 'Code - Preparar Contexto IA': mainTo('IF - IA Habilitada?'), 'IF - IA Habilitada?': { main: [ [{ node: 'HTTP Request - OpenAI Chat Completions', type: 'main', index: 0 }], [{ node: 'Code - IA Desativada', type: 'main', index: 0 }], ], }, 'HTTP Request - OpenAI Chat Completions': mainTo('Code - Normalizar Resposta IA'), 'Code - Normalizar Resposta IA': mainTo('Code - Motor V2: Relatorio Completo'), 'Code - IA Desativada': mainTo('Code - Motor V2: Relatorio Completo'), 'Code - Motor V2: Relatorio Completo': { main: [ [ { node: 'Split Out - Memoria Atualizacoes', type: 'main', index: 0 }, { node: 'Code - Preparar Historico', type: 'main', index: 0 }, { node: 'IF - Pode Enviar (fora de Shadow/Dev)?', type: 'main', index: 0 }, ], ], }, 'Split Out - Memoria Atualizacoes': mainTo('Data Table - Upsert Memoria de Insights'), 'Code - Preparar Historico': mainTo('Data Table - Insert Historico do Periodo'), 'IF - Pode Enviar (fora de Shadow/Dev)?': { main: [ [{ node: 'HTTP - Slack Enviar Mensagem V2', type: 'main', index: 0 }], [{ node: 'Set - Envio Suprimido (Shadow/Dev Mode)', type: 'main', index: 0 }], ], }, }; const workflow = { name: '[DEV] GLPI - Relatorio Operacional IA v2', nodes, connections, pinData: {}, active: false, settings: { executionOrder: 'v1' }, versionId: '', meta: { templateCredsSetupCompleted: false }, tags: [], }; fs.writeFileSync(OUT_FILE, JSON.stringify(workflow, null, 2) + '\n', 'utf8'); console.log(`Workflow gerado: ${OUT_FILE}`); console.log(`Nodes: ${nodes.length}`); }; function mainTo(nodeName) { return { main: [[{ node: nodeName, type: 'main', index: 0 }]] }; } function mysqlNode(id, name, position, queryFieldPath) { return { id, name, type: 'n8n-nodes-base.mySql', typeVersion: 2.4, position, // executeOnce: obrigatorio. Sem isso, cada node MySQL roda uma vez POR // ITEM recebido do node anterior, e como cada query aqui retorna varias // linhas, o efeito e multiplicativo ao longo da cadeia (Ranking:7 -> // Cliente:70 -> Contexto:840 -> Baseline Categoria:840 execucoes, // gerando 76.440 itens -> Baseline Cliente tentou rodar 76.440 vezes e // deu timeout). Causa raiz real, validada com execution telemetry do // n8n em 2026-08-25 (execucao 16209 do workflow DEV). executeOnce: true, parameters: { operation: 'executeQuery', query: `={{ $('Code - Periodo e Queries SQL').first().json.${queryFieldPath} }}`, options: {}, }, credentials: MYSQL_CRED, }; }