@wondai/n8n-nodes-nucleo 0.6.5 → 0.8.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.
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Nucleo = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
6
|
+
const quote_contract_1 = require("./quote-contract");
|
|
6
7
|
/**
|
|
7
8
|
* Node Núcleo Wondai — satélite de IA → Núcleo (ADR-003/013/015/016).
|
|
8
9
|
*
|
|
@@ -63,8 +64,11 @@ function parseConsultas(raw, max = 10) {
|
|
|
63
64
|
continue;
|
|
64
65
|
seen.add(k);
|
|
65
66
|
out.push(s);
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
}
|
|
68
|
+
// Plano 030: rejeicao EXPLICITA acima do limite. O break silencioso descartava consultas
|
|
69
|
+
// (incidente 65144: 4 consultas perdidas), produzindo resultado parcial com aparencia de sucesso.
|
|
70
|
+
if (out.length > max) {
|
|
71
|
+
throw new Error(`Catalogo Resolver: ${out.length} consultas acima do limite de ${max}. Envie no maximo ${max} consultas de PRODUTO por chamada; adicionais e opcoes ja resolvidos nao sao novas consultas.`);
|
|
68
72
|
}
|
|
69
73
|
return out;
|
|
70
74
|
}
|
|
@@ -268,9 +272,96 @@ class Nucleo {
|
|
|
268
272
|
action: "Anexar foto-referência ao pedido",
|
|
269
273
|
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.",
|
|
270
274
|
},
|
|
275
|
+
{
|
|
276
|
+
name: "Cotar",
|
|
277
|
+
value: "cotar",
|
|
278
|
+
action: "Cotar pedido (cotação canônica)",
|
|
279
|
+
description: "Gera a COTAÇÃO CANÔNICA do Núcleo (preço/quantidade/config oficiais; nada de texto) e a persiste como pending. Use os mesmos itens estruturados de Criar.",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "Validar Cotação (Dry-Run)",
|
|
283
|
+
value: "validarCotacao",
|
|
284
|
+
action: "Validar cotação sem escrever (shadow)",
|
|
285
|
+
description: "Roda o preparo/precificação e devolve o mesmo shape da cotação com dry_run:true, SEM escrever nada.",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "Marcar Cotação Apresentada",
|
|
289
|
+
value: "apresentarCotacao",
|
|
290
|
+
action: "Marcar cotação apresentada",
|
|
291
|
+
description: "pending -> presented. Chamar SÓ no ramo de sucesso do envio público. Requer cotacao_id e cotacao_version.",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "Consultar Cotação Ativa",
|
|
295
|
+
value: "consultarCotacaoAtiva",
|
|
296
|
+
action: "Consultar cotação apresentada ativa",
|
|
297
|
+
description: "Devolve a cotação presented ativa do cliente (por telefone). Sem cotação apresentada => 404.",
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "Confirmar Cotação",
|
|
301
|
+
value: "confirmarCotacao",
|
|
302
|
+
action: "Confirmar cotação (cria 1 pedido)",
|
|
303
|
+
description: "Revalida a cotação por fingerprint e cria exatamente UM pedido (transacional). Divergência => 409 e novo resumo. Requer cotacao_id, cotacao_version e cliente_telefone.",
|
|
304
|
+
},
|
|
271
305
|
],
|
|
272
306
|
default: "criar",
|
|
273
307
|
},
|
|
308
|
+
{
|
|
309
|
+
displayName: "Cotação (Corpo JSON)",
|
|
310
|
+
name: "cotacaoBody",
|
|
311
|
+
type: "json",
|
|
312
|
+
default: "{}",
|
|
313
|
+
description: "Corpo da cotação (mesmo formato de Criar): telefone, itens[], tipo_entrega, endereco_entrega, agendado_para, forma_pagamento, taxa_entrega, desconto, cobrancas[], observacoes.",
|
|
314
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["cotar", "validarCotacao"] } },
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
displayName: "Cotação ID",
|
|
318
|
+
name: "cotacaoId",
|
|
319
|
+
type: "string",
|
|
320
|
+
default: "",
|
|
321
|
+
description: "cotacao_id retornado por Cotar (vindo por expressão do workflow, nunca da LLM).",
|
|
322
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["apresentarCotacao", "confirmarCotacao"] } },
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
displayName: "Cotação Versão",
|
|
326
|
+
name: "cotacaoVersion",
|
|
327
|
+
type: "number",
|
|
328
|
+
default: 1,
|
|
329
|
+
description: "cotacao_version retornado por Cotar.",
|
|
330
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["apresentarCotacao", "confirmarCotacao"] } },
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
displayName: "Message ID Apresentado",
|
|
334
|
+
name: "cotacaoMessageId",
|
|
335
|
+
type: "string",
|
|
336
|
+
default: "",
|
|
337
|
+
description: "ID da mensagem enviada (opcional), para auditoria da apresentação.",
|
|
338
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["apresentarCotacao"] } },
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
displayName: "Message ID da Cotação",
|
|
342
|
+
name: "cotacaoRequestMessageId",
|
|
343
|
+
type: "string",
|
|
344
|
+
default: "",
|
|
345
|
+
required: true,
|
|
346
|
+
description: "messageID real do turno. O package combina este valor com o hash do payload para gerar a Idempotency-Key.",
|
|
347
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["cotar"] } },
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
displayName: "Telefone do Cliente",
|
|
351
|
+
name: "cotacaoTelefone",
|
|
352
|
+
type: "string",
|
|
353
|
+
default: "",
|
|
354
|
+
description: "Telefone normalizado, injetado por expressão do workflow.",
|
|
355
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["consultarCotacaoAtiva", "confirmarCotacao"] } },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
displayName: "Nome do Cliente",
|
|
359
|
+
name: "cotacaoNome",
|
|
360
|
+
type: "string",
|
|
361
|
+
default: "",
|
|
362
|
+
description: "Nome do cliente (opcional).",
|
|
363
|
+
displayOptions: { show: { resource: ["pedido"], operation: ["confirmarCotacao"] } },
|
|
364
|
+
},
|
|
274
365
|
{
|
|
275
366
|
displayName: "Operação",
|
|
276
367
|
name: "operation",
|
|
@@ -1446,6 +1537,53 @@ class Nucleo {
|
|
|
1446
1537
|
path = `/api/v1/agent/pedido/${encodeURIComponent(pedidoId)}/anexo`;
|
|
1447
1538
|
bodyObj = b;
|
|
1448
1539
|
}
|
|
1540
|
+
else if (resource === "pedido" && operation === "cotar") {
|
|
1541
|
+
const body = asObject(this.getNodeParameter("cotacaoBody", i, "{}")) || {};
|
|
1542
|
+
(0, quote_contract_1.assertQuoteContract)(body);
|
|
1543
|
+
const messageId = this.getNodeParameter("cotacaoRequestMessageId", i).trim();
|
|
1544
|
+
idempotencyKey = (0, quote_contract_1.buildQuoteIdempotencyKey)(tokenId, messageId, body);
|
|
1545
|
+
method = "POST";
|
|
1546
|
+
path = "/api/v1/agent/pedido/cotacao";
|
|
1547
|
+
bodyObj = body;
|
|
1548
|
+
}
|
|
1549
|
+
else if (resource === "pedido" && operation === "validarCotacao") {
|
|
1550
|
+
const body = asObject(this.getNodeParameter("cotacaoBody", i, "{}")) || {};
|
|
1551
|
+
(0, quote_contract_1.assertQuoteContract)(body);
|
|
1552
|
+
method = "POST";
|
|
1553
|
+
path = "/api/v1/agent/pedido/cotacao/validar";
|
|
1554
|
+
bodyObj = body;
|
|
1555
|
+
}
|
|
1556
|
+
else if (resource === "pedido" && operation === "apresentarCotacao") {
|
|
1557
|
+
const cotacaoId = this.getNodeParameter("cotacaoId", i).trim();
|
|
1558
|
+
const version = Number(this.getNodeParameter("cotacaoVersion", i));
|
|
1559
|
+
const messageId = this.getNodeParameter("cotacaoMessageId", i, "").trim();
|
|
1560
|
+
const b = { cotacao_version: version };
|
|
1561
|
+
if (messageId)
|
|
1562
|
+
b.presented_message_id = messageId;
|
|
1563
|
+
method = "POST";
|
|
1564
|
+
path = `/api/v1/agent/pedido/cotacao/${encodeURIComponent(cotacaoId)}/apresentar`;
|
|
1565
|
+
bodyObj = b;
|
|
1566
|
+
}
|
|
1567
|
+
else if (resource === "pedido" && operation === "consultarCotacaoAtiva") {
|
|
1568
|
+
const telefone = this.getNodeParameter("cotacaoTelefone", i).trim();
|
|
1569
|
+
method = "POST";
|
|
1570
|
+
path = "/api/v1/agent/pedido/cotacao/ativa";
|
|
1571
|
+
bodyObj = { telefone };
|
|
1572
|
+
}
|
|
1573
|
+
else if (resource === "pedido" && operation === "confirmarCotacao") {
|
|
1574
|
+
const cotacaoId = this.getNodeParameter("cotacaoId", i).trim();
|
|
1575
|
+
const version = Number(this.getNodeParameter("cotacaoVersion", i));
|
|
1576
|
+
const telefone = this.getNodeParameter("cotacaoTelefone", i).trim();
|
|
1577
|
+
const nome = this.getNodeParameter("cotacaoNome", i, "").trim();
|
|
1578
|
+
const b = { cotacao_version: version, cliente_telefone: telefone };
|
|
1579
|
+
if (nome)
|
|
1580
|
+
b.cliente_nome = nome;
|
|
1581
|
+
// Idempotência DETERMINÍSTICA montada pelo package (nunca pela LLM), Plano 030 §9.5.
|
|
1582
|
+
idempotencyKey = `quote-confirm:${cotacaoId}:${version}`;
|
|
1583
|
+
method = "POST";
|
|
1584
|
+
path = `/api/v1/agent/pedido/cotacao/${encodeURIComponent(cotacaoId)}/confirmar`;
|
|
1585
|
+
bodyObj = b;
|
|
1586
|
+
}
|
|
1449
1587
|
else if (resource === "conversa" && operation === "registrar") {
|
|
1450
1588
|
const resumo = this.getNodeParameter("resumo", i).trim();
|
|
1451
1589
|
const resultado = this.getNodeParameter("resultado", i);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IDataObject } from "n8n-workflow";
|
|
2
|
+
export declare function canonicalQuotePayload(payload: IDataObject): string;
|
|
3
|
+
export declare function buildQuoteIdempotencyKey(tokenId: string, messageId: string, payload: IDataObject): string;
|
|
4
|
+
export declare function assertQuoteContract(payload: IDataObject): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonicalQuotePayload = canonicalQuotePayload;
|
|
4
|
+
exports.buildQuoteIdempotencyKey = buildQuoteIdempotencyKey;
|
|
5
|
+
exports.assertQuoteContract = assertQuoteContract;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
function canonicalize(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.map(canonicalize);
|
|
10
|
+
if (!value || typeof value !== "object") {
|
|
11
|
+
return typeof value === "string" ? value.normalize("NFC") : value;
|
|
12
|
+
}
|
|
13
|
+
return Object.fromEntries(Object.entries(value)
|
|
14
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
15
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
16
|
+
}
|
|
17
|
+
function canonicalQuotePayload(payload) {
|
|
18
|
+
return JSON.stringify(canonicalize(payload));
|
|
19
|
+
}
|
|
20
|
+
function buildQuoteIdempotencyKey(tokenId, messageId, payload) {
|
|
21
|
+
const message = messageId.trim();
|
|
22
|
+
if (!message)
|
|
23
|
+
throw new Error("Cotar pedido: messageID obrigatorio.");
|
|
24
|
+
const hash = (0, node_crypto_1.createHash)("sha256")
|
|
25
|
+
.update(canonicalQuotePayload(payload), "utf8")
|
|
26
|
+
.digest("hex")
|
|
27
|
+
.slice(0, 16);
|
|
28
|
+
return `quote:${tokenId}:${message}:${hash}`;
|
|
29
|
+
}
|
|
30
|
+
function assertQuoteContract(payload) {
|
|
31
|
+
const items = payload.itens;
|
|
32
|
+
if (!Array.isArray(items) || items.length === 0 || items.length > 10) {
|
|
33
|
+
throw new Error("Cotacao: itens precisa conter entre 1 e 10 linhas.");
|
|
34
|
+
}
|
|
35
|
+
for (const [index, raw] of items.entries()) {
|
|
36
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
37
|
+
throw new Error(`Cotacao: itens[${index}] invalido.`);
|
|
38
|
+
}
|
|
39
|
+
const item = raw;
|
|
40
|
+
const informed = item.quantidade_informada;
|
|
41
|
+
if (!String(item.produto_id ?? "").trim() ||
|
|
42
|
+
!String(item.consulta_original ?? "").trim() ||
|
|
43
|
+
!String(item.quantidade_original ?? "").trim() ||
|
|
44
|
+
!informed ||
|
|
45
|
+
!Number.isFinite(Number(informed.valor)) ||
|
|
46
|
+
Number(informed.valor) <= 0 ||
|
|
47
|
+
!String(informed.unidade ?? "").trim()) {
|
|
48
|
+
throw new Error(`Cotacao: itens[${index}] exige produto_id, consulta_original, quantidade_original e quantidade_informada.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const charges = Array.isArray(payload.cobrancas) ? payload.cobrancas : [];
|
|
52
|
+
for (const [index, raw] of charges.entries()) {
|
|
53
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
54
|
+
continue;
|
|
55
|
+
const charge = raw;
|
|
56
|
+
if (charge.tipo !== "paid_addon")
|
|
57
|
+
continue;
|
|
58
|
+
if (!String(charge.regra_chave ?? "").trim() ||
|
|
59
|
+
!String(charge.operational_config_revision_id ?? "").trim()) {
|
|
60
|
+
throw new Error(`Cotacao: cobrancas[${index}] paid_addon exige regra_chave e operational_config_revision_id.`);
|
|
61
|
+
}
|
|
62
|
+
if (charge.nome != null || charge.valor != null) {
|
|
63
|
+
throw new Error(`Cotacao: cobrancas[${index}] nao pode enviar nome ou valor; o Nucleo resolve pela revisao.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wondai/n8n-nodes-nucleo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Node n8n para o Núcleo Wondai — atendimento de IA multi-vertical (gate liga/desliga, cliente/paciente, catálogo fuzzy, pedidos [padaria], procedimento/agenda/agendamento [odonto], contexto operacional, telemetria) com assinatura HMAC v1. Tenant e vertical vêm do token (a parede, ADR-013/038).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "node scripts/build.mjs",
|
|
27
27
|
"dev": "tsc --watch",
|
|
28
|
+
"test": "node scripts/test.mjs",
|
|
28
29
|
"prepublishOnly": "node scripts/build.mjs"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|