Ran the deployed workflow for real (temporary webhook trigger, briefly
activated, immediately deactivated and removed after each attempt) against
the real GLPI MySQL database and a real OpenAI credential. Found and fixed:
1. $env access is blocked on this n8n instance (N8N_BLOCK_ENV_ACCESS_IN_NODE).
parseConfig($env) threw "access to env vars denied" on the very first
Code node. Config now comes from the "Config - Coordenador GLPI v2" Set
node instead (same pattern the V1 workflow already used) -- expanded
that node with all 13 tunable parameters, and pointed the 3 affected
node drivers (build-sql-candidatos, decide-needs-ai, select-top10) at
it instead of $env.
2. Data Table "upsert" operation requires both `columns` (values) and a
separate `filters` (match condition) -- I had only set columns with
matchingColumns, which isn't enough; n8n refused to activate the
workflow ("Missing or invalid required parameters: filters"). Added
filters.conditions matching on ticket_id to both memory-writing Data
Table nodes.
3. Real architecture bug: "Data Table - Buscar Memoria" sat inline in the
41-candidate chain (Mesclar Contexto -> Buscar Memoria -> Decidir
Necessidade de IA). A bulk "get all rows" Data Table node run with 41
input items produced 41 identical fetches and its OWN output (the empty
memory table on a first run) replaced the 41 candidates entirely --
"Decidir Necessidade de IA" received 0 items and the whole main branch
silently skipped. Moved the memory fetch to a parallel branch off
Config (fires once) and added a real Merge node ("Merge - Aguardar
Memoria") to guarantee both the candidates and the memory rows have
executed before "Decidir Necessidade de IA" runs -- same synchronization
pattern already used for context and for the shadow comparison.
Also fixed a cosmetic bug found in the real message output: buildTopMessage
produced a quadruple newline between the header and the first ticket block
([header, '', ...blocks].join('\n\n') double-counts the blank line already
inserted by the '\n\n' join). Added a regression test.
Verified with a real execution (id 13599, 24/08/2026 19:25 UTC) against the
live GLPI database: 41 real candidates, 30 sent to OpenAI (capped), 11
fallback-capped, memory table populated with 41 real rows including
genuine WAITING_VENDOR/WAITING_REQUESTER classifications with real
blocker_type/waiting_for fields extracted from real ticket task content,
and a real shadow_runs row comparing Legacy x V2 (4 tickets removed by V2:
2 for INSUFFICIENT_CONTEXT, 1 WAITING_REQUESTER, 1 WAITING_VENDOR; 4 added).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
2.1 KiB
JavaScript
56 lines
2.1 KiB
JavaScript
const ACTION_LABELS = {
|
|
UNASSIGNED_ACTIONABLE: 'Atribuir técnico',
|
|
ACTIONABLE_NOW: 'Validar e executar',
|
|
WAITING_APPROVAL: 'Cobrar aprovação',
|
|
INSUFFICIENT_CONTEXT: 'Completar informação',
|
|
};
|
|
|
|
function shortActionLabel(aiClassification) {
|
|
if (aiClassification.is_fallback) return 'Revisar chamado';
|
|
const fromMap = ACTION_LABELS[aiClassification.classification];
|
|
if (fromMap) return fromMap;
|
|
const raw = String(aiClassification.next_action || 'Revisar chamado').trim();
|
|
return raw.length > 40 ? `${raw.slice(0, 37)}...` : raw;
|
|
}
|
|
|
|
function tituloCurto(texto) {
|
|
const limpo = String(texto || 'Sem titulo').replace(/\s+/g, ' ').trim();
|
|
return limpo.length > 60 ? `${limpo.slice(0, 57)}...` : limpo;
|
|
}
|
|
|
|
function signalLine(ticket) {
|
|
const parts = [];
|
|
parts.push(ticket.sem_tecnico ? 'SEM TÉCNICO' : ticket.tecnico_atribuido);
|
|
if (ticket.sla_vencido) parts.push('SLA vencido');
|
|
parts.push(`parado ${ticket.dias_sem_interacao}d`);
|
|
return parts.join(' • ');
|
|
}
|
|
|
|
function buildTopMessage(selected, dateStr, config) {
|
|
if (!selected.length) {
|
|
return `@time Coordenador GLPI - ${dateStr}\n\nNenhum chamado acionável encontrado hoje.`;
|
|
}
|
|
const header = `@time ${selected.length} chamados para atenção hoje — ${dateStr}`;
|
|
const blocks = selected.map((item, index) => {
|
|
const t = item.ticket;
|
|
const linhasDoBloco = [
|
|
`${index + 1}. #${t.id} • ${tituloCurto(t.titulo)}`,
|
|
signalLine(t),
|
|
`→ ${shortActionLabel(item.aiClassification)}`,
|
|
t.link_glpi,
|
|
];
|
|
return linhasDoBloco.join('\n');
|
|
});
|
|
const partesDaMensagem = [header, ...blocks];
|
|
return partesDaMensagem.join('\n\n');
|
|
}
|
|
|
|
function buildCoordinationDigest(changedBlocked, dateStr) {
|
|
if (!changedBlocked.length) return null;
|
|
const lines = changedBlocked.map((item) => `#${item.ticket.id} • ${(item.aiClassification && item.aiClassification.reason) || item.exclusionReason}`);
|
|
const partesDoDigest = [`Coordenação — bloqueios novos/alterados — ${dateStr}`, '', ...lines];
|
|
return partesDoDigest.join('\n');
|
|
}
|
|
|
|
module.exports = { shortActionLabel, tituloCurto, signalLine, buildTopMessage, buildCoordinationDigest };
|