n8n-nodes-aib 0.5.0 → 0.6.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.
|
@@ -28,18 +28,23 @@ class AibDigitalApi {
|
|
|
28
28
|
description: "Gerada em Integrações → Chaves de API. Use uma chave de agência para escolher a conta dentro do nó.",
|
|
29
29
|
},
|
|
30
30
|
];
|
|
31
|
-
authenticate
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
// Sem `authenticate` de propósito. O n8n injeta um "Custom API Call" no
|
|
32
|
+
// fim de toda lista chamada `resource` ou `operation` quando a credencial
|
|
33
|
+
// define esse campo — a regra é literal, sem como desligar:
|
|
34
|
+
//
|
|
35
|
+
// if (credType.authenticate !== undefined) return true;
|
|
36
|
+
// — load-nodes-and-credentials.js, supportsProxyAuth()
|
|
37
|
+
//
|
|
38
|
+
// A opção caía no meio das operações de verdade, e quem a escolhesse via
|
|
39
|
+
// o n8n esconder todos os outros campos do nó. O nó nunca dependeu disso:
|
|
40
|
+
// ele monta o `Authorization` na mão em `api()`. O que se perde é a
|
|
41
|
+
// credencial aparecer como tipo predefinido no nó HTTP Request.
|
|
39
42
|
test = {
|
|
40
43
|
request: {
|
|
41
44
|
baseURL: "={{$credentials.baseUrl}}",
|
|
42
45
|
url: "/api/v1/ping",
|
|
46
|
+
// Com `authenticate` fora, o cabeçalho do teste vem daqui.
|
|
47
|
+
headers: { Authorization: "=Bearer {{$credentials.apiKey}}" },
|
|
43
48
|
},
|
|
44
49
|
};
|
|
45
50
|
}
|
|
@@ -31,18 +31,45 @@ async function api(ctx, method, path, body, qs) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Lê um parâmetro que pode ser texto (nó v1) ou seletor de recurso (v2).
|
|
36
|
+
*
|
|
37
|
+
* O resourceLocator guarda `{ mode, value }` onde a v1 guardava a string
|
|
38
|
+
* crua. As duas versões convivem no mesmo `execute`, então quem lê
|
|
39
|
+
* normaliza aqui em vez de espalhar `typeof` pelo arquivo.
|
|
40
|
+
*/
|
|
41
|
+
function idDe(valor) {
|
|
42
|
+
if (valor && typeof valor === "object" && "value" in valor) {
|
|
43
|
+
return String(valor.value ?? "");
|
|
44
|
+
}
|
|
45
|
+
return valor ? String(valor) : "";
|
|
46
|
+
}
|
|
34
47
|
/** A conta escolhida no nó (vazio = a conta da própria chave). */
|
|
35
48
|
function contaQs(ctx, i) {
|
|
36
|
-
const conta = ctx.getNodeParameter("conta", i, "");
|
|
49
|
+
const conta = idDe(ctx.getNodeParameter("conta", i, ""));
|
|
37
50
|
return conta ? { conta } : {};
|
|
38
51
|
}
|
|
52
|
+
/** A conta escolhida, do ponto de vista dos carregadores de lista. */
|
|
53
|
+
function contaDeLista(ctx) {
|
|
54
|
+
try {
|
|
55
|
+
return idDe(ctx.getNodeParameter("conta", ""));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
39
61
|
class AibDigital {
|
|
40
62
|
description = {
|
|
41
63
|
displayName: "AIB Digital",
|
|
42
64
|
icon: "file:aibdigital.svg",
|
|
43
65
|
name: "aibDigital",
|
|
44
66
|
group: ["output"],
|
|
45
|
-
|
|
67
|
+
// v2 troca os menus de Conta/Ambiente pelo seletor de recurso do n8n
|
|
68
|
+
// (o "Da lista | Por ID", com busca) e ganha a Caixa de entrada. O que
|
|
69
|
+
// ele guarda muda de texto para `{ mode, value }`, então a v1 fica de
|
|
70
|
+
// pé: fluxo antigo continua na v1, nó novo nasce na v2.
|
|
71
|
+
version: [1, 2],
|
|
72
|
+
defaultVersion: 2,
|
|
46
73
|
subtitle: '={{$parameter["operation"]}}',
|
|
47
74
|
description: "Opera o Sistema AIB Digital: conversas, mensagens e funil",
|
|
48
75
|
defaults: { name: "AIB Digital" },
|
|
@@ -58,8 +85,31 @@ class AibDigital {
|
|
|
58
85
|
type: "options",
|
|
59
86
|
typeOptions: { loadOptionsMethod: "getContas" },
|
|
60
87
|
default: "",
|
|
88
|
+
displayOptions: { show: { "@version": [1] } },
|
|
61
89
|
description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
|
|
62
90
|
},
|
|
91
|
+
{
|
|
92
|
+
displayName: "Conta",
|
|
93
|
+
name: "conta",
|
|
94
|
+
type: "resourceLocator",
|
|
95
|
+
default: { mode: "list", value: "" },
|
|
96
|
+
displayOptions: { show: { "@version": [2] } },
|
|
97
|
+
description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
|
|
98
|
+
modes: [
|
|
99
|
+
{
|
|
100
|
+
displayName: "Da lista",
|
|
101
|
+
name: "list",
|
|
102
|
+
type: "list",
|
|
103
|
+
typeOptions: { searchListMethod: "buscarContas", searchable: true },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
displayName: "Por ID",
|
|
107
|
+
name: "id",
|
|
108
|
+
type: "string",
|
|
109
|
+
hint: "O UUID da conta. Vazio = a conta da própria chave.",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
63
113
|
// ── Recurso → Operação, como no nó do Chatwoot ──────────────
|
|
64
114
|
{
|
|
65
115
|
displayName: "Recurso",
|
|
@@ -73,6 +123,7 @@ class AibDigital {
|
|
|
73
123
|
{ name: "Mensagem", value: "mensagem" },
|
|
74
124
|
{ name: "Contato", value: "contato" },
|
|
75
125
|
{ name: "Funil (Kanban)", value: "funil" },
|
|
126
|
+
{ name: "Etiqueta", value: "etiqueta" },
|
|
76
127
|
],
|
|
77
128
|
},
|
|
78
129
|
{
|
|
@@ -137,6 +188,18 @@ class AibDigital {
|
|
|
137
188
|
{ name: "Criar etapa", value: "criarEtapa", action: "Criar etapa num funil" },
|
|
138
189
|
],
|
|
139
190
|
},
|
|
191
|
+
{
|
|
192
|
+
displayName: "Operação",
|
|
193
|
+
name: "operation",
|
|
194
|
+
type: "options",
|
|
195
|
+
noDataExpression: true,
|
|
196
|
+
default: "criarEtiqueta",
|
|
197
|
+
displayOptions: { show: { recurso: ["etiqueta"] } },
|
|
198
|
+
options: [
|
|
199
|
+
{ name: "Criar etiqueta", value: "criarEtiqueta", action: "Criar ou atualizar uma etiqueta" },
|
|
200
|
+
{ name: "Listar etiquetas", value: "listarEtiquetas", action: "Listar as etiquetas da conta" },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
140
203
|
// ── parâmetros por operação ─────────────────────────────
|
|
141
204
|
{
|
|
142
205
|
displayName: "Conversa (ID)",
|
|
@@ -252,8 +315,49 @@ class AibDigital {
|
|
|
252
315
|
type: "options",
|
|
253
316
|
typeOptions: { loadOptionsMethod: "getAmbientes", loadOptionsDependsOn: ["conta"] },
|
|
254
317
|
default: "",
|
|
255
|
-
displayOptions: {
|
|
318
|
+
displayOptions: {
|
|
319
|
+
show: { "@version": [1], operation: ["selecionarConta", "criarFunil", "criarEtiqueta"] },
|
|
320
|
+
},
|
|
321
|
+
description: "Onde o funil/dado vai nascer. Conta com 2+ ambientes exige escolher aqui.",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
displayName: "Ambiente",
|
|
325
|
+
name: "ambienteId",
|
|
326
|
+
type: "resourceLocator",
|
|
327
|
+
default: { mode: "list", value: "" },
|
|
328
|
+
displayOptions: {
|
|
329
|
+
show: { "@version": [2], operation: ["selecionarConta", "criarFunil", "criarEtiqueta"] },
|
|
330
|
+
},
|
|
256
331
|
description: "Onde o funil/dado vai nascer. Conta com 2+ ambientes exige escolher aqui.",
|
|
332
|
+
modes: [
|
|
333
|
+
{
|
|
334
|
+
displayName: "Da lista",
|
|
335
|
+
name: "list",
|
|
336
|
+
type: "list",
|
|
337
|
+
typeOptions: { searchListMethod: "buscarAmbientes", searchable: true },
|
|
338
|
+
},
|
|
339
|
+
{ displayName: "Por ID", name: "id", type: "string" },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
// A outra metade do que o atendimento precisa saber antes de agir:
|
|
344
|
+
// escolhida a conta, escolher o WhatsApp em que se opera. Fica na
|
|
345
|
+
// âncora do fluxo, para os nós seguintes lerem `.caixa_id`.
|
|
346
|
+
displayName: "Caixa de entrada",
|
|
347
|
+
name: "caixaId",
|
|
348
|
+
type: "resourceLocator",
|
|
349
|
+
default: { mode: "list", value: "" },
|
|
350
|
+
displayOptions: { show: { "@version": [2], operation: ["selecionarConta"] } },
|
|
351
|
+
description: "O WhatsApp da conta. Vazio = o fluxo não se prende a uma caixa.",
|
|
352
|
+
modes: [
|
|
353
|
+
{
|
|
354
|
+
displayName: "Da lista",
|
|
355
|
+
name: "list",
|
|
356
|
+
type: "list",
|
|
357
|
+
typeOptions: { searchListMethod: "buscarCaixas", searchable: true },
|
|
358
|
+
},
|
|
359
|
+
{ displayName: "Por ID", name: "id", type: "string" },
|
|
360
|
+
],
|
|
257
361
|
},
|
|
258
362
|
{
|
|
259
363
|
displayName: "Funil",
|
|
@@ -295,6 +399,28 @@ class AibDigital {
|
|
|
295
399
|
displayOptions: { show: { operation: ["criarEtapa"] } },
|
|
296
400
|
description: "Card que chega aqui conta como negócio perdido",
|
|
297
401
|
},
|
|
402
|
+
{
|
|
403
|
+
displayName: "Nome da etiqueta",
|
|
404
|
+
name: "etiquetaNome",
|
|
405
|
+
type: "string",
|
|
406
|
+
default: "",
|
|
407
|
+
required: true,
|
|
408
|
+
displayOptions: { show: { operation: ["criarEtiqueta"] } },
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
displayName: "Cor",
|
|
412
|
+
name: "etiquetaCor",
|
|
413
|
+
type: "color",
|
|
414
|
+
default: "",
|
|
415
|
+
displayOptions: { show: { operation: ["criarEtiqueta"] } },
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
displayName: "Descrição",
|
|
419
|
+
name: "etiquetaDescricao",
|
|
420
|
+
type: "string",
|
|
421
|
+
default: "",
|
|
422
|
+
displayOptions: { show: { operation: ["criarEtiqueta"] } },
|
|
423
|
+
},
|
|
298
424
|
{
|
|
299
425
|
displayName: "Contato (ID)",
|
|
300
426
|
name: "contatoId",
|
|
@@ -319,6 +445,80 @@ class AibDigital {
|
|
|
319
445
|
],
|
|
320
446
|
};
|
|
321
447
|
methods = {
|
|
448
|
+
// O seletor de recurso da v2. Devolve `{ results }` e filtra pelo que a
|
|
449
|
+
// pessoa digita — é o que dá a busca que o menu simples não tinha.
|
|
450
|
+
listSearch: {
|
|
451
|
+
async buscarContas(filtro) {
|
|
452
|
+
let contas = [];
|
|
453
|
+
try {
|
|
454
|
+
const res = await api(this, "GET", "/api/v1/contas");
|
|
455
|
+
contas = (res?.contas ?? []);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Chave de conta não lista contas (403): sobra ela mesma, que é
|
|
459
|
+
// o que o valor vazio já significa.
|
|
460
|
+
return { results: [{ name: "— A conta da chave —", value: "" }] };
|
|
461
|
+
}
|
|
462
|
+
const cred = (await this.getCredentials("aibDigitalApi"));
|
|
463
|
+
const base = String(cred?.baseUrl ?? "").replace(/\/+$/, "");
|
|
464
|
+
const alvo = (filtro ?? "").toLowerCase();
|
|
465
|
+
const results = [
|
|
466
|
+
{ name: "— A conta da chave —", value: "" },
|
|
467
|
+
...contas
|
|
468
|
+
.filter((c) => !alvo || c.nome.toLowerCase().includes(alvo))
|
|
469
|
+
.map((c) => ({
|
|
470
|
+
name: ["ativa", "ativo", "active"].includes(c.status) ? c.nome : `${c.nome} (suspensa)`,
|
|
471
|
+
value: c.id,
|
|
472
|
+
url: base ? `${base}/inbox` : undefined,
|
|
473
|
+
})),
|
|
474
|
+
];
|
|
475
|
+
return { results };
|
|
476
|
+
},
|
|
477
|
+
async buscarAmbientes(filtro) {
|
|
478
|
+
const conta = contaDeLista(this);
|
|
479
|
+
try {
|
|
480
|
+
const res = await api(this, "GET", "/api/v1/ambientes", undefined, conta ? { conta } : undefined);
|
|
481
|
+
const ambientes = (res?.ambientes ?? []);
|
|
482
|
+
if (ambientes.length <= 1) {
|
|
483
|
+
return {
|
|
484
|
+
results: [{ name: "— Único ambiente da conta —", value: ambientes[0]?.id ?? "" }],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const alvo = (filtro ?? "").toLowerCase();
|
|
488
|
+
return {
|
|
489
|
+
results: ambientes
|
|
490
|
+
.filter((a) => !alvo || a.nome.toLowerCase().includes(alvo))
|
|
491
|
+
.map((a) => ({ name: a.nome, value: a.id })),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return { results: [{ name: "— Único ambiente da conta —", value: "" }] };
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
async buscarCaixas(filtro) {
|
|
499
|
+
const conta = contaDeLista(this);
|
|
500
|
+
try {
|
|
501
|
+
const res = await api(this, "GET", "/api/v1/caixas", undefined, conta ? { conta } : undefined);
|
|
502
|
+
const caixas = (res?.caixas ?? []);
|
|
503
|
+
const alvo = (filtro ?? "").toLowerCase();
|
|
504
|
+
return {
|
|
505
|
+
results: caixas
|
|
506
|
+
.filter((c) => !alvo ||
|
|
507
|
+
c.nome?.toLowerCase().includes(alvo) ||
|
|
508
|
+
(c.telefone ?? "").includes(alvo))
|
|
509
|
+
// O telefone entra no rótulo porque é ele que distingue duas
|
|
510
|
+
// caixas com nome parecido — "Comercial" e "Comercial 2".
|
|
511
|
+
.map((c) => ({
|
|
512
|
+
name: c.telefone ? `${c.nome} · ${c.telefone}` : c.nome,
|
|
513
|
+
value: c.id,
|
|
514
|
+
})),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return { results: [] };
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
},
|
|
322
522
|
loadOptions: {
|
|
323
523
|
async getContas() {
|
|
324
524
|
try {
|
|
@@ -338,7 +538,7 @@ class AibDigital {
|
|
|
338
538
|
}
|
|
339
539
|
},
|
|
340
540
|
async getAmbientes() {
|
|
341
|
-
const conta = this
|
|
541
|
+
const conta = contaDeLista(this);
|
|
342
542
|
try {
|
|
343
543
|
const res = await api(this, "GET", "/api/v1/ambientes", undefined, conta ? { conta } : undefined);
|
|
344
544
|
const ambientes = (res?.ambientes ?? []);
|
|
@@ -352,13 +552,13 @@ class AibDigital {
|
|
|
352
552
|
}
|
|
353
553
|
},
|
|
354
554
|
async getFunis() {
|
|
355
|
-
const conta = this
|
|
555
|
+
const conta = contaDeLista(this);
|
|
356
556
|
const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
|
|
357
557
|
const funis = (res?.funis ?? []);
|
|
358
558
|
return funis.map((f) => ({ name: f.nome, value: f.id }));
|
|
359
559
|
},
|
|
360
560
|
async getEtapas() {
|
|
361
|
-
const conta = this
|
|
561
|
+
const conta = contaDeLista(this);
|
|
362
562
|
const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
|
|
363
563
|
const funis = (res?.funis ?? []);
|
|
364
564
|
return funis.flatMap((f) => f.etapas.map((e) => ({ name: `${f.nome} → ${e.nome}`, value: e.id })));
|
|
@@ -369,9 +569,10 @@ class AibDigital {
|
|
|
369
569
|
const items = this.getInputData();
|
|
370
570
|
const out = [];
|
|
371
571
|
let operation = this.getNodeParameter("operation", 0);
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
572
|
+
// Rede para o que ficou salvo antes: a credencial não declara mais
|
|
573
|
+
// `authenticate`, então o n8n parou de injetar o "__CUSTOM_API_CALL__"
|
|
574
|
+
// nas listas — mas um nó gravado com esse valor continua no banco do
|
|
575
|
+
// cliente. Em vez de falhar, assume a operação natural do recurso.
|
|
375
576
|
if (operation === "__CUSTOM_API_CALL__") {
|
|
376
577
|
const recurso = this.getNodeParameter("recurso", 0, "conversa");
|
|
377
578
|
const padrao = {
|
|
@@ -380,6 +581,7 @@ class AibDigital {
|
|
|
380
581
|
mensagem: "enviarMensagem",
|
|
381
582
|
contato: "atualizarContato",
|
|
382
583
|
funil: "criarFunil",
|
|
584
|
+
etiqueta: "criarEtiqueta",
|
|
383
585
|
};
|
|
384
586
|
operation = padrao[recurso] ?? "selecionarConta";
|
|
385
587
|
}
|
|
@@ -437,7 +639,7 @@ class AibDigital {
|
|
|
437
639
|
}
|
|
438
640
|
else if (operation === "criarFunil") {
|
|
439
641
|
const nome = this.getNodeParameter("funilNome", i);
|
|
440
|
-
const ambienteId = this.getNodeParameter("ambienteId", i, "");
|
|
642
|
+
const ambienteId = idDe(this.getNodeParameter("ambienteId", i, ""));
|
|
441
643
|
const bruto = this.getNodeParameter("funilEtapas", i, "[]");
|
|
442
644
|
const etapas = typeof bruto === "string" ? JSON.parse(bruto || "[]") : bruto;
|
|
443
645
|
res = await api(this, "POST", "/api/v1/funis", {
|
|
@@ -460,12 +662,26 @@ class AibDigital {
|
|
|
460
662
|
const campos = this.getNodeParameter("camposContato", i, {});
|
|
461
663
|
res = await api(this, "PATCH", `/api/v1/contatos/${contatoId}`, campos, qs);
|
|
462
664
|
}
|
|
665
|
+
else if (operation === "criarEtiqueta") {
|
|
666
|
+
const ambienteSel = idDe(this.getNodeParameter("ambienteId", i, ""));
|
|
667
|
+
res = await api(this, "POST", "/api/v1/etiquetas", {
|
|
668
|
+
nome: this.getNodeParameter("etiquetaNome", i),
|
|
669
|
+
...(this.getNodeParameter("etiquetaCor", i, "") ? { cor: this.getNodeParameter("etiquetaCor", i, "") } : {}),
|
|
670
|
+
...(this.getNodeParameter("etiquetaDescricao", i, "") ? { descricao: this.getNodeParameter("etiquetaDescricao", i, "") } : {}),
|
|
671
|
+
...(ambienteSel ? { ambiente_id: ambienteSel } : {}),
|
|
672
|
+
}, qs);
|
|
673
|
+
}
|
|
674
|
+
else if (operation === "listarEtiquetas") {
|
|
675
|
+
res = await api(this, "GET", "/api/v1/etiquetas", undefined, qs);
|
|
676
|
+
}
|
|
463
677
|
else if (operation === "selecionarConta") {
|
|
464
|
-
// A âncora do fluxo: devolve a conta
|
|
465
|
-
// nós seguintes referenciarem ($('Selecionar conta').item.json.id
|
|
466
|
-
// .ambiente_id). É onde o funil vai nascer
|
|
467
|
-
|
|
468
|
-
const
|
|
678
|
+
// A âncora do fluxo: devolve a conta, o ambiente E a caixa escolhidos
|
|
679
|
+
// para os nós seguintes referenciarem ($('Selecionar conta').item.json.id,
|
|
680
|
+
// .ambiente_id e .caixa_id). É onde o funil vai nascer e por onde o
|
|
681
|
+
// atendimento fala.
|
|
682
|
+
const contaSel = idDe(this.getNodeParameter("conta", i, ""));
|
|
683
|
+
const ambienteSel = idDe(this.getNodeParameter("ambienteId", i, ""));
|
|
684
|
+
const caixaSel = idDe(this.getNodeParameter("caixaId", i, ""));
|
|
469
685
|
let base;
|
|
470
686
|
if (contaSel) {
|
|
471
687
|
const r = await api(this, "GET", "/api/v1/contas");
|
|
@@ -479,6 +695,7 @@ class AibDigital {
|
|
|
479
695
|
base = { id: "", nome: "— a conta da chave —", escopo: ping?.escopo };
|
|
480
696
|
}
|
|
481
697
|
base.ambiente_id = ambienteSel || null;
|
|
698
|
+
base.caixa_id = caixaSel || null;
|
|
482
699
|
res = base;
|
|
483
700
|
}
|
|
484
701
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-aib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Nós do Sistema AIB Digital para n8n: gatilho de mensagens do WhatsApp (via AIB Inbox) e ações — enviar mensagem, mover card no funil, pausar IA — escolhendo conta e caixa por dropdown.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AIB Digital",
|