n8n-nodes-aib 0.5.1 → 0.7.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" },
|
|
@@ -52,15 +79,12 @@ class AibDigital {
|
|
|
52
79
|
outputs: ["main"],
|
|
53
80
|
credentials: [{ name: "aibDigitalApi", required: true }],
|
|
54
81
|
properties: [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
|
|
62
|
-
},
|
|
63
|
-
// ── Recurso → Operação, como no nó do Chatwoot ──────────────
|
|
82
|
+
// ── Recurso → Operação → Conta ──────────────────────────────
|
|
83
|
+
//
|
|
84
|
+
// Nesta ordem, e não com a Conta na frente: primeiro se diz O QUE
|
|
85
|
+
// fazer, depois ONDE. É como o nó do Chatwoot se apresenta, e é a
|
|
86
|
+
// ordem em que a pessoa pensa. A Conta na frente obrigava a
|
|
87
|
+
// escolher o cliente antes de saber o que se ia fazer nele.
|
|
64
88
|
{
|
|
65
89
|
displayName: "Recurso",
|
|
66
90
|
name: "recurso",
|
|
@@ -150,6 +174,37 @@ class AibDigital {
|
|
|
150
174
|
{ name: "Listar etiquetas", value: "listarEtiquetas", action: "Listar as etiquetas da conta" },
|
|
151
175
|
],
|
|
152
176
|
},
|
|
177
|
+
{
|
|
178
|
+
displayName: "Conta",
|
|
179
|
+
name: "conta",
|
|
180
|
+
type: "options",
|
|
181
|
+
typeOptions: { loadOptionsMethod: "getContas" },
|
|
182
|
+
default: "",
|
|
183
|
+
displayOptions: { show: { "@version": [1] } },
|
|
184
|
+
description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
displayName: "Conta",
|
|
188
|
+
name: "conta",
|
|
189
|
+
type: "resourceLocator",
|
|
190
|
+
default: { mode: "list", value: "" },
|
|
191
|
+
displayOptions: { show: { "@version": [2] } },
|
|
192
|
+
description: "O cliente. Com chave de agência é obrigatória — a API recusa o pedido sem ela. Com chave de conta, deixe vazio: é a conta da própria chave.",
|
|
193
|
+
modes: [
|
|
194
|
+
{
|
|
195
|
+
displayName: "Da lista",
|
|
196
|
+
name: "list",
|
|
197
|
+
type: "list",
|
|
198
|
+
typeOptions: { searchListMethod: "buscarContas", searchable: true },
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
displayName: "Por ID",
|
|
202
|
+
name: "id",
|
|
203
|
+
type: "string",
|
|
204
|
+
hint: "O UUID da conta. Vazio = a conta da própria chave.",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
153
208
|
// ── parâmetros por operação ─────────────────────────────
|
|
154
209
|
{
|
|
155
210
|
displayName: "Conversa (ID)",
|
|
@@ -265,8 +320,62 @@ class AibDigital {
|
|
|
265
320
|
type: "options",
|
|
266
321
|
typeOptions: { loadOptionsMethod: "getAmbientes", loadOptionsDependsOn: ["conta"] },
|
|
267
322
|
default: "",
|
|
268
|
-
displayOptions: {
|
|
323
|
+
displayOptions: {
|
|
324
|
+
show: { "@version": [1], operation: ["selecionarConta", "criarFunil", "criarEtiqueta"] },
|
|
325
|
+
},
|
|
326
|
+
description: "Onde o funil/dado vai nascer. Conta com 2+ ambientes exige escolher aqui.",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
// Onde o dado NASCE, então continua em primeiro plano: criar um
|
|
330
|
+
// funil ou uma etiqueta no ambiente errado é trabalho refeito.
|
|
331
|
+
displayName: "Ambiente",
|
|
332
|
+
name: "ambienteId",
|
|
333
|
+
type: "resourceLocator",
|
|
334
|
+
default: { mode: "list", value: "" },
|
|
335
|
+
displayOptions: {
|
|
336
|
+
show: { "@version": [2], operation: ["criarFunil", "criarEtiqueta"] },
|
|
337
|
+
},
|
|
269
338
|
description: "Onde o funil/dado vai nascer. Conta com 2+ ambientes exige escolher aqui.",
|
|
339
|
+
modes: [
|
|
340
|
+
{
|
|
341
|
+
displayName: "Da lista",
|
|
342
|
+
name: "list",
|
|
343
|
+
type: "list",
|
|
344
|
+
typeOptions: { searchListMethod: "buscarAmbientes", searchable: true },
|
|
345
|
+
},
|
|
346
|
+
{ displayName: "Por ID", name: "id", type: "string" },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
// Na âncora, Ambiente e Caixa são REFINAMENTO: sem eles o nó já
|
|
351
|
+
// responde, e a conta com um ambiente só — que é a maioria —
|
|
352
|
+
// nunca precisa tocar aqui. Soltos na tela, pareciam pendências e
|
|
353
|
+
// faziam a pergunta "preciso preencher isso?" a cada vez. Dentro
|
|
354
|
+
// de Opções, aparecem quando são procurados.
|
|
355
|
+
displayName: "Opções",
|
|
356
|
+
name: "opcoes",
|
|
357
|
+
type: "collection",
|
|
358
|
+
placeholder: "Escolher ambiente ou caixa",
|
|
359
|
+
default: {},
|
|
360
|
+
displayOptions: { show: { "@version": [2], operation: ["selecionarConta"] } },
|
|
361
|
+
options: [
|
|
362
|
+
{
|
|
363
|
+
displayName: "Ambiente",
|
|
364
|
+
name: "ambienteId",
|
|
365
|
+
type: "options",
|
|
366
|
+
typeOptions: { loadOptionsMethod: "getAmbientes", loadOptionsDependsOn: ["conta.value"] },
|
|
367
|
+
default: "",
|
|
368
|
+
description: "Vazio = o único ambiente da conta",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
displayName: "Caixa de Entrada",
|
|
372
|
+
name: "caixaId",
|
|
373
|
+
type: "options",
|
|
374
|
+
typeOptions: { loadOptionsMethod: "getCaixas", loadOptionsDependsOn: ["conta.value"] },
|
|
375
|
+
default: "",
|
|
376
|
+
description: "O WhatsApp da conta. Vazio = o fluxo não se prende a uma caixa.",
|
|
377
|
+
},
|
|
378
|
+
],
|
|
270
379
|
},
|
|
271
380
|
{
|
|
272
381
|
displayName: "Funil",
|
|
@@ -354,6 +463,80 @@ class AibDigital {
|
|
|
354
463
|
],
|
|
355
464
|
};
|
|
356
465
|
methods = {
|
|
466
|
+
// O seletor de recurso da v2. Devolve `{ results }` e filtra pelo que a
|
|
467
|
+
// pessoa digita — é o que dá a busca que o menu simples não tinha.
|
|
468
|
+
listSearch: {
|
|
469
|
+
async buscarContas(filtro) {
|
|
470
|
+
let contas = [];
|
|
471
|
+
try {
|
|
472
|
+
const res = await api(this, "GET", "/api/v1/contas");
|
|
473
|
+
contas = (res?.contas ?? []);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Chave de conta não lista contas (403): sobra ela mesma, que é
|
|
477
|
+
// o que o valor vazio já significa.
|
|
478
|
+
return { results: [{ name: "— A conta da chave —", value: "" }] };
|
|
479
|
+
}
|
|
480
|
+
const cred = (await this.getCredentials("aibDigitalApi"));
|
|
481
|
+
const base = String(cred?.baseUrl ?? "").replace(/\/+$/, "");
|
|
482
|
+
const alvo = (filtro ?? "").toLowerCase();
|
|
483
|
+
const results = [
|
|
484
|
+
{ name: "— A conta da chave —", value: "" },
|
|
485
|
+
...contas
|
|
486
|
+
.filter((c) => !alvo || c.nome.toLowerCase().includes(alvo))
|
|
487
|
+
.map((c) => ({
|
|
488
|
+
name: ["ativa", "ativo", "active"].includes(c.status) ? c.nome : `${c.nome} (suspensa)`,
|
|
489
|
+
value: c.id,
|
|
490
|
+
url: base ? `${base}/inbox` : undefined,
|
|
491
|
+
})),
|
|
492
|
+
];
|
|
493
|
+
return { results };
|
|
494
|
+
},
|
|
495
|
+
async buscarAmbientes(filtro) {
|
|
496
|
+
const conta = contaDeLista(this);
|
|
497
|
+
try {
|
|
498
|
+
const res = await api(this, "GET", "/api/v1/ambientes", undefined, conta ? { conta } : undefined);
|
|
499
|
+
const ambientes = (res?.ambientes ?? []);
|
|
500
|
+
if (ambientes.length <= 1) {
|
|
501
|
+
return {
|
|
502
|
+
results: [{ name: "— Único ambiente da conta —", value: ambientes[0]?.id ?? "" }],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const alvo = (filtro ?? "").toLowerCase();
|
|
506
|
+
return {
|
|
507
|
+
results: ambientes
|
|
508
|
+
.filter((a) => !alvo || a.nome.toLowerCase().includes(alvo))
|
|
509
|
+
.map((a) => ({ name: a.nome, value: a.id })),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return { results: [{ name: "— Único ambiente da conta —", value: "" }] };
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
async buscarCaixas(filtro) {
|
|
517
|
+
const conta = contaDeLista(this);
|
|
518
|
+
try {
|
|
519
|
+
const res = await api(this, "GET", "/api/v1/caixas", undefined, conta ? { conta } : undefined);
|
|
520
|
+
const caixas = (res?.caixas ?? []);
|
|
521
|
+
const alvo = (filtro ?? "").toLowerCase();
|
|
522
|
+
return {
|
|
523
|
+
results: caixas
|
|
524
|
+
.filter((c) => !alvo ||
|
|
525
|
+
c.nome?.toLowerCase().includes(alvo) ||
|
|
526
|
+
(c.telefone ?? "").includes(alvo))
|
|
527
|
+
// O telefone entra no rótulo porque é ele que distingue duas
|
|
528
|
+
// caixas com nome parecido — "Comercial" e "Comercial 2".
|
|
529
|
+
.map((c) => ({
|
|
530
|
+
name: c.telefone ? `${c.nome} · ${c.telefone}` : c.nome,
|
|
531
|
+
value: c.id,
|
|
532
|
+
})),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return { results: [] };
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
},
|
|
357
540
|
loadOptions: {
|
|
358
541
|
async getContas() {
|
|
359
542
|
try {
|
|
@@ -373,7 +556,7 @@ class AibDigital {
|
|
|
373
556
|
}
|
|
374
557
|
},
|
|
375
558
|
async getAmbientes() {
|
|
376
|
-
const conta = this
|
|
559
|
+
const conta = contaDeLista(this);
|
|
377
560
|
try {
|
|
378
561
|
const res = await api(this, "GET", "/api/v1/ambientes", undefined, conta ? { conta } : undefined);
|
|
379
562
|
const ambientes = (res?.ambientes ?? []);
|
|
@@ -386,14 +569,28 @@ class AibDigital {
|
|
|
386
569
|
return [{ name: "— Único ambiente da conta —", value: "" }];
|
|
387
570
|
}
|
|
388
571
|
},
|
|
572
|
+
async getCaixas() {
|
|
573
|
+
const conta = contaDeLista(this);
|
|
574
|
+
try {
|
|
575
|
+
const res = await api(this, "GET", "/api/v1/caixas", undefined, conta ? { conta } : undefined);
|
|
576
|
+
const caixas = (res?.caixas ?? []);
|
|
577
|
+
return caixas.map((c) => ({
|
|
578
|
+
name: c.telefone ? `${c.nome} · ${c.telefone}` : c.nome,
|
|
579
|
+
value: c.id,
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
},
|
|
389
586
|
async getFunis() {
|
|
390
|
-
const conta = this
|
|
587
|
+
const conta = contaDeLista(this);
|
|
391
588
|
const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
|
|
392
589
|
const funis = (res?.funis ?? []);
|
|
393
590
|
return funis.map((f) => ({ name: f.nome, value: f.id }));
|
|
394
591
|
},
|
|
395
592
|
async getEtapas() {
|
|
396
|
-
const conta = this
|
|
593
|
+
const conta = contaDeLista(this);
|
|
397
594
|
const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
|
|
398
595
|
const funis = (res?.funis ?? []);
|
|
399
596
|
return funis.flatMap((f) => f.etapas.map((e) => ({ name: `${f.nome} → ${e.nome}`, value: e.id })));
|
|
@@ -404,9 +601,10 @@ class AibDigital {
|
|
|
404
601
|
const items = this.getInputData();
|
|
405
602
|
const out = [];
|
|
406
603
|
let operation = this.getNodeParameter("operation", 0);
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
604
|
+
// Rede para o que ficou salvo antes: a credencial não declara mais
|
|
605
|
+
// `authenticate`, então o n8n parou de injetar o "__CUSTOM_API_CALL__"
|
|
606
|
+
// nas listas — mas um nó gravado com esse valor continua no banco do
|
|
607
|
+
// cliente. Em vez de falhar, assume a operação natural do recurso.
|
|
410
608
|
if (operation === "__CUSTOM_API_CALL__") {
|
|
411
609
|
const recurso = this.getNodeParameter("recurso", 0, "conversa");
|
|
412
610
|
const padrao = {
|
|
@@ -473,7 +671,7 @@ class AibDigital {
|
|
|
473
671
|
}
|
|
474
672
|
else if (operation === "criarFunil") {
|
|
475
673
|
const nome = this.getNodeParameter("funilNome", i);
|
|
476
|
-
const ambienteId = this.getNodeParameter("ambienteId", i, "");
|
|
674
|
+
const ambienteId = idDe(this.getNodeParameter("ambienteId", i, ""));
|
|
477
675
|
const bruto = this.getNodeParameter("funilEtapas", i, "[]");
|
|
478
676
|
const etapas = typeof bruto === "string" ? JSON.parse(bruto || "[]") : bruto;
|
|
479
677
|
res = await api(this, "POST", "/api/v1/funis", {
|
|
@@ -497,7 +695,7 @@ class AibDigital {
|
|
|
497
695
|
res = await api(this, "PATCH", `/api/v1/contatos/${contatoId}`, campos, qs);
|
|
498
696
|
}
|
|
499
697
|
else if (operation === "criarEtiqueta") {
|
|
500
|
-
const ambienteSel = this.getNodeParameter("ambienteId", i, "");
|
|
698
|
+
const ambienteSel = idDe(this.getNodeParameter("ambienteId", i, ""));
|
|
501
699
|
res = await api(this, "POST", "/api/v1/etiquetas", {
|
|
502
700
|
nome: this.getNodeParameter("etiquetaNome", i),
|
|
503
701
|
...(this.getNodeParameter("etiquetaCor", i, "") ? { cor: this.getNodeParameter("etiquetaCor", i, "") } : {}),
|
|
@@ -509,11 +707,19 @@ class AibDigital {
|
|
|
509
707
|
res = await api(this, "GET", "/api/v1/etiquetas", undefined, qs);
|
|
510
708
|
}
|
|
511
709
|
else if (operation === "selecionarConta") {
|
|
512
|
-
// A âncora do fluxo: devolve a conta
|
|
513
|
-
// nós seguintes referenciarem ($('Selecionar conta').item.json.id
|
|
514
|
-
// .ambiente_id). É onde o funil vai nascer
|
|
515
|
-
|
|
516
|
-
const
|
|
710
|
+
// A âncora do fluxo: devolve a conta, o ambiente E a caixa escolhidos
|
|
711
|
+
// para os nós seguintes referenciarem ($('Selecionar conta').item.json.id,
|
|
712
|
+
// .ambiente_id e .caixa_id). É onde o funil vai nascer e por onde o
|
|
713
|
+
// atendimento fala.
|
|
714
|
+
const contaSel = idDe(this.getNodeParameter("conta", i, ""));
|
|
715
|
+
// Ambiente e caixa mudaram de lugar na 0.7.0 (foram para `opcoes`).
|
|
716
|
+
// O nó gravado antes disso guardou os dois no primeiro nível, e
|
|
717
|
+
// continua valendo: lê-se a coleção e, se ela não disser nada, o
|
|
718
|
+
// campo antigo. Sem isso, um fluxo já montado perderia o ambiente
|
|
719
|
+
// em silêncio — e criaria o funil no lugar errado.
|
|
720
|
+
const opcoes = this.getNodeParameter("opcoes", i, {});
|
|
721
|
+
const ambienteSel = idDe(opcoes.ambienteId) || idDe(this.getNodeParameter("ambienteId", i, ""));
|
|
722
|
+
const caixaSel = idDe(opcoes.caixaId) || idDe(this.getNodeParameter("caixaId", i, ""));
|
|
517
723
|
let base;
|
|
518
724
|
if (contaSel) {
|
|
519
725
|
const r = await api(this, "GET", "/api/v1/contas");
|
|
@@ -527,6 +733,7 @@ class AibDigital {
|
|
|
527
733
|
base = { id: "", nome: "— a conta da chave —", escopo: ping?.escopo };
|
|
528
734
|
}
|
|
529
735
|
base.ambiente_id = ambienteSel || null;
|
|
736
|
+
base.caixa_id = caixaSel || null;
|
|
530
737
|
res = base;
|
|
531
738
|
}
|
|
532
739
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-aib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|