Files
SLACK_COORD_RELATORIO/tools/build_v2_workflow.js
T
Automacao GLPIandClaude Sonnet 5 64ddfb9b7b V2: guarda de envio real (SEND_ENABLED/FORCE_SEND), idempotencia por send_key e Slack nativo
- 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>
2026-08-25 09:22:42 -03:00

269 lines
9.8 KiB
JavaScript

'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);
}