@wondai/n8n-nodes-nucleo 0.2.9 → 0.5.0

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 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) e criar/alterar/cancelar pedido. Assinatura HMAC; o tenant vem do token.",
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
  },
@@ -223,6 +234,12 @@ class Nucleo {
223
234
  action: "Cancelar pedido",
224
235
  description: "Cancela o pedido inteiro (idempotente)",
225
236
  },
237
+ {
238
+ name: "Anexar Imagem",
239
+ value: "anexar",
240
+ action: "Anexar foto-referência ao pedido",
241
+ description: "Anexa a FOTO que o cliente mandou no WhatsApp (a decoração que ele quer) ao pedido — a imagem literal, não uma descrição. Recebe base64 (data-URL ou puro + mime). Idempotente pelo messageID.",
242
+ },
226
243
  ],
227
244
  default: "criar",
228
245
  },
@@ -233,6 +250,12 @@ class Nucleo {
233
250
  noDataExpression: true,
234
251
  displayOptions: { show: { resource: ["conversa"] } },
235
252
  options: [
253
+ {
254
+ name: "Preparar",
255
+ value: "preparar",
256
+ action: "Preparar conversa (gate + contexto)",
257
+ 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",
258
+ },
236
259
  {
237
260
  name: "Registrar",
238
261
  value: "registrar",
@@ -240,7 +263,41 @@ class Nucleo {
240
263
  description: "Encerra a conversa com resumo + resultado (handoff = precisa_humano)",
241
264
  },
242
265
  ],
243
- default: "registrar",
266
+ default: "preparar",
267
+ },
268
+ {
269
+ displayName: "Operação",
270
+ name: "operation",
271
+ type: "options",
272
+ noDataExpression: true,
273
+ displayOptions: { show: { resource: ["contexto"] } },
274
+ options: [
275
+ {
276
+ name: "Consultar Data",
277
+ value: "consultar",
278
+ action: "Consultar contexto de uma data",
279
+ description: "Horário/exceção/regra vigente para UMA data (ex.: 'abrem no feriado?')",
280
+ },
281
+ ],
282
+ default: "consultar",
283
+ },
284
+ {
285
+ displayName: "Operação",
286
+ name: "operation",
287
+ type: "options",
288
+ noDataExpression: true,
289
+ displayOptions: { show: { resource: ["telemetria"] } },
290
+ options: [
291
+ {
292
+ name: "Enviar",
293
+ value: "enviar",
294
+ action: "Enviar telemetria da execução",
295
+ description: "Envia métricas TÉCNICAS da execução (tokens/custo estimado/duração/ferramentas) DEPOIS da " +
296
+ "resposta. NÃO use como tool do AI Agent nem antes do gate. Sem prompt/resposta/PII. " +
297
+ "Idempotente por 'Chave do Evento' — reenvio não duplica. Falha aqui não repete a resposta.",
298
+ },
299
+ ],
300
+ default: "enviar",
244
301
  },
245
302
  // ----------------------------------------------------------------- cliente:buscar
246
303
  {
@@ -265,6 +322,33 @@ class Nucleo {
265
322
  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
323
  displayOptions: { show: { resource: ["catalogo"], operation: ["resolver"] } },
267
324
  },
325
+ // ----------------------------------------------------------------- catalogo:disponibilidadeRede
326
+ {
327
+ displayName: "Produto (ID)",
328
+ name: "produtoIdRede",
329
+ type: "string",
330
+ default: "",
331
+ placeholder: "UUID do produto já resolvido",
332
+ 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.",
333
+ displayOptions: { show: { resource: ["catalogo"], operation: ["disponibilidadeRede"] } },
334
+ },
335
+ {
336
+ displayName: "Variação (ID, opcional)",
337
+ name: "variacaoIdRede",
338
+ type: "string",
339
+ default: "",
340
+ description: "ID da variação (UUID), opcional. A rede resolve por produto.",
341
+ displayOptions: { show: { resource: ["catalogo"], operation: ["disponibilidadeRede"] } },
342
+ },
343
+ {
344
+ displayName: "Consulta (opcional)",
345
+ name: "consultaRede",
346
+ type: "string",
347
+ default: "",
348
+ placeholder: "pão de queijo",
349
+ description: "Texto curto do produto, usado SÓ se o ID não for informado. Se ambíguo, nada é retornado — prefira resolver o produto antes.",
350
+ displayOptions: { show: { resource: ["catalogo"], operation: ["disponibilidadeRede"] } },
351
+ },
268
352
  // ----------------------------------------------------------------- pedido:detalhar
269
353
  {
270
354
  displayName: "Pedido (ref)",
@@ -382,7 +466,7 @@ class Nucleo {
382
466
  name: "unitId",
383
467
  type: "string",
384
468
  default: "",
385
- description: "Opcional. Vazio = primeira loja da padaria.",
469
+ 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).",
386
470
  displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
387
471
  },
388
472
  {
@@ -391,9 +475,11 @@ class Nucleo {
391
475
  type: "string",
392
476
  default: "",
393
477
  description: "Opcional. Para criar: vazio = gerada automaticamente. Para alterar/cancelar: use a MESMA chave (ex.: messageID do WhatsApp) num retry para não reaplicar a operação.",
394
- displayOptions: { show: { resource: ["pedido"], operation: ["criar", "alterar", "cancelar"] } },
478
+ displayOptions: {
479
+ show: { resource: ["pedido"], operation: ["criar", "alterar", "cancelar", "anexar"] },
480
+ },
395
481
  },
396
- // ----------------------------------------------------------------- pedido:alterar / cancelar
482
+ // ----------------------------------------------------------------- pedido:alterar / cancelar / anexar
397
483
  {
398
484
  displayName: "Pedido ID",
399
485
  name: "pedidoId",
@@ -401,7 +487,7 @@ class Nucleo {
401
487
  default: "",
402
488
  required: true,
403
489
  description: "Id do pedido (do tenant). Pedido de outra padaria → 404.",
404
- displayOptions: { show: { resource: ["pedido"], operation: ["alterar", "cancelar"] } },
490
+ displayOptions: { show: { resource: ["pedido"], operation: ["alterar", "cancelar", "anexar"] } },
405
491
  },
406
492
  {
407
493
  displayName: "Observações",
@@ -534,6 +620,39 @@ class Nucleo {
534
620
  description: "Motivo do cancelamento (opcional).",
535
621
  displayOptions: { show: { resource: ["pedido"], operation: ["cancelar"] } },
536
622
  },
623
+ // ----------------------------------------------------------------- pedido:anexar
624
+ {
625
+ displayName: "Imagem (base64)",
626
+ name: "imagemBase64",
627
+ type: "string",
628
+ typeOptions: { rows: 2 },
629
+ default: "",
630
+ required: true,
631
+ description: "A imagem que o cliente mandou, em base64. Aceita data-URL (data:image/jpeg;base64,...) — de onde o mime é lido — OU base64 puro (então preencha 'MIME'). Limite 5 MB; PNG/JPG/WEBP.",
632
+ displayOptions: { show: { resource: ["pedido"], operation: ["anexar"] } },
633
+ },
634
+ {
635
+ displayName: "MIME (se base64 puro)",
636
+ name: "imagemMime",
637
+ type: "options",
638
+ options: [
639
+ { name: "(data-URL traz o mime)", value: "" },
640
+ { name: "image/jpeg", value: "image/jpeg" },
641
+ { name: "image/png", value: "image/png" },
642
+ { name: "image/webp", value: "image/webp" },
643
+ ],
644
+ default: "",
645
+ description: "Obrigatório só quando a imagem for base64 PURO (sem o prefixo data:). Com data-URL, deixe vazio.",
646
+ displayOptions: { show: { resource: ["pedido"], operation: ["anexar"] } },
647
+ },
648
+ {
649
+ displayName: "Legenda",
650
+ name: "imagemLegenda",
651
+ type: "string",
652
+ default: "",
653
+ description: "Opcional. Texto curto sobre a foto (ex.: a legenda que veio com a imagem).",
654
+ displayOptions: { show: { resource: ["pedido"], operation: ["anexar"] } },
655
+ },
537
656
  // ----------------------------------------------------------------- conversa:registrar
538
657
  {
539
658
  displayName: "Resumo",
@@ -583,6 +702,135 @@ class Nucleo {
583
702
  description: "Opcional. Use a MESMA chave (ex.: messageID do WhatsApp) num retry para não registrar a conversa duas vezes.",
584
703
  displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
585
704
  },
705
+ {
706
+ displayName: "Chave da Conversa (conversation_key)",
707
+ name: "conversationKey",
708
+ type: "string",
709
+ default: "",
710
+ placeholder: "+5511999999999",
711
+ 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).",
712
+ displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
713
+ },
714
+ // ----------------------------------------------------------------- conversa:preparar
715
+ {
716
+ displayName: "Chave da Conversa (conversation_key)",
717
+ name: "conversationKey",
718
+ type: "string",
719
+ default: "",
720
+ required: true,
721
+ placeholder: "+5511999999999",
722
+ description: "Identificador externo do atendimento (geralmente o telefone/remoteJid do WhatsApp). Entra só como HASH no servidor — nunca é gravado cru.",
723
+ displayOptions: { show: { resource: ["conversa"], operation: ["preparar"] } },
724
+ },
725
+ {
726
+ displayName: "Canal",
727
+ name: "canal",
728
+ type: "string",
729
+ default: "whatsapp",
730
+ description: "Canal do atendimento (default 'whatsapp').",
731
+ displayOptions: { show: { resource: ["conversa"], operation: ["preparar"] } },
732
+ },
733
+ // ----------------------------------------------------------------- contexto:consultar
734
+ {
735
+ displayName: "Data",
736
+ name: "data",
737
+ type: "string",
738
+ default: "",
739
+ required: true,
740
+ placeholder: "2026-12-25",
741
+ description: "UMA data no formato YYYY-MM-DD (janela ±366 dias). Devolve horário/exceção/regra vigente para a data.",
742
+ displayOptions: { show: { resource: ["contexto"], operation: ["consultar"] } },
743
+ },
744
+ // ----------------------------------------------------------------- telemetria:enviar
745
+ {
746
+ displayName: "Chave do Evento (event_key)",
747
+ name: "telEventKey",
748
+ type: "string",
749
+ default: "",
750
+ required: true,
751
+ placeholder: "{{$execution.id}}",
752
+ description: "Identificador único da execução/evento (ex.: ID da execução do n8n). Idempotência: reenvio com a mesma chave não duplica.",
753
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
754
+ },
755
+ {
756
+ displayName: "Provedor",
757
+ name: "telProvider",
758
+ type: "string",
759
+ default: "google",
760
+ description: "Provedor do modelo (ex.: 'google'). Usado com o modelo para estimar o custo.",
761
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
762
+ },
763
+ {
764
+ displayName: "Modelo",
765
+ name: "telModel",
766
+ type: "string",
767
+ default: "",
768
+ placeholder: "gemini-2.5-flash",
769
+ description: "Nome do modelo retornado pelo provedor. Casa com a tabela de preços para o custo estimado.",
770
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
771
+ },
772
+ {
773
+ displayName: "Tokens de Entrada",
774
+ name: "telInputTokens",
775
+ type: "number",
776
+ default: 0,
777
+ description: "Tokens de entrada (usage metadata do provedor). 0/vazio = não informado.",
778
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
779
+ },
780
+ {
781
+ displayName: "Tokens de Saída",
782
+ name: "telOutputTokens",
783
+ type: "number",
784
+ default: 0,
785
+ description: "Tokens de saída (usage metadata do provedor). 0/vazio = não informado.",
786
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
787
+ },
788
+ {
789
+ displayName: "Status",
790
+ name: "telStatus",
791
+ type: "options",
792
+ options: [
793
+ { name: "OK", value: "ok" },
794
+ { name: "Erro", value: "error" },
795
+ { name: "Bloqueado (gate)", value: "blocked" },
796
+ ],
797
+ default: "ok",
798
+ description: "Desfecho técnico da execução.",
799
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
800
+ },
801
+ {
802
+ displayName: "Duração (ms)",
803
+ name: "telDurationMs",
804
+ type: "number",
805
+ default: 0,
806
+ description: "Duração total da execução em milissegundos. 0/vazio = não informado.",
807
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
808
+ },
809
+ {
810
+ displayName: "Ferramentas Usadas",
811
+ name: "telToolNames",
812
+ type: "string",
813
+ default: "",
814
+ placeholder: "catalogo.resolver, pedido.criar",
815
+ description: "Nomes das ferramentas usadas (vírgula ou JSON array). Só nomes — sem argumentos.",
816
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
817
+ },
818
+ {
819
+ displayName: "Conversa ID",
820
+ name: "telConversationId",
821
+ type: "string",
822
+ default: "",
823
+ description: "Opcional. UUID da conversa (validado contra o tenant; o que não bater é descartado).",
824
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
825
+ },
826
+ {
827
+ displayName: "Pedido ID",
828
+ name: "telPedidoId",
829
+ type: "string",
830
+ default: "",
831
+ description: "Opcional. UUID do pedido vinculado (validado contra o tenant).",
832
+ displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
833
+ },
586
834
  ],
587
835
  };
588
836
  }
@@ -612,6 +860,21 @@ class Nucleo {
612
860
  path = "/api/v1/agent/catalogo/resolver";
613
861
  bodyObj = { consultas };
614
862
  }
863
+ else if (resource === "catalogo" && operation === "disponibilidadeRede") {
864
+ const produtoId = this.getNodeParameter("produtoIdRede", i, "").trim();
865
+ const variacaoId = this.getNodeParameter("variacaoIdRede", i, "").trim();
866
+ const consulta = this.getNodeParameter("consultaRede", i, "").trim();
867
+ const b = {};
868
+ if (produtoId)
869
+ b.produto_id = produtoId;
870
+ if (variacaoId)
871
+ b.variacao_id = variacaoId;
872
+ if (consulta)
873
+ b.consulta = consulta;
874
+ method = "POST";
875
+ path = "/api/v1/agent/catalogo/disponibilidade-rede";
876
+ bodyObj = b;
877
+ }
615
878
  else if (resource === "pedido" && operation === "detalhar") {
616
879
  const ref = this.getNodeParameter("ref", i).trim();
617
880
  method = "GET";
@@ -728,11 +991,30 @@ class Nucleo {
728
991
  path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}/cancelar`;
729
992
  bodyObj = motivo ? { motivo } : {};
730
993
  }
994
+ else if (resource === "pedido" && operation === "anexar") {
995
+ const pedidoId = this.getNodeParameter("pedidoId", i).trim();
996
+ const imagemBase64 = this.getNodeParameter("imagemBase64", i).trim();
997
+ const mime = this.getNodeParameter("imagemMime", i, "").trim();
998
+ const legenda = this.getNodeParameter("imagemLegenda", i, "").trim();
999
+ const b = { imagem_base64: imagemBase64 };
1000
+ // mime só é necessário quando a imagem é base64 PURO; data-URL já carrega o mime.
1001
+ if (mime && !imagemBase64.startsWith("data:"))
1002
+ b.mime = mime;
1003
+ if (legenda)
1004
+ b.legenda = legenda;
1005
+ // Idempotência: mesma chave (messageID do WhatsApp) num retry → não duplica a foto.
1006
+ idempotencyKey =
1007
+ this.getNodeParameter("idempotencyKey", i, "").trim() || undefined;
1008
+ method = "POST";
1009
+ path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}/anexo`;
1010
+ bodyObj = b;
1011
+ }
731
1012
  else if (resource === "conversa" && operation === "registrar") {
732
1013
  const resumo = this.getNodeParameter("resumo", i).trim();
733
1014
  const resultado = this.getNodeParameter("resultado", i);
734
1015
  const convTelefone = this.getNodeParameter("convTelefone", i, "").trim();
735
1016
  const convPedidoId = this.getNodeParameter("convPedidoId", i, "").trim();
1017
+ const conversationKey = this.getNodeParameter("conversationKey", i, "").trim();
736
1018
  const b = { resumo, resultado };
737
1019
  if (convTelefone)
738
1020
  b.telefone = convTelefone;
@@ -740,12 +1022,65 @@ class Nucleo {
740
1022
  // Só envia se for UUID bem-formado — lixo nunca chega ao Núcleo.
741
1023
  if (convPedidoId && UUID_RE.test(convPedidoId))
742
1024
  b.pedido_id = convPedidoId;
1025
+ // conversation_key liga o fechamento à sessão (medição "fora do horário"). Opcional (compat).
1026
+ if (conversationKey)
1027
+ b.conversation_key = conversationKey;
743
1028
  idempotencyKey =
744
1029
  this.getNodeParameter("convIdempotencyKey", i, "").trim() || undefined;
745
1030
  method = "POST";
746
1031
  path = "/api/v1/agent/conversa/fechar";
747
1032
  bodyObj = b;
748
1033
  }
1034
+ else if (resource === "conversa" && operation === "preparar") {
1035
+ const conversationKey = this.getNodeParameter("conversationKey", i).trim();
1036
+ const canal = this.getNodeParameter("canal", i, "whatsapp").trim();
1037
+ const b = { conversation_key: conversationKey };
1038
+ if (canal)
1039
+ b.canal = canal;
1040
+ method = "POST";
1041
+ path = "/api/v1/agent/conversa/preparar";
1042
+ bodyObj = b;
1043
+ }
1044
+ else if (resource === "contexto" && operation === "consultar") {
1045
+ const data = this.getNodeParameter("data", i).trim();
1046
+ method = "POST";
1047
+ path = "/api/v1/agent/contexto/consultar";
1048
+ bodyObj = { data };
1049
+ }
1050
+ else if (resource === "telemetria" && operation === "enviar") {
1051
+ const eventKey = this.getNodeParameter("telEventKey", i).trim();
1052
+ const provider = this.getNodeParameter("telProvider", i, "").trim();
1053
+ const model = this.getNodeParameter("telModel", i, "").trim();
1054
+ const inputTokens = Number(this.getNodeParameter("telInputTokens", i, 0));
1055
+ const outputTokens = Number(this.getNodeParameter("telOutputTokens", i, 0));
1056
+ const telStatus = this.getNodeParameter("telStatus", i, "ok").trim();
1057
+ const durationMs = Number(this.getNodeParameter("telDurationMs", i, 0));
1058
+ const toolNames = parseIds(this.getNodeParameter("telToolNames", i, ""));
1059
+ const conversationId = this.getNodeParameter("telConversationId", i, "").trim();
1060
+ const pedidoId = this.getNodeParameter("telPedidoId", i, "").trim();
1061
+ const b = { event_key: eventKey };
1062
+ if (provider)
1063
+ b.provider = provider;
1064
+ if (model)
1065
+ b.model = model;
1066
+ if (Number.isFinite(inputTokens) && inputTokens > 0)
1067
+ b.input_tokens = inputTokens;
1068
+ if (Number.isFinite(outputTokens) && outputTokens > 0)
1069
+ b.output_tokens = outputTokens;
1070
+ if (telStatus)
1071
+ b.status = telStatus;
1072
+ if (Number.isFinite(durationMs) && durationMs > 0)
1073
+ b.duration_ms = durationMs;
1074
+ if (toolNames.length)
1075
+ b.tool_names = toolNames;
1076
+ if (conversationId && UUID_RE.test(conversationId))
1077
+ b.conversation_id = conversationId;
1078
+ if (pedidoId && UUID_RE.test(pedidoId))
1079
+ b.pedido_id = pedidoId;
1080
+ method = "POST";
1081
+ path = "/api/v1/agent/telemetria";
1082
+ bodyObj = b;
1083
+ }
749
1084
  else {
750
1085
  throw new n8n_workflow_1.NodeApiError(this.getNode(), {
751
1086
  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.2.9",
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.5.0",
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",