git-subtree-dir: relatorio git-subtree-mainline:e322cec92bgit-subtree-split:34bcb132f3
269 lines
9.8 KiB
JavaScript
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);
|
|
}
|