@wondai/n8n-nodes-nucleo 0.2.8 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/nodes/Nucleo/Nucleo.node.js +357 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,16 +20,62 @@ parede. Isso mantém o segredo fora do workflow, a lógica num lugar só e o tok
|
|
|
20
20
|
|---|---|---|---|
|
|
21
21
|
| Cliente | Buscar | `GET /api/v1/agent/cliente` | `cliente:ler` |
|
|
22
22
|
| Catálogo | Resolver Produtos | `POST /api/v1/agent/catalogo/resolver` | `catalogo:ler` |
|
|
23
|
+
| Catálogo | Disponibilidade na Rede | `POST /api/v1/agent/catalogo/disponibilidade-rede` | `rede:consultar` |
|
|
23
24
|
| Pedido | Detalhar | `GET /api/v1/agent/pedido/:ref` | `pedido:ler` |
|
|
24
25
|
| Pedido | Criar | `POST /api/v1/agent/pedido` | `pedido:escrever` |
|
|
25
26
|
| Pedido | Alterar | `PATCH /api/v1/agent/pedido/:id` | `pedido:escrever` |
|
|
26
27
|
| Pedido | Cancelar | `POST /api/v1/agent/pedido/:id/cancelar` | `pedido:escrever` |
|
|
28
|
+
| Conversa | Preparar | `POST /api/v1/agent/conversa/preparar` | `contexto:ler` |
|
|
29
|
+
| Contexto | Consultar Data | `POST /api/v1/agent/contexto/consultar` | `contexto:ler` |
|
|
27
30
|
| Conversa | Registrar | `POST /api/v1/agent/conversa/fechar` | `conversa:escrever` |
|
|
28
31
|
|
|
29
32
|
**Resolver Produtos** é a operação inteligente: manda várias consultas numa chamada (máx 10),
|
|
30
33
|
tolera erro de digitação (`banofe`→Banoffee), falta de acento (`pao frances`→Pão Francês) e
|
|
31
34
|
apelidos; devolve no máx 3 candidatos por consulta com `status` (`achou`/`ambiguo`/`nao_achou`).
|
|
32
35
|
|
|
36
|
+
**Disponibilidade na Rede** ("tem na outra loja?") é exposta como **tool** do AI Agent, mas com regra
|
|
37
|
+
estrita no prompt: **usar SÓ quando o cliente pedir explicitamente outra unidade**. Passe o `produto_id`
|
|
38
|
+
já resolvido (ou uma `consulta` curta). A loja que pergunta vem do **vínculo do token** (a IA não
|
|
39
|
+
escolhe a unidade). Devolve **no máximo 5** unidades participantes com estado, "atualizado há",
|
|
40
|
+
telefone/endereço (allowlist) e distância opcional. **Disponibilidade NÃO é garantia**: vencida vira
|
|
41
|
+
`status: "unknown"` (`confirmation_required: true`) — a IA **nunca promete**, só informa o que a loja
|
|
42
|
+
declarou e oferece o contato para o cliente confirmar. Este endpoint **não** entra no contexto fixo do
|
|
43
|
+
gate; é chamado sob demanda. Tenant/unidade vêm do token — nunca do prompt.
|
|
44
|
+
|
|
45
|
+
## Gate da IA — Preparar como PRIMEIRO passo (ADR-021, Plano 004)
|
|
46
|
+
|
|
47
|
+
**Conversa → Preparar** é o **gate determinístico** da IA. Use-o como **node normal** no primeiro
|
|
48
|
+
passo do workflow, **antes de qualquer nó de LLM/memória/router** — **nunca** como ferramenta do AI
|
|
49
|
+
Agent (senão a LLM decidiria se atende, o que anula o gate e gasta tokens).
|
|
50
|
+
|
|
51
|
+
A resposta vem sempre em **HTTP 200** e o node a entrega **intacta** (não vira erro nem retry):
|
|
52
|
+
|
|
53
|
+
- `{ "allowed": false, "state": "disabled", "reason": "tenant_disabled" | "unit_disabled" | "configuration_error" | "unavailable" }`
|
|
54
|
+
→ **PARE o fluxo sem responder** ao cliente (a empresa/unidade desligou a IA; falha de consulta também para).
|
|
55
|
+
- `{ "allowed": true, "state": "enabled", "runtime_version": N, "session_id": "...", "primeiro_contato": true, "contexto": { ... } }`
|
|
56
|
+
→ **siga** para memória/router/LLM. O `contexto` traz horário/status/flags/regras (Plano 003).
|
|
57
|
+
|
|
58
|
+
O Núcleo é a fonte da verdade do liga/desliga; o estado `active` do workflow no n8n **não** é usado
|
|
59
|
+
como controle por padaria. Quem desliga é o dono/gerente em `/configuracoes` (ou o super-admin).
|
|
60
|
+
|
|
61
|
+
### Workflow-base (reproduza assim; não há export real nesta entrega)
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Trigger Evolution (WhatsApp)
|
|
65
|
+
→ Normaliza só o identificador técnico (remoteJid → conversation_key/telefone)
|
|
66
|
+
→ Núcleo: Conversa → Preparar (NODE NORMAL — o gate)
|
|
67
|
+
→ IF {{ $json.allowed === true }}
|
|
68
|
+
├─ true → memória / router / especialistas / LLM → (Pedido/Catálogo/Conversa) → Registrar
|
|
69
|
+
└─ false → encerrar SEM resposta (No-Op / Stop)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Regras do artefato de workflow:
|
|
73
|
+
|
|
74
|
+
- **Nenhum** nó Gemini/AI Agent/memory/subworkflow com LLM pode vir **antes** do Preparar.
|
|
75
|
+
- As ferramentas de **escrita** (Pedido Criar/Alterar/Cancelar) revalidam o runtime no servidor
|
|
76
|
+
(403 se a IA foi desligada no meio); **Registrar** continua permitido (fecha sessão já iniciada).
|
|
77
|
+
- O node permanece **burro**: só assina e chama; toda decisão é do Núcleo.
|
|
78
|
+
|
|
33
79
|
**Criar** é idempotente: deixe *Idempotency Key* vazio (gera uma) ou repita a mesma chave num retry
|
|
34
80
|
— o Núcleo nunca duplica o pedido.
|
|
35
81
|
|
|
@@ -37,6 +83,15 @@ No **Pedido Criar**, `Nome do Cliente` e `Endereço de Entrega (JSON)` são opci
|
|
|
37
83
|
carregar o que a IA já coletou no atendimento. O node continua burro: só envia `nome_cliente` e
|
|
38
84
|
`endereco_entrega`; quem decide tenant, valida e grava snapshot em `crm.entregas` é o Núcleo.
|
|
39
85
|
|
|
86
|
+
### Loja e preço por unidade (Plano 005, ADR-018) — sem lógica no node
|
|
87
|
+
|
|
88
|
+
A unidade do atendimento vem do **vínculo do token** (`unit_id`), no servidor — a IA **não escolhe a
|
|
89
|
+
loja**. Deixe o campo **Loja (unit_id)** do *Pedido Criar* **vazio** quando o token está vinculado a
|
|
90
|
+
uma unidade; se preenchido, ele precisa **bater com o vínculo** (senão o Núcleo recusa com **422**).
|
|
91
|
+
O servidor devolve sempre o **sortimento e o preço EFETIVOS** da unidade (override da unidade vence a
|
|
92
|
+
base) — o node **não** tem regra de herança/preço: só assina e chama. Produto **indisponível** na
|
|
93
|
+
unidade é recusado (422) ao criar/alterar; o **Resolver Produtos** já não lista o que está fora.
|
|
94
|
+
|
|
40
95
|
## Instalação (n8n self-hosted)
|
|
41
96
|
|
|
42
97
|
1. **Settings → Community Nodes → Install** → `@wondai/n8n-nodes-nucleo`.
|
|
@@ -137,7 +137,7 @@ class Nucleo {
|
|
|
137
137
|
group: ["transform"],
|
|
138
138
|
version: 1,
|
|
139
139
|
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
|
|
140
|
-
description: "Atendimento de IA no Núcleo Wondai: buscar cliente, resolver catálogo (fuzzy)
|
|
140
|
+
description: "Atendimento de IA no Núcleo Wondai: preparar conversa (GATE liga/desliga + contexto operacional ANTES da LLM), buscar cliente, resolver catálogo (fuzzy), criar/alterar/cancelar pedido, consultar contexto e registrar conversa. Assinatura HMAC; o tenant vem do token.",
|
|
141
141
|
defaults: { name: "Núcleo Wondai" },
|
|
142
142
|
// Permite usar o node como ferramenta do AI Agent (router/especialistas, ADR-016).
|
|
143
143
|
usableAsTool: true,
|
|
@@ -156,6 +156,8 @@ class Nucleo {
|
|
|
156
156
|
{ name: "Catálogo", value: "catalogo" },
|
|
157
157
|
{ name: "Pedido", value: "pedido" },
|
|
158
158
|
{ name: "Conversa", value: "conversa" },
|
|
159
|
+
{ name: "Contexto", value: "contexto" },
|
|
160
|
+
{ name: "Telemetria", value: "telemetria" },
|
|
159
161
|
],
|
|
160
162
|
default: "catalogo",
|
|
161
163
|
},
|
|
@@ -189,6 +191,15 @@ class Nucleo {
|
|
|
189
191
|
action: "Resolver produtos do catálogo",
|
|
190
192
|
description: "Busca vários produtos numa chamada — tolera erro de digitação e apelidos",
|
|
191
193
|
},
|
|
194
|
+
{
|
|
195
|
+
name: "Disponibilidade Na Rede",
|
|
196
|
+
value: "disponibilidadeRede",
|
|
197
|
+
action: "Consultar disponibilidade em outras unidades da rede",
|
|
198
|
+
description: "USAR SOMENTE quando o cliente pedir explicitamente outra unidade ('tem na outra loja?'). " +
|
|
199
|
+
"Devolve no máximo 5 unidades com estado e contato. Disponibilidade NÃO é garantia: pode " +
|
|
200
|
+
"estar vencida (status 'unknown') — NUNCA prometa que tem; informe o que a loja declarou e " +
|
|
201
|
+
"ofereça o contato para o cliente confirmar.",
|
|
202
|
+
},
|
|
192
203
|
],
|
|
193
204
|
default: "resolver",
|
|
194
205
|
},
|
|
@@ -233,6 +244,12 @@ class Nucleo {
|
|
|
233
244
|
noDataExpression: true,
|
|
234
245
|
displayOptions: { show: { resource: ["conversa"] } },
|
|
235
246
|
options: [
|
|
247
|
+
{
|
|
248
|
+
name: "Preparar",
|
|
249
|
+
value: "preparar",
|
|
250
|
+
action: "Preparar conversa (gate + contexto)",
|
|
251
|
+
description: "GATE da IA + contexto. Use como NODE NORMAL no PRIMEIRO passo, antes de qualquer LLM/memória — nunca como tool do AI Agent. Devolve allowed:false (200) quando a IA está desligada (trate qualquer allowed!==true como PARAR, sem responder); allowed:true segue para memória/router/LLM com o contexto operacional",
|
|
252
|
+
},
|
|
236
253
|
{
|
|
237
254
|
name: "Registrar",
|
|
238
255
|
value: "registrar",
|
|
@@ -240,7 +257,41 @@ class Nucleo {
|
|
|
240
257
|
description: "Encerra a conversa com resumo + resultado (handoff = precisa_humano)",
|
|
241
258
|
},
|
|
242
259
|
],
|
|
243
|
-
default: "
|
|
260
|
+
default: "preparar",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
displayName: "Operação",
|
|
264
|
+
name: "operation",
|
|
265
|
+
type: "options",
|
|
266
|
+
noDataExpression: true,
|
|
267
|
+
displayOptions: { show: { resource: ["contexto"] } },
|
|
268
|
+
options: [
|
|
269
|
+
{
|
|
270
|
+
name: "Consultar Data",
|
|
271
|
+
value: "consultar",
|
|
272
|
+
action: "Consultar contexto de uma data",
|
|
273
|
+
description: "Horário/exceção/regra vigente para UMA data (ex.: 'abrem no feriado?')",
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
default: "consultar",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
displayName: "Operação",
|
|
280
|
+
name: "operation",
|
|
281
|
+
type: "options",
|
|
282
|
+
noDataExpression: true,
|
|
283
|
+
displayOptions: { show: { resource: ["telemetria"] } },
|
|
284
|
+
options: [
|
|
285
|
+
{
|
|
286
|
+
name: "Enviar",
|
|
287
|
+
value: "enviar",
|
|
288
|
+
action: "Enviar telemetria da execução",
|
|
289
|
+
description: "Envia métricas TÉCNICAS da execução (tokens/custo estimado/duração/ferramentas) DEPOIS da " +
|
|
290
|
+
"resposta. NÃO use como tool do AI Agent nem antes do gate. Sem prompt/resposta/PII. " +
|
|
291
|
+
"Idempotente por 'Chave do Evento' — reenvio não duplica. Falha aqui não repete a resposta.",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
default: "enviar",
|
|
244
295
|
},
|
|
245
296
|
// ----------------------------------------------------------------- cliente:buscar
|
|
246
297
|
{
|
|
@@ -265,6 +316,33 @@ class Nucleo {
|
|
|
265
316
|
description: "Produtos a buscar: um por linha ou separados por vírgula (máx 10). Aceita erro de digitação, falta de acento e apelidos. Ex: 'banofe, pão francês'.",
|
|
266
317
|
displayOptions: { show: { resource: ["catalogo"], operation: ["resolver"] } },
|
|
267
318
|
},
|
|
319
|
+
// ----------------------------------------------------------------- catalogo:disponibilidadeRede
|
|
320
|
+
{
|
|
321
|
+
displayName: "Produto (ID)",
|
|
322
|
+
name: "produtoIdRede",
|
|
323
|
+
type: "string",
|
|
324
|
+
default: "",
|
|
325
|
+
placeholder: "UUID do produto já resolvido",
|
|
326
|
+
description: "ID do produto (UUID) já resolvido por 'Resolver Produtos'. Preferível. A loja que pergunta vem do token (vínculo) — você não escolhe a unidade.",
|
|
327
|
+
displayOptions: { show: { resource: ["catalogo"], operation: ["disponibilidadeRede"] } },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
displayName: "Variação (ID, opcional)",
|
|
331
|
+
name: "variacaoIdRede",
|
|
332
|
+
type: "string",
|
|
333
|
+
default: "",
|
|
334
|
+
description: "ID da variação (UUID), opcional. A rede resolve por produto.",
|
|
335
|
+
displayOptions: { show: { resource: ["catalogo"], operation: ["disponibilidadeRede"] } },
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
displayName: "Consulta (opcional)",
|
|
339
|
+
name: "consultaRede",
|
|
340
|
+
type: "string",
|
|
341
|
+
default: "",
|
|
342
|
+
placeholder: "pão de queijo",
|
|
343
|
+
description: "Texto curto do produto, usado SÓ se o ID não for informado. Se ambíguo, nada é retornado — prefira resolver o produto antes.",
|
|
344
|
+
displayOptions: { show: { resource: ["catalogo"], operation: ["disponibilidadeRede"] } },
|
|
345
|
+
},
|
|
268
346
|
// ----------------------------------------------------------------- pedido:detalhar
|
|
269
347
|
{
|
|
270
348
|
displayName: "Pedido (ref)",
|
|
@@ -361,12 +439,28 @@ class Nucleo {
|
|
|
361
439
|
description: "SÓ dinheiro na entrega: valor da NOTA que o cliente vai entregar (ex.: 50). Tem que ser ≥ total do pedido. 0/vazio = não precisa de troco. O troco a separar = troco_para − total. (Só é enviado quando a forma de pagamento é dinheiro.)",
|
|
362
440
|
displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
|
|
363
441
|
},
|
|
442
|
+
{
|
|
443
|
+
displayName: "Taxa de Entrega",
|
|
444
|
+
name: "taxaEntrega",
|
|
445
|
+
type: "number",
|
|
446
|
+
default: 0,
|
|
447
|
+
description: "Opcional. Valor da taxa de entrega cobrada (ex.: 8). Soma no valor a cobrar e no troco. 0/vazio = sem taxa.",
|
|
448
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
displayName: "Desconto",
|
|
452
|
+
name: "desconto",
|
|
453
|
+
type: "number",
|
|
454
|
+
default: 0,
|
|
455
|
+
description: "Opcional. Desconto concedido em reais (ex.: 5). Abate do valor a cobrar e do troco. 0/vazio = sem desconto.",
|
|
456
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
|
|
457
|
+
},
|
|
364
458
|
{
|
|
365
459
|
displayName: "Loja (unit_id)",
|
|
366
460
|
name: "unitId",
|
|
367
461
|
type: "string",
|
|
368
462
|
default: "",
|
|
369
|
-
description: "Opcional.
|
|
463
|
+
description: "Opcional/legado. DEIXE VAZIO: a loja vem do vínculo do token (Plano 005) — a IA não escolhe a loja. Se preenchido, precisa bater com o vínculo do token, senão o Núcleo recusa (422).",
|
|
370
464
|
displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
|
|
371
465
|
},
|
|
372
466
|
{
|
|
@@ -472,6 +566,44 @@ class Nucleo {
|
|
|
472
566
|
description: "produto_ids a remover do pedido (todas as linhas do produto), separados por vírgula ou JSON array. Use para 'tirar os X' e para a troca. produto_id vem do Catálogo Resolver.",
|
|
473
567
|
displayOptions: { show: { resource: ["pedido"], operation: ["alterar"] } },
|
|
474
568
|
},
|
|
569
|
+
{
|
|
570
|
+
displayName: "Forma de Pagamento (alterar)",
|
|
571
|
+
name: "formaPagamentoNova",
|
|
572
|
+
type: "options",
|
|
573
|
+
options: [
|
|
574
|
+
{ name: "(não alterar)", value: "" },
|
|
575
|
+
{ name: "PIX", value: "pix" },
|
|
576
|
+
{ name: "Cartão", value: "cartao" },
|
|
577
|
+
{ name: "Dinheiro", value: "dinheiro" },
|
|
578
|
+
],
|
|
579
|
+
default: "",
|
|
580
|
+
description: "Troca/registra a forma de pagamento de um pedido já criado. Para dinheiro na entrega, pergunte se precisa de troco e preencha 'Troco Para (alterar)'. Vazio = não mexe no pagamento.",
|
|
581
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["alterar"] } },
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
displayName: "Troco Para (alterar)",
|
|
585
|
+
name: "trocoParaNovo",
|
|
586
|
+
type: "number",
|
|
587
|
+
default: 0,
|
|
588
|
+
description: "SÓ dinheiro na entrega: valor da NOTA que o cliente vai entregar. Tem que ser ≥ valor a cobrar. 0/vazio = não precisa de troco. Só é enviado quando 'Forma de Pagamento (alterar)' = dinheiro.",
|
|
589
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["alterar"] } },
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
displayName: "Taxa de Entrega (alterar)",
|
|
593
|
+
name: "taxaEntregaNova",
|
|
594
|
+
type: "string",
|
|
595
|
+
default: "",
|
|
596
|
+
description: "Redefine a taxa de entrega (recalcula o valor a cobrar e o pagamento pendente). Ex.: 8. Vazio = não altera a taxa. Use 0 para zerar.",
|
|
597
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["alterar"] } },
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
displayName: "Desconto (alterar)",
|
|
601
|
+
name: "descontoNovo",
|
|
602
|
+
type: "string",
|
|
603
|
+
default: "",
|
|
604
|
+
description: "Redefine o desconto (recalcula o valor a cobrar e o pagamento pendente). Ex.: 5. Vazio = não altera o desconto. Use 0 para zerar.",
|
|
605
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["alterar"] } },
|
|
606
|
+
},
|
|
475
607
|
{
|
|
476
608
|
displayName: "Motivo",
|
|
477
609
|
name: "motivo",
|
|
@@ -529,6 +661,135 @@ class Nucleo {
|
|
|
529
661
|
description: "Opcional. Use a MESMA chave (ex.: messageID do WhatsApp) num retry para não registrar a conversa duas vezes.",
|
|
530
662
|
displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
|
|
531
663
|
},
|
|
664
|
+
{
|
|
665
|
+
displayName: "Chave da Conversa (conversation_key)",
|
|
666
|
+
name: "conversationKey",
|
|
667
|
+
type: "string",
|
|
668
|
+
default: "",
|
|
669
|
+
placeholder: "+5511999999999",
|
|
670
|
+
description: "Mesma chave usada no Preparar (geralmente o telefone/remoteJid). Liga o fechamento à sessão para medir 'atendimento iniciado com a loja fechada'. Vazio = não mede (compat).",
|
|
671
|
+
displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
|
|
672
|
+
},
|
|
673
|
+
// ----------------------------------------------------------------- conversa:preparar
|
|
674
|
+
{
|
|
675
|
+
displayName: "Chave da Conversa (conversation_key)",
|
|
676
|
+
name: "conversationKey",
|
|
677
|
+
type: "string",
|
|
678
|
+
default: "",
|
|
679
|
+
required: true,
|
|
680
|
+
placeholder: "+5511999999999",
|
|
681
|
+
description: "Identificador externo do atendimento (geralmente o telefone/remoteJid do WhatsApp). Entra só como HASH no servidor — nunca é gravado cru.",
|
|
682
|
+
displayOptions: { show: { resource: ["conversa"], operation: ["preparar"] } },
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
displayName: "Canal",
|
|
686
|
+
name: "canal",
|
|
687
|
+
type: "string",
|
|
688
|
+
default: "whatsapp",
|
|
689
|
+
description: "Canal do atendimento (default 'whatsapp').",
|
|
690
|
+
displayOptions: { show: { resource: ["conversa"], operation: ["preparar"] } },
|
|
691
|
+
},
|
|
692
|
+
// ----------------------------------------------------------------- contexto:consultar
|
|
693
|
+
{
|
|
694
|
+
displayName: "Data",
|
|
695
|
+
name: "data",
|
|
696
|
+
type: "string",
|
|
697
|
+
default: "",
|
|
698
|
+
required: true,
|
|
699
|
+
placeholder: "2026-12-25",
|
|
700
|
+
description: "UMA data no formato YYYY-MM-DD (janela ±366 dias). Devolve horário/exceção/regra vigente para a data.",
|
|
701
|
+
displayOptions: { show: { resource: ["contexto"], operation: ["consultar"] } },
|
|
702
|
+
},
|
|
703
|
+
// ----------------------------------------------------------------- telemetria:enviar
|
|
704
|
+
{
|
|
705
|
+
displayName: "Chave do Evento (event_key)",
|
|
706
|
+
name: "telEventKey",
|
|
707
|
+
type: "string",
|
|
708
|
+
default: "",
|
|
709
|
+
required: true,
|
|
710
|
+
placeholder: "{{$execution.id}}",
|
|
711
|
+
description: "Identificador único da execução/evento (ex.: ID da execução do n8n). Idempotência: reenvio com a mesma chave não duplica.",
|
|
712
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
displayName: "Provedor",
|
|
716
|
+
name: "telProvider",
|
|
717
|
+
type: "string",
|
|
718
|
+
default: "google",
|
|
719
|
+
description: "Provedor do modelo (ex.: 'google'). Usado com o modelo para estimar o custo.",
|
|
720
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
displayName: "Modelo",
|
|
724
|
+
name: "telModel",
|
|
725
|
+
type: "string",
|
|
726
|
+
default: "",
|
|
727
|
+
placeholder: "gemini-2.5-flash",
|
|
728
|
+
description: "Nome do modelo retornado pelo provedor. Casa com a tabela de preços para o custo estimado.",
|
|
729
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
displayName: "Tokens de Entrada",
|
|
733
|
+
name: "telInputTokens",
|
|
734
|
+
type: "number",
|
|
735
|
+
default: 0,
|
|
736
|
+
description: "Tokens de entrada (usage metadata do provedor). 0/vazio = não informado.",
|
|
737
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
displayName: "Tokens de Saída",
|
|
741
|
+
name: "telOutputTokens",
|
|
742
|
+
type: "number",
|
|
743
|
+
default: 0,
|
|
744
|
+
description: "Tokens de saída (usage metadata do provedor). 0/vazio = não informado.",
|
|
745
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
displayName: "Status",
|
|
749
|
+
name: "telStatus",
|
|
750
|
+
type: "options",
|
|
751
|
+
options: [
|
|
752
|
+
{ name: "OK", value: "ok" },
|
|
753
|
+
{ name: "Erro", value: "error" },
|
|
754
|
+
{ name: "Bloqueado (gate)", value: "blocked" },
|
|
755
|
+
],
|
|
756
|
+
default: "ok",
|
|
757
|
+
description: "Desfecho técnico da execução.",
|
|
758
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
displayName: "Duração (ms)",
|
|
762
|
+
name: "telDurationMs",
|
|
763
|
+
type: "number",
|
|
764
|
+
default: 0,
|
|
765
|
+
description: "Duração total da execução em milissegundos. 0/vazio = não informado.",
|
|
766
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
displayName: "Ferramentas Usadas",
|
|
770
|
+
name: "telToolNames",
|
|
771
|
+
type: "string",
|
|
772
|
+
default: "",
|
|
773
|
+
placeholder: "catalogo.resolver, pedido.criar",
|
|
774
|
+
description: "Nomes das ferramentas usadas (vírgula ou JSON array). Só nomes — sem argumentos.",
|
|
775
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
displayName: "Conversa ID",
|
|
779
|
+
name: "telConversationId",
|
|
780
|
+
type: "string",
|
|
781
|
+
default: "",
|
|
782
|
+
description: "Opcional. UUID da conversa (validado contra o tenant; o que não bater é descartado).",
|
|
783
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
displayName: "Pedido ID",
|
|
787
|
+
name: "telPedidoId",
|
|
788
|
+
type: "string",
|
|
789
|
+
default: "",
|
|
790
|
+
description: "Opcional. UUID do pedido vinculado (validado contra o tenant).",
|
|
791
|
+
displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
|
|
792
|
+
},
|
|
532
793
|
],
|
|
533
794
|
};
|
|
534
795
|
}
|
|
@@ -558,6 +819,21 @@ class Nucleo {
|
|
|
558
819
|
path = "/api/v1/agent/catalogo/resolver";
|
|
559
820
|
bodyObj = { consultas };
|
|
560
821
|
}
|
|
822
|
+
else if (resource === "catalogo" && operation === "disponibilidadeRede") {
|
|
823
|
+
const produtoId = this.getNodeParameter("produtoIdRede", i, "").trim();
|
|
824
|
+
const variacaoId = this.getNodeParameter("variacaoIdRede", i, "").trim();
|
|
825
|
+
const consulta = this.getNodeParameter("consultaRede", i, "").trim();
|
|
826
|
+
const b = {};
|
|
827
|
+
if (produtoId)
|
|
828
|
+
b.produto_id = produtoId;
|
|
829
|
+
if (variacaoId)
|
|
830
|
+
b.variacao_id = variacaoId;
|
|
831
|
+
if (consulta)
|
|
832
|
+
b.consulta = consulta;
|
|
833
|
+
method = "POST";
|
|
834
|
+
path = "/api/v1/agent/catalogo/disponibilidade-rede";
|
|
835
|
+
bodyObj = b;
|
|
836
|
+
}
|
|
561
837
|
else if (resource === "pedido" && operation === "detalhar") {
|
|
562
838
|
const ref = this.getNodeParameter("ref", i).trim();
|
|
563
839
|
method = "GET";
|
|
@@ -573,6 +849,8 @@ class Nucleo {
|
|
|
573
849
|
const enderecoEntrega = asObject(this.getNodeParameter("enderecoEntrega", i, "{}"));
|
|
574
850
|
const formaPagamento = this.getNodeParameter("formaPagamento", i, "").trim();
|
|
575
851
|
const trocoPara = Number(this.getNodeParameter("trocoPara", i, 0));
|
|
852
|
+
const taxaEntrega = Number(this.getNodeParameter("taxaEntrega", i, 0));
|
|
853
|
+
const desconto = Number(this.getNodeParameter("desconto", i, 0));
|
|
576
854
|
const unitId = this.getNodeParameter("unitId", i, "").trim();
|
|
577
855
|
idempotencyKey =
|
|
578
856
|
this.getNodeParameter("idempotencyKey", i, "").trim() || (0, node_crypto_1.randomUUID)();
|
|
@@ -594,6 +872,11 @@ class Nucleo {
|
|
|
594
872
|
if (formaPagamento === "dinheiro" && Number.isFinite(trocoPara) && trocoPara > 0) {
|
|
595
873
|
b.troco_para = trocoPara;
|
|
596
874
|
}
|
|
875
|
+
// Taxa/desconto: 0/vazio = não envia (mantém o default 0 do banco).
|
|
876
|
+
if (Number.isFinite(taxaEntrega) && taxaEntrega > 0)
|
|
877
|
+
b.taxa_entrega = taxaEntrega;
|
|
878
|
+
if (Number.isFinite(desconto) && desconto > 0)
|
|
879
|
+
b.desconto = desconto;
|
|
597
880
|
if (unitId)
|
|
598
881
|
b.unit_id = unitId;
|
|
599
882
|
method = "POST";
|
|
@@ -612,6 +895,10 @@ class Nucleo {
|
|
|
612
895
|
const ajustar = asArray(this.getNodeParameter("ajustarItens", i, "[]"));
|
|
613
896
|
const definir = asArray(this.getNodeParameter("definirItens", i, "[]"));
|
|
614
897
|
const removerProdutos = parseIds(this.getNodeParameter("removerProdutoIds", i, ""));
|
|
898
|
+
const formaPagamentoNova = this.getNodeParameter("formaPagamentoNova", i, "").trim();
|
|
899
|
+
const trocoParaNovo = Number(this.getNodeParameter("trocoParaNovo", i, 0));
|
|
900
|
+
const taxaEntregaNova = this.getNodeParameter("taxaEntregaNova", i, "").trim();
|
|
901
|
+
const descontoNovo = this.getNodeParameter("descontoNovo", i, "").trim();
|
|
615
902
|
const b = {};
|
|
616
903
|
if (observacoes.trim())
|
|
617
904
|
b.observacoes = observacoes.trim();
|
|
@@ -634,6 +921,19 @@ class Nucleo {
|
|
|
634
921
|
b.definir_itens = definir;
|
|
635
922
|
if (removerProdutos.length)
|
|
636
923
|
b.remover_produto_ids = removerProdutos;
|
|
924
|
+
// Pagamento: troca a forma do pedido já criado. Troco só com dinheiro e > 0.
|
|
925
|
+
if (formaPagamentoNova)
|
|
926
|
+
b.forma_pagamento = formaPagamentoNova;
|
|
927
|
+
if (formaPagamentoNova === "dinheiro" && Number.isFinite(trocoParaNovo) && trocoParaNovo > 0) {
|
|
928
|
+
b.troco_para = trocoParaNovo;
|
|
929
|
+
}
|
|
930
|
+
// Taxa/desconto (string p/ distinguir "não mexer" de "zerar"): vazio = não envia; "0" zera.
|
|
931
|
+
if (taxaEntregaNova !== "" && Number.isFinite(Number(taxaEntregaNova))) {
|
|
932
|
+
b.taxa_entrega = Number(taxaEntregaNova);
|
|
933
|
+
}
|
|
934
|
+
if (descontoNovo !== "" && Number.isFinite(Number(descontoNovo))) {
|
|
935
|
+
b.desconto = Number(descontoNovo);
|
|
936
|
+
}
|
|
637
937
|
// Idempotência (opcional): mesma chave num retry → backend não reaplica a alteração.
|
|
638
938
|
idempotencyKey =
|
|
639
939
|
this.getNodeParameter("idempotencyKey", i, "").trim() || undefined;
|
|
@@ -655,6 +955,7 @@ class Nucleo {
|
|
|
655
955
|
const resultado = this.getNodeParameter("resultado", i);
|
|
656
956
|
const convTelefone = this.getNodeParameter("convTelefone", i, "").trim();
|
|
657
957
|
const convPedidoId = this.getNodeParameter("convPedidoId", i, "").trim();
|
|
958
|
+
const conversationKey = this.getNodeParameter("conversationKey", i, "").trim();
|
|
658
959
|
const b = { resumo, resultado };
|
|
659
960
|
if (convTelefone)
|
|
660
961
|
b.telefone = convTelefone;
|
|
@@ -662,12 +963,65 @@ class Nucleo {
|
|
|
662
963
|
// Só envia se for UUID bem-formado — lixo nunca chega ao Núcleo.
|
|
663
964
|
if (convPedidoId && UUID_RE.test(convPedidoId))
|
|
664
965
|
b.pedido_id = convPedidoId;
|
|
966
|
+
// conversation_key liga o fechamento à sessão (medição "fora do horário"). Opcional (compat).
|
|
967
|
+
if (conversationKey)
|
|
968
|
+
b.conversation_key = conversationKey;
|
|
665
969
|
idempotencyKey =
|
|
666
970
|
this.getNodeParameter("convIdempotencyKey", i, "").trim() || undefined;
|
|
667
971
|
method = "POST";
|
|
668
972
|
path = "/api/v1/agent/conversa/fechar";
|
|
669
973
|
bodyObj = b;
|
|
670
974
|
}
|
|
975
|
+
else if (resource === "conversa" && operation === "preparar") {
|
|
976
|
+
const conversationKey = this.getNodeParameter("conversationKey", i).trim();
|
|
977
|
+
const canal = this.getNodeParameter("canal", i, "whatsapp").trim();
|
|
978
|
+
const b = { conversation_key: conversationKey };
|
|
979
|
+
if (canal)
|
|
980
|
+
b.canal = canal;
|
|
981
|
+
method = "POST";
|
|
982
|
+
path = "/api/v1/agent/conversa/preparar";
|
|
983
|
+
bodyObj = b;
|
|
984
|
+
}
|
|
985
|
+
else if (resource === "contexto" && operation === "consultar") {
|
|
986
|
+
const data = this.getNodeParameter("data", i).trim();
|
|
987
|
+
method = "POST";
|
|
988
|
+
path = "/api/v1/agent/contexto/consultar";
|
|
989
|
+
bodyObj = { data };
|
|
990
|
+
}
|
|
991
|
+
else if (resource === "telemetria" && operation === "enviar") {
|
|
992
|
+
const eventKey = this.getNodeParameter("telEventKey", i).trim();
|
|
993
|
+
const provider = this.getNodeParameter("telProvider", i, "").trim();
|
|
994
|
+
const model = this.getNodeParameter("telModel", i, "").trim();
|
|
995
|
+
const inputTokens = Number(this.getNodeParameter("telInputTokens", i, 0));
|
|
996
|
+
const outputTokens = Number(this.getNodeParameter("telOutputTokens", i, 0));
|
|
997
|
+
const telStatus = this.getNodeParameter("telStatus", i, "ok").trim();
|
|
998
|
+
const durationMs = Number(this.getNodeParameter("telDurationMs", i, 0));
|
|
999
|
+
const toolNames = parseIds(this.getNodeParameter("telToolNames", i, ""));
|
|
1000
|
+
const conversationId = this.getNodeParameter("telConversationId", i, "").trim();
|
|
1001
|
+
const pedidoId = this.getNodeParameter("telPedidoId", i, "").trim();
|
|
1002
|
+
const b = { event_key: eventKey };
|
|
1003
|
+
if (provider)
|
|
1004
|
+
b.provider = provider;
|
|
1005
|
+
if (model)
|
|
1006
|
+
b.model = model;
|
|
1007
|
+
if (Number.isFinite(inputTokens) && inputTokens > 0)
|
|
1008
|
+
b.input_tokens = inputTokens;
|
|
1009
|
+
if (Number.isFinite(outputTokens) && outputTokens > 0)
|
|
1010
|
+
b.output_tokens = outputTokens;
|
|
1011
|
+
if (telStatus)
|
|
1012
|
+
b.status = telStatus;
|
|
1013
|
+
if (Number.isFinite(durationMs) && durationMs > 0)
|
|
1014
|
+
b.duration_ms = durationMs;
|
|
1015
|
+
if (toolNames.length)
|
|
1016
|
+
b.tool_names = toolNames;
|
|
1017
|
+
if (conversationId && UUID_RE.test(conversationId))
|
|
1018
|
+
b.conversation_id = conversationId;
|
|
1019
|
+
if (pedidoId && UUID_RE.test(pedidoId))
|
|
1020
|
+
b.pedido_id = pedidoId;
|
|
1021
|
+
method = "POST";
|
|
1022
|
+
path = "/api/v1/agent/telemetria";
|
|
1023
|
+
bodyObj = b;
|
|
1024
|
+
}
|
|
671
1025
|
else {
|
|
672
1026
|
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
673
1027
|
message: `Operação não suportada: ${resource}.${operation}`,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wondai/n8n-nodes-nucleo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Node n8n para o Núcleo Wondai — atendimento de IA (cliente, catálogo fuzzy, pedidos) com assinatura HMAC v1. Tenant vem do token (a parede, ADR-013).",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "Node n8n para o Núcleo Wondai — atendimento de IA (gate liga/desliga, cliente, catálogo fuzzy, disponibilidade em rede, pedidos, contexto operacional, telemetria) com assinatura HMAC v1. Tenant vem do token (a parede, ADR-013).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
7
7
|
"n8n",
|