@wondai/n8n-nodes-nucleo 0.2.4 → 0.2.6
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/dist/nodes/Nucleo/Nucleo.node.js +68 -34
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const n8n_workflow_1 = require("n8n-workflow");
|
|
|
15
15
|
* signature = HMAC_SHA256(signingSecret, canônica) em hex.
|
|
16
16
|
*/
|
|
17
17
|
const SIGNATURE_VERSION = "v1";
|
|
18
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
19
|
const HEADERS = {
|
|
19
20
|
key: "x-wondai-key",
|
|
20
21
|
timestamp: "x-wondai-timestamp",
|
|
@@ -351,8 +352,8 @@ class Nucleo {
|
|
|
351
352
|
name: "idempotencyKey",
|
|
352
353
|
type: "string",
|
|
353
354
|
default: "",
|
|
354
|
-
description: "Opcional.
|
|
355
|
-
displayOptions: { show: { resource: ["pedido"], operation: ["criar"] } },
|
|
355
|
+
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.",
|
|
356
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["criar", "alterar", "cancelar"] } },
|
|
356
357
|
},
|
|
357
358
|
// ----------------------------------------------------------------- pedido:alterar / cancelar
|
|
358
359
|
{
|
|
@@ -477,6 +478,14 @@ class Nucleo {
|
|
|
477
478
|
description: "Opcional. UUID de pedido gerado na conversa (validado contra o tenant).",
|
|
478
479
|
displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
|
|
479
480
|
},
|
|
481
|
+
{
|
|
482
|
+
displayName: "Idempotency Key",
|
|
483
|
+
name: "convIdempotencyKey",
|
|
484
|
+
type: "string",
|
|
485
|
+
default: "",
|
|
486
|
+
description: "Opcional. Use a MESMA chave (ex.: messageID do WhatsApp) num retry para não registrar a conversa duas vezes.",
|
|
487
|
+
displayOptions: { show: { resource: ["conversa"], operation: ["registrar"] } },
|
|
488
|
+
},
|
|
480
489
|
],
|
|
481
490
|
};
|
|
482
491
|
}
|
|
@@ -567,6 +576,9 @@ class Nucleo {
|
|
|
567
576
|
b.definir_itens = definir;
|
|
568
577
|
if (removerProdutos.length)
|
|
569
578
|
b.remover_produto_ids = removerProdutos;
|
|
579
|
+
// Idempotência (opcional): mesma chave num retry → backend não reaplica a alteração.
|
|
580
|
+
idempotencyKey =
|
|
581
|
+
this.getNodeParameter("idempotencyKey", i, "").trim() || undefined;
|
|
570
582
|
method = "PATCH";
|
|
571
583
|
path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}`;
|
|
572
584
|
bodyObj = b;
|
|
@@ -574,6 +586,8 @@ class Nucleo {
|
|
|
574
586
|
else if (resource === "pedido" && operation === "cancelar") {
|
|
575
587
|
const pedidoId = this.getNodeParameter("pedidoId", i).trim();
|
|
576
588
|
const motivo = this.getNodeParameter("motivo", i, "").trim();
|
|
589
|
+
idempotencyKey =
|
|
590
|
+
this.getNodeParameter("idempotencyKey", i, "").trim() || undefined;
|
|
577
591
|
method = "POST";
|
|
578
592
|
path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}/cancelar`;
|
|
579
593
|
bodyObj = motivo ? { motivo } : {};
|
|
@@ -586,8 +600,12 @@ class Nucleo {
|
|
|
586
600
|
const b = { resumo, resultado };
|
|
587
601
|
if (convTelefone)
|
|
588
602
|
b.telefone = convTelefone;
|
|
589
|
-
|
|
603
|
+
// pedido_id é opcional e a IA costuma alucinar ("nenhum", texto, UUID stale).
|
|
604
|
+
// Só envia se for UUID bem-formado — lixo nunca chega ao Núcleo.
|
|
605
|
+
if (convPedidoId && UUID_RE.test(convPedidoId))
|
|
590
606
|
b.pedido_id = convPedidoId;
|
|
607
|
+
idempotencyKey =
|
|
608
|
+
this.getNodeParameter("convIdempotencyKey", i, "").trim() || undefined;
|
|
591
609
|
method = "POST";
|
|
592
610
|
path = "/api/v1/agent/conversa/fechar";
|
|
593
611
|
bodyObj = b;
|
|
@@ -598,38 +616,54 @@ class Nucleo {
|
|
|
598
616
|
});
|
|
599
617
|
}
|
|
600
618
|
// ---- assina e envia (HMAC v1) ----
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
619
|
+
const sendSigned = async (rawBody) => {
|
|
620
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
621
|
+
const nonce = (0, node_crypto_1.randomUUID)();
|
|
622
|
+
const canonical = canonicalString(method, path, timestamp, nonce, rawBody);
|
|
623
|
+
const signature = sign(signingSecret, canonical);
|
|
624
|
+
const headers = {
|
|
625
|
+
[HEADERS.key]: tokenId,
|
|
626
|
+
[HEADERS.timestamp]: timestamp,
|
|
627
|
+
[HEADERS.nonce]: nonce,
|
|
628
|
+
[HEADERS.signature]: signature,
|
|
629
|
+
};
|
|
630
|
+
if (rawBody)
|
|
631
|
+
headers["content-type"] = "application/json";
|
|
632
|
+
if (idempotencyKey)
|
|
633
|
+
headers["idempotency-key"] = idempotencyKey;
|
|
634
|
+
const res = await this.helpers.httpRequest({
|
|
635
|
+
method,
|
|
636
|
+
url: baseUrl + path,
|
|
637
|
+
headers,
|
|
638
|
+
body: rawBody || undefined,
|
|
639
|
+
json: false,
|
|
640
|
+
returnFullResponse: true,
|
|
641
|
+
ignoreHttpStatusErrors: true,
|
|
642
|
+
});
|
|
643
|
+
const status = res.statusCode;
|
|
644
|
+
const rawRes = res.body;
|
|
645
|
+
let payload;
|
|
646
|
+
try {
|
|
647
|
+
payload = typeof rawRes === "string" ? JSON.parse(rawRes) : rawRes;
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
payload = { raw: rawRes };
|
|
651
|
+
}
|
|
652
|
+
return { status, payload };
|
|
611
653
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const status = res.statusCode;
|
|
626
|
-
const rawRes = res.body;
|
|
627
|
-
let payload;
|
|
628
|
-
try {
|
|
629
|
-
payload = typeof rawRes === "string" ? JSON.parse(rawRes) : rawRes;
|
|
630
|
-
}
|
|
631
|
-
catch {
|
|
632
|
-
payload = { raw: rawRes };
|
|
654
|
+
let rawBody = bodyObj === undefined ? "" : JSON.stringify(bodyObj);
|
|
655
|
+
let { status, payload } = await sendSigned(rawBody);
|
|
656
|
+
// Registrar conversa nunca deve falhar por causa de um pedido_id inválido
|
|
657
|
+
// (a IA pode mandar um UUID que não existe no tenant). Se o Núcleo recusar
|
|
658
|
+
// o pedido, reenvia sem ele — a conversa ainda é registrada.
|
|
659
|
+
if (status === 404 &&
|
|
660
|
+
resource === "conversa" &&
|
|
661
|
+
operation === "registrar" &&
|
|
662
|
+
bodyObj &&
|
|
663
|
+
bodyObj.pedido_id) {
|
|
664
|
+
delete bodyObj.pedido_id;
|
|
665
|
+
rawBody = JSON.stringify(bodyObj);
|
|
666
|
+
({ status, payload } = await sendSigned(rawBody));
|
|
633
667
|
}
|
|
634
668
|
if (status >= 400) {
|
|
635
669
|
const msg = (payload && payload.error) || `Núcleo respondeu HTTP ${status}.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wondai/n8n-nodes-nucleo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
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).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|