- src/v2/sendGuard.js (novo): canSend() reduz SHADOW_MODE/DEV_MODE/ SEND_ENABLED/FORCE_SEND a uma formula unica testada exaustivamente (secao 15 do ciclo final: matriz completa de guarda de seguranca). computeMessageHash/buildSendKey/isDuplicate implementam a chave logica de idempotencia da secao 13 (report_type+periodo+destino+hash). - src/v2/config.js: SEND_ENABLED, FORCE_SEND, SLACK_DESTINATION. - src/v2/pipeline.js: buildReport agora calcula envio.pode_enviar/ motivo_guarda/duplicata_bloqueada/send_key a partir de triggerType (.mode) e sendLogRows (Data Table de log de envios). - tools/build_v2_workflow*.js: novo Data Table 'Buscar Log de Envios', troca do HTTP Slack com webhook placeholder por node Slack nativo (credencial real lNqwBBrtLfkRqw2X, reaproveitada do projeto irmao Coordenador GLPI), branches de sucesso/falha gravando o log de envio so apos confirmacao real do Slack. - test/sendGuard.test.js, test/workflowStructure.test.js (novos): cobrem a matriz de guarda de seguranca e travam regressao do bug de timeout (executeOnce=true nos 8 nodes de leitura em lote) e do bug de sendGuard.js nao concatenado no Code node gerado. - Corrigido bug real encontrado nesta sessao: sendGuard.js nao estava na lista de modulos concatenados pelo gerador, deixando canSend/ computeMessageHash/buildSendKey undefined em runtime. - Deployado e validado ao vivo no DEV (workflow 3x5kb58TeBCvirm6): n8n_validate_workflow -> valid:true, 0 erros, executeOnce preservado nos 9 nodes de leitura em lote. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
579 lines
23 KiB
JavaScript
579 lines
23 KiB
JavaScript
'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' };
|
|
// Data Table de log de envios (idempotencia, secao 13) — criada em
|
|
// 2026-08-25 via n8n_manage_datatable (createTable).
|
|
const DATATABLE_SEND_LOG = { mode: 'list', value: 'SWIeAdMmpMCZ2rVE', cachedResultName: 'dev_glpi_v2_send_log' };
|
|
// Credencial Slack REAL, ja provisionada e em uso pelo projeto irmao "[DEV]
|
|
// Coordenador GLPI - Chamados Abertos v2 (Shadow)" (workflow y6JFEnp7Z2IJEUe2,
|
|
// node "Slack - Enviar Mensagem") — reaproveitada aqui em vez de criar uma
|
|
// credencial nova, conforme exigido pela missao ("nao me peca webhook se ja
|
|
// existe nas credenciais do ambiente").
|
|
const SLACK_CRED = { slackApi: { id: 'lNqwBBrtLfkRqw2X', name: 'Slack - Bot Coordenador (Chamados Suporte)' } };
|
|
|
|
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('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),
|
|
// Guarda de envio real (secoes 12/13/15) — ver src/v2/sendGuard.js.
|
|
// SEND_ENABLED/FORCE_SEND nao tem efeito sozinhos: DEV_MODE=true e
|
|
// SHADOW_MODE=true continuam bloqueando qualquer envio, absoluto,
|
|
// ate a promocao explicita deste workflow para producao.
|
|
setAssignment('SEND_ENABLED', 'SEND_ENABLED', 'boolean', true),
|
|
setAssignment('FORCE_SEND', 'FORCE_SEND', 'boolean', false),
|
|
// Canal Slack de destino do Resumo Diario/Semanal. Vazio ate
|
|
// confirmacao explicita do usuario sobre qual canal deve receber
|
|
// (nao deve ser adivinhado — ver secao 6 da missao).
|
|
setAssignment('SLACK_DESTINATION', 'SLACK_DESTINATION', 'string', ''),
|
|
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: 'dt-buscar-log-envios',
|
|
name: 'Data Table - Buscar Log de Envios',
|
|
type: 'n8n-nodes-base.dataTable',
|
|
typeVersion: 1.1,
|
|
position: [2860, 260],
|
|
// executeOnce + alwaysOutputData: mesmo motivo dos outros Data Table
|
|
// reads (ver Data Table - Buscar Memoria de Insights acima).
|
|
executeOnce: true,
|
|
alwaysOutputData: true,
|
|
parameters: {
|
|
resource: 'row',
|
|
operation: 'get',
|
|
dataTableId: DATATABLE_SEND_LOG,
|
|
filters: {
|
|
conditions: [
|
|
{
|
|
keyName: 'report_type',
|
|
condition: 'eq',
|
|
keyValue: "={{ $('Code - Periodo e Queries SQL').first().json.period.history_bucket }}",
|
|
},
|
|
{
|
|
keyName: 'period_start',
|
|
condition: 'eq',
|
|
keyValue: "={{ $('Code - Periodo e Queries SQL').first().json.period.data_inicio }}",
|
|
},
|
|
{
|
|
keyName: 'period_end',
|
|
condition: 'eq',
|
|
keyValue: "={{ $('Code - Periodo e Queries SQL').first().json.period.data_fim }}",
|
|
},
|
|
],
|
|
},
|
|
returnAll: true,
|
|
},
|
|
},
|
|
{
|
|
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: [
|
|
// A decisao completa (SHADOW_MODE + DEV_MODE + SEND_ENABLED +
|
|
// FORCE_SEND por tipo de gatilho + bloqueio de duplicata por
|
|
// idempotencia) ja foi calculada em JS testado por unidade
|
|
// (src/v2/sendGuard.js#canSend, chamado dentro de
|
|
// src/v2/pipeline.js#buildReport) — este IF so LE o resultado.
|
|
// Isso evita reproduzir a logica de guarda como condicoes soltas
|
|
// no n8n (fonte do bug real de operation:'true'/'false' invertido
|
|
// encontrado e corrigido em 2026-08-25).
|
|
{
|
|
id: 'cond-pode-enviar',
|
|
leftValue: '={{ $json.envio.pode_enviar }}',
|
|
rightValue: true,
|
|
operator: { type: 'boolean', operation: 'true', singleValue: true },
|
|
},
|
|
],
|
|
combinator: 'and',
|
|
},
|
|
options: {},
|
|
},
|
|
},
|
|
{
|
|
id: 'slack-enviar-mensagem-v2',
|
|
name: 'Slack - Enviar Mensagem V2',
|
|
type: 'n8n-nodes-base.slack',
|
|
typeVersion: 2.5,
|
|
position: [4420, 200],
|
|
retryOnFail: true,
|
|
maxTries: 2,
|
|
waitBetweenTries: 3000,
|
|
// continueErrorOutput: mesmo padrao do node irmao "Slack - Enviar
|
|
// Mensagem" (workflow y6JFEnp7Z2IJEUe2) — separa sucesso/falha em duas
|
|
// saidas, para so gravar status=success no log de idempotencia DEPOIS
|
|
// que o Slack de fato confirmar o envio (secao 13 da missao).
|
|
onError: 'continueErrorOutput',
|
|
parameters: {
|
|
select: 'channel',
|
|
// channelId.value fica vazio ate o usuario confirmar o canal de
|
|
// destino do Resumo Diario/Semanal (nao deve ser adivinhado — ver
|
|
// secao 6 da missao). Ate la, IF - Pode Enviar so libera este node
|
|
// com SHADOW_MODE=false e SEND_ENABLED=true explicitos, e o teste
|
|
// controlado de envio (secao 14) so roda apos essa confirmacao.
|
|
channelId: { __rl: true, mode: 'id', value: "={{ $('Code - Periodo e Queries SQL').first().json.config.SLACK_DESTINATION }}" },
|
|
text: '={{ $json.mensagem_v2 }}',
|
|
otherOptions: { includeLinkToWorkflow: false },
|
|
},
|
|
credentials: SLACK_CRED,
|
|
},
|
|
{
|
|
id: 'code-preparar-log-envio-sucesso',
|
|
name: 'Code - Preparar Log de Envio (Sucesso)',
|
|
type: 'n8n-nodes-base.code',
|
|
typeVersion: 2,
|
|
position: [4680, 100],
|
|
parameters: {
|
|
jsCode:
|
|
"const relatorio = $('Code - Motor V2: Relatorio Completo').first().json;\n" +
|
|
'const envio = relatorio.envio;\n' +
|
|
'return [{ json: {\n' +
|
|
' send_key: envio.send_key,\n' +
|
|
' report_type: envio.report_type,\n' +
|
|
' period_start: envio.period_start,\n' +
|
|
' period_end: envio.period_end,\n' +
|
|
' destination: envio.destino,\n' +
|
|
' message_hash: envio.message_hash,\n' +
|
|
' trigger_type: envio.trigger_type,\n' +
|
|
" status: 'success',\n" +
|
|
' sent_at: new Date().toISOString(),\n' +
|
|
'} }];\n',
|
|
},
|
|
},
|
|
{
|
|
id: 'dt-registrar-log-envio-sucesso',
|
|
name: 'Data Table - Registrar Log de Envio (Sucesso)',
|
|
type: 'n8n-nodes-base.dataTable',
|
|
typeVersion: 1.1,
|
|
position: [4940, 100],
|
|
parameters: {
|
|
resource: 'row',
|
|
operation: 'insert',
|
|
dataTableId: DATATABLE_SEND_LOG,
|
|
columns: { mappingMode: 'autoMapInputData', value: {}, matchingColumns: [], schema: [] },
|
|
options: {},
|
|
},
|
|
},
|
|
{
|
|
id: 'code-preparar-log-envio-falha',
|
|
name: 'Code - Preparar Log de Envio (Falha)',
|
|
type: 'n8n-nodes-base.code',
|
|
typeVersion: 2,
|
|
position: [4680, 300],
|
|
parameters: {
|
|
jsCode:
|
|
"const relatorio = $('Code - Motor V2: Relatorio Completo').first().json;\n" +
|
|
'const envio = relatorio.envio;\n' +
|
|
'const erro = $input.first().json.error || null;\n' +
|
|
'return [{ json: {\n' +
|
|
' send_key: envio.send_key,\n' +
|
|
' report_type: envio.report_type,\n' +
|
|
' period_start: envio.period_start,\n' +
|
|
' period_end: envio.period_end,\n' +
|
|
' destination: envio.destino,\n' +
|
|
' message_hash: envio.message_hash,\n' +
|
|
' trigger_type: envio.trigger_type,\n' +
|
|
" status: 'failed',\n" +
|
|
' sent_at: new Date().toISOString(),\n' +
|
|
' erro: erro ? String((erro && erro.message) || erro) : null,\n' +
|
|
'} }];\n',
|
|
},
|
|
},
|
|
{
|
|
id: 'dt-registrar-log-envio-falha',
|
|
name: 'Data Table - Registrar Log de Envio (Falha)',
|
|
type: 'n8n-nodes-base.dataTable',
|
|
typeVersion: 1.1,
|
|
position: [4940, 300],
|
|
parameters: {
|
|
resource: 'row',
|
|
operation: 'insert',
|
|
dataTableId: DATATABLE_SEND_LOG,
|
|
// "erro" nao faz parte do schema fixo da Data Table (imutavel apos
|
|
// criacao) — autoMapInputData ignora campos sem coluna correspondente,
|
|
// entao o detalhe do erro fica so no log/execucao do n8n, nao na tabela.
|
|
columns: { mappingMode: 'autoMapInputData', value: {}, matchingColumns: [], schema: [] },
|
|
options: {},
|
|
},
|
|
},
|
|
{
|
|
id: 'set-envio-suprimido',
|
|
name: 'Set - Envio Suprimido (Guarda/Duplicata)',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3.4,
|
|
position: [4420, 440],
|
|
parameters: {
|
|
assignments: {
|
|
assignments: [
|
|
setAssignment('envio_suprimido', 'envio_suprimido', 'boolean', true),
|
|
setAssignment('motivo', 'motivo', 'string', '={{ $json.envio.duplicata_bloqueada ? "BLOCKED_DUPLICATE: " + $json.envio.send_key : $json.envio.motivo_guarda }}'),
|
|
],
|
|
},
|
|
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('Data Table - Buscar Log de Envios'),
|
|
'Data Table - Buscar Log de Envios': 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: 'Slack - Enviar Mensagem V2', type: 'main', index: 0 }],
|
|
[{ node: 'Set - Envio Suprimido (Guarda/Duplicata)', type: 'main', index: 0 }],
|
|
],
|
|
},
|
|
'Slack - Enviar Mensagem V2': {
|
|
main: [
|
|
[{ node: 'Code - Preparar Log de Envio (Sucesso)', type: 'main', index: 0 }],
|
|
[{ node: 'Code - Preparar Log de Envio (Falha)', type: 'main', index: 0 }],
|
|
],
|
|
},
|
|
'Code - Preparar Log de Envio (Sucesso)': mainTo('Data Table - Registrar Log de Envio (Sucesso)'),
|
|
'Code - Preparar Log de Envio (Falha)': mainTo('Data Table - Registrar Log de Envio (Falha)'),
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|